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
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:
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user