feat: admin Suspect sources report, for files named like video rips (#5410)
release / govulncheck (push) Successful in 37s
release / go (push) Failing after 1m10s
release / web (push) Failing after 1m24s
release / integration (push) Successful in 4m38s
release / android (push) Successful in 5m5s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (releases and dev) (push) Canceled after 5m18s

Humanz turned out to be YouTube rips: "(Official Video)", "Visualizer", a
reaction video filed as a song, junk disc numbers (#5401). The library holds
about 200 more files named the same way. This report lists them, grouped by
folder like Missing files, with the markers each file name carries.

- GET /api/admin/library/suspect-sources: one marker list in Go builds both
  the Postgres ~* filter and each row's labels, so they cannot drift.
  Basename only; missing files are left out.
- Markers calibrated on the live library: "live in/at" dropped (real live
  albums), "reaction" narrowed (it caught "Chain Reaction").
- Admin tab "Suspect sources", read-only, loads as you scroll; a folder split
  across pages is joined back into one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:51:01 -04:00
co-authored by Claude Opus 5.5
parent 918db27fec
commit 3f0540cb7a
12 changed files with 775 additions and 2 deletions
+30 -1
View File
@@ -1,9 +1,10 @@
import { createQuery } from '@tanstack/svelte-query';
import { createInfiniteQuery, createQuery } from '@tanstack/svelte-query';
import { api } from './client';
import { qk } from './queries';
import type {
ActionResult,
AdminMissingResponse,
AdminSuspectResponse,
AdminDuplicatesResponse,
MergeDuplicateResult,
AdminPlaybackError,
@@ -896,6 +897,34 @@ export function createMissingFilesQuery(offset: number = 0, limit: number = 50)
});
}
// Suspect sources (#5410) --------------------------------------------------
export const SUSPECT_SOURCES_PAGE_SIZE = 50;
export async function listSuspectSources(offset: number = 0): Promise<AdminSuspectResponse> {
return api.get<AdminSuspectResponse>(
`/api/admin/library/suspect-sources?limit=${SUSPECT_SOURCES_PAGE_SIZE}&offset=${offset}`
);
}
// The next offset counts tracks, not groups: limit and total are in tracks.
export function suspectSourcesNextOffset(last: AdminSuspectResponse): number | undefined {
const loaded = last.offset + last.groups.reduce((n, g) => n + g.tracks.length, 0);
return loaded >= last.total ? undefined : loaded;
}
// Loads as the page scrolls (rule 172). Changes only when files are added or
// removed, so it is not re-fetched on every focus.
export function createSuspectSourcesQuery() {
return createInfiniteQuery({
queryKey: qk.adminSuspectSources(),
queryFn: ({ pageParam }) => listSuspectSources(pageParam as number),
initialPageParam: 0,
getNextPageParam: suspectSourcesNextOffset,
staleTime: 120_000
});
}
// Missing-file re-acquisition (#2527 / milestone #290) ----------------------
export type ReacquisitionSettings = {
+1
View File
@@ -58,6 +58,7 @@ export const qk = {
['adminMissingFiles', { offset: offset ?? 0 }] as const,
adminDuplicates: (offset?: number) =>
['adminDuplicates', { offset: offset ?? 0 }] as const,
adminSuspectSources: () => ['adminSuspectSources'] as const,
adminDiagnosticDevices: (userId?: string) =>
['adminDiagnosticDevices', { userId: userId ?? 'all' }] as const,
smtpConfig: () => ['smtpConfig'] as const,
+31
View File
@@ -419,6 +419,37 @@ export type AdminMissingResponse = {
groups: AdminMissingGroup[];
};
// Suspect sources (#5410) --------------------------------------------------
// A present track whose filename carries a video-rip marker. markers are the
// reasons, read from the file's basename: "music video", "[Audio]", "MV"...
export type AdminSuspectTrack = {
track_id: string;
title: string;
artist_id: string;
artist_name: string;
album_id: string;
album_title: string;
file_path: string;
duration_sec: number;
disc_number: number | null;
track_number: number | null;
markers: string[];
};
// A folder's worth of flagged tracks; the server orders and groups by folder.
export type AdminSuspectGroup = {
directory: string;
tracks: AdminSuspectTrack[];
};
export type AdminSuspectResponse = {
total: number;
limit: number;
offset: number;
groups: AdminSuspectGroup[];
};
// Duplicates report (#3912) -------------------------------------------------
// One copy in a proposed duplicate group. like_count and play_count cover every