Notifications inbox (M489), Discover taste-arm fix, web tooling majors #151
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pageTitle } from '$lib/branding';
|
import { pageTitle } from '$lib/branding';
|
||||||
import { Copy } from 'lucide-svelte';
|
import { Copy, TriangleAlert } from 'lucide-svelte';
|
||||||
import {
|
import {
|
||||||
createDuplicatesQuery,
|
createDuplicatesQuery,
|
||||||
runDuplicateSweep,
|
runDuplicateSweep,
|
||||||
@@ -122,6 +122,27 @@
|
|||||||
return n === 1 ? `1 ${one}` : `${n} ${many}`;
|
return n === 1 ? `1 ${one}` : `${n} ${many}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identical audio under different titles means at least one file's tags are
|
||||||
|
// wrong, and the recording the other title names may not be in the library at
|
||||||
|
// all. #3885: "WWW" was a second copy of the instrumental, and the vocal was
|
||||||
|
// missing with nothing to say so. Exact tier only: an acoustic match spans
|
||||||
|
// encodings, where a "Remastered" suffix is routine rather than a lie.
|
||||||
|
function differingTitles(g: AdminDuplicateGroup): string[] {
|
||||||
|
if (g.tier !== 'exact') return [];
|
||||||
|
const byKey = new Map<string, string>();
|
||||||
|
for (const m of g.members) {
|
||||||
|
const title = m.title.trim().replace(/\s+/g, ' ');
|
||||||
|
const key = title.toLowerCase();
|
||||||
|
if (!byKey.has(key)) byKey.set(key, title);
|
||||||
|
}
|
||||||
|
return byKey.size > 1 ? [...byKey.values()] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function quotedList(titles: string[]): string {
|
||||||
|
const q = titles.map((t) => `“${t}”`);
|
||||||
|
return q.length <= 2 ? q.join(' and ') : `${q.slice(0, -1).join(', ')} and ${q[q.length - 1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
// What the copy carries is the fact that decides which to keep.
|
// What the copy carries is the fact that decides which to keep.
|
||||||
function historyLabel(m: AdminDuplicateMember): string {
|
function historyLabel(m: AdminDuplicateMember): string {
|
||||||
if (m.like_count === 0 && m.play_count === 0) return 'no likes or plays';
|
if (m.like_count === 0 && m.play_count === 0) return 'no likes or plays';
|
||||||
@@ -224,6 +245,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<ul class="space-y-4">
|
<ul class="space-y-4">
|
||||||
{#each groups as group (group.id)}
|
{#each groups as group (group.id)}
|
||||||
|
{@const titles = differingTitles(group)}
|
||||||
<li class="overflow-hidden rounded-lg border border-border bg-surface" data-testid="duplicate-group">
|
<li class="overflow-hidden rounded-lg border border-border bg-surface" data-testid="duplicate-group">
|
||||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-3">
|
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
@@ -258,6 +280,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if titles.length > 0}
|
||||||
|
<p
|
||||||
|
class="flex items-start gap-2 border-b border-border bg-surface-hover px-4 py-3 text-xs text-text-secondary"
|
||||||
|
data-testid="title-mismatch"
|
||||||
|
>
|
||||||
|
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-warning-fg" aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
The audio is identical, but the copies are titled {quotedList(titles)}. If those
|
||||||
|
name different recordings, one file is mislabelled and the other recording isn't in
|
||||||
|
your library. Keep the copy whose title matches what you hear.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if confirmingMerge === group.id}
|
{#if confirmingMerge === group.id}
|
||||||
<!-- What the second click will do, said plainly before it is done. -->
|
<!-- What the second click will do, said plainly before it is done. -->
|
||||||
<div class="space-y-2 border-b border-border bg-surface-hover px-4 py-3 text-sm" data-testid="merge-confirm">
|
<div class="space-y-2 border-b border-border bg-surface-hover px-4 py-3 text-sm" data-testid="merge-confirm">
|
||||||
|
|||||||
@@ -124,6 +124,61 @@ describe('admin duplicates', () => {
|
|||||||
expect(text(screen.getByTestId('duplicate-tier'))).toBe('Same recording · 96% match');
|
expect(text(screen.getByTestId('duplicate-tier'))).toBe('Same recording · 96% match');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #3885: identical audio titled "WWW" and "WWW (instrumental)". One file
|
||||||
|
// claimed to be the vocal and wasn't, and nothing said the vocal was missing.
|
||||||
|
describe('identical audio under different titles', () => {
|
||||||
|
function www() {
|
||||||
|
const r = response();
|
||||||
|
r.groups[0] = {
|
||||||
|
...r.groups[0],
|
||||||
|
members: [member('www-01'), member('www-02', { title: 'WWW (instrumental)' })]
|
||||||
|
};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('says the copies are titled differently and names both titles', () => {
|
||||||
|
renderWith(www());
|
||||||
|
const notice = text(screen.getByTestId('title-mismatch'));
|
||||||
|
expect(notice).toContain('titled “WWW” and “WWW (instrumental)”');
|
||||||
|
expect(notice).toContain("isn't in your library");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('three different titles are all named', () => {
|
||||||
|
const r = www();
|
||||||
|
r.groups[0].members.push(member('www-03', { title: 'WWW (Radio Edit)' }));
|
||||||
|
renderWith(r);
|
||||||
|
expect(text(screen.getByTestId('title-mismatch'))).toContain(
|
||||||
|
'“WWW”, “WWW (instrumental)” and “WWW (Radio Edit)”'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('titles differing only in case or spacing are the same title', () => {
|
||||||
|
const r = response();
|
||||||
|
r.groups[0] = {
|
||||||
|
...r.groups[0],
|
||||||
|
members: [
|
||||||
|
member('www-01', { title: 'WWW (Instrumental)' }),
|
||||||
|
member('www-02', { title: ' www (instrumental) ' })
|
||||||
|
]
|
||||||
|
};
|
||||||
|
renderWith(r);
|
||||||
|
expect(screen.queryByTestId('title-mismatch')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matching titles raise nothing', () => {
|
||||||
|
renderWith(response());
|
||||||
|
expect(screen.queryByTestId('title-mismatch')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Across encodings a "Remastered" suffix is routine, not a mislabel.
|
||||||
|
test('an acoustic group with different titles raises nothing', () => {
|
||||||
|
const r = www();
|
||||||
|
r.groups[0] = { ...r.groups[0], tier: 'acoustic', worst_bit_error_rate: 0.04 };
|
||||||
|
renderWith(r);
|
||||||
|
expect(screen.queryByTestId('title-mismatch')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Three states all show zero groups. Each must say which it is.
|
// Three states all show zero groups. Each must say which it is.
|
||||||
test('empty while still fingerprinting says so', () => {
|
test('empty while still fingerprinting says so', () => {
|
||||||
renderWith(
|
renderWith(
|
||||||
|
|||||||
Reference in New Issue
Block a user