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:
@@ -70,6 +70,20 @@ func (q *Queries) CountMissingTracks(ctx context.Context) (int64, error) {
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countSuspectSourceTracks = `-- name: CountSuspectSourceTracks :one
|
||||
SELECT COUNT(*) FROM tracks t
|
||||
WHERE t.missing_since IS NULL
|
||||
AND regexp_replace(t.file_path, '^.*/', '') ~* $1::text
|
||||
`
|
||||
|
||||
// Total for the report's paging; the same filter as the list above.
|
||||
func (q *Queries) CountSuspectSourceTracks(ctx context.Context, pattern string) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countSuspectSourceTracks, pattern)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countTracksByAlbum = `-- name: CountTracksByAlbum :one
|
||||
SELECT count(*) FROM tracks WHERE album_id = $1
|
||||
`
|
||||
@@ -576,6 +590,89 @@ func (q *Queries) ListRandomTracksForUser(ctx context.Context, arg ListRandomTra
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listSuspectSourceTracks = `-- name: ListSuspectSourceTracks :many
|
||||
SELECT t.id,
|
||||
t.title,
|
||||
t.file_path,
|
||||
regexp_replace(t.file_path, '/[^/]*$', '') AS directory,
|
||||
t.duration_ms,
|
||||
t.disc_number,
|
||||
t.track_number,
|
||||
albums.id AS album_id,
|
||||
albums.title AS album_title,
|
||||
artists.id AS artist_id,
|
||||
artists.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
WHERE t.missing_since IS NULL
|
||||
AND regexp_replace(t.file_path, '^.*/', '') ~* $1::text
|
||||
ORDER BY directory, t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.title
|
||||
LIMIT $3 OFFSET $2
|
||||
`
|
||||
|
||||
type ListSuspectSourceTracksParams struct {
|
||||
Pattern string
|
||||
PageOffset int32
|
||||
PageLimit int32
|
||||
}
|
||||
|
||||
type ListSuspectSourceTracksRow struct {
|
||||
ID pgtype.UUID
|
||||
Title string
|
||||
FilePath string
|
||||
Directory string
|
||||
DurationMs int32
|
||||
DiscNumber *int32
|
||||
TrackNumber *int32
|
||||
AlbumID pgtype.UUID
|
||||
AlbumTitle string
|
||||
ArtistID pgtype.UUID
|
||||
ArtistName string
|
||||
}
|
||||
|
||||
// The admin report of present tracks whose filename looks like a video rip
|
||||
// (#5410): "(Official Video)", "[Audio]", "Visualizer" and the like.
|
||||
//
|
||||
// pattern is built in Go (internal/api/admin_suspect_sources.go) from the
|
||||
// same marker list that labels each row, so the filter and the labels cannot
|
||||
// drift. It is matched against the basename only: a folder called "Reaction
|
||||
// Sessions" says nothing about how a file was sourced.
|
||||
//
|
||||
// Ordered by directory, like ListMissingTracks, so the handler's fold gets
|
||||
// each folder as one contiguous run.
|
||||
func (q *Queries) ListSuspectSourceTracks(ctx context.Context, arg ListSuspectSourceTracksParams) ([]ListSuspectSourceTracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, listSuspectSourceTracks, arg.Pattern, arg.PageOffset, arg.PageLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListSuspectSourceTracksRow
|
||||
for rows.Next() {
|
||||
var i ListSuspectSourceTracksRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.FilePath,
|
||||
&i.Directory,
|
||||
&i.DurationMs,
|
||||
&i.DiscNumber,
|
||||
&i.TrackNumber,
|
||||
&i.AlbumID,
|
||||
&i.AlbumTitle,
|
||||
&i.ArtistID,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listTrackPathsForReconcile = `-- name: ListTrackPathsForReconcile :many
|
||||
SELECT id, file_path, missing_since FROM tracks
|
||||
`
|
||||
|
||||
@@ -263,3 +263,39 @@ SELECT t.id,
|
||||
-- Total for the admin surface's badge and paging. Uses the same partial index
|
||||
-- (tracks_missing_since_idx) as the list above.
|
||||
SELECT COUNT(*) FROM tracks WHERE missing_since IS NOT NULL;
|
||||
|
||||
-- name: ListSuspectSourceTracks :many
|
||||
-- The admin report of present tracks whose filename looks like a video rip
|
||||
-- (#5410): "(Official Video)", "[Audio]", "Visualizer" and the like.
|
||||
--
|
||||
-- pattern is built in Go (internal/api/admin_suspect_sources.go) from the
|
||||
-- same marker list that labels each row, so the filter and the labels cannot
|
||||
-- drift. It is matched against the basename only: a folder called "Reaction
|
||||
-- Sessions" says nothing about how a file was sourced.
|
||||
--
|
||||
-- Ordered by directory, like ListMissingTracks, so the handler's fold gets
|
||||
-- each folder as one contiguous run.
|
||||
SELECT t.id,
|
||||
t.title,
|
||||
t.file_path,
|
||||
regexp_replace(t.file_path, '/[^/]*$', '') AS directory,
|
||||
t.duration_ms,
|
||||
t.disc_number,
|
||||
t.track_number,
|
||||
albums.id AS album_id,
|
||||
albums.title AS album_title,
|
||||
artists.id AS artist_id,
|
||||
artists.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN albums ON albums.id = t.album_id
|
||||
JOIN artists ON artists.id = t.artist_id
|
||||
WHERE t.missing_since IS NULL
|
||||
AND regexp_replace(t.file_path, '^.*/', '') ~* sqlc.arg(pattern)::text
|
||||
ORDER BY directory, t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.title
|
||||
LIMIT sqlc.arg(page_limit) OFFSET sqlc.arg(page_offset);
|
||||
|
||||
-- name: CountSuspectSourceTracks :one
|
||||
-- Total for the report's paging; the same filter as the list above.
|
||||
SELECT COUNT(*) FROM tracks t
|
||||
WHERE t.missing_since IS NULL
|
||||
AND regexp_replace(t.file_path, '^.*/', '') ~* sqlc.arg(pattern)::text;
|
||||
|
||||
Reference in New Issue
Block a user