feat(discovery): acquire recommendations that keep their names
Acquisition ran yt-dlp without --embed-metadata, so every download arrived untagged. The scanner then stored the video id as the title and "Unknown Artist" as the artist, the vetted-candidate tag check rejected the mismatch, and all 18 acquired tracks were hidden and retired. - Pass --embed-metadata so downloads carry real tags. - Let a scan take fallback title/artist from the candidate, for sources that still ship untagged files. - Install Deno alongside yt-dlp: YouTube guards some formats with a JS challenge yt-dlp must execute, and no other runtime is enabled. - Dedupe candidates by artist and title. The (source, external_id) key misses the same song reaching us under two Deezer release ids. Also carries the in-flight discovery work this builds on: the Recommendations page replacing Discover, the discovery source service, and the acquisition spec tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,13 @@ export interface ScanContext {
|
||||
sourceType?: 'MANUAL' | 'RECOMMENDATION';
|
||||
probationStatus?: 'probation' | 'retained' | 'retired';
|
||||
candidateId?: string;
|
||||
/**
|
||||
* Names to use when the file carries no title/artist tags. An acquired
|
||||
* download often has none, and the filename ("VTKqlmCpTmQ.mp3") plus
|
||||
* "Unknown Artist" is worse than the vetted candidate's own names.
|
||||
*/
|
||||
fallbackTitle?: string;
|
||||
fallbackArtist?: string;
|
||||
}
|
||||
|
||||
export interface ScanResult {
|
||||
@@ -31,7 +38,7 @@ export interface ScanResult {
|
||||
* in the title ("Song (feat. X)") are folded in either way. First artist is the
|
||||
* main artist; the rest are featured. See ./utils/artist-names for the rules.
|
||||
*/
|
||||
function parseArtistsFromMetadata(common: any): { main: string; featured: string[] } {
|
||||
function parseArtistsFromMetadata(common: any, fallbackArtist = 'Unknown Artist'): { main: string; featured: string[] } {
|
||||
const rawTitle = common.title || '';
|
||||
|
||||
// Structured array present: still split each entry (tags sometimes put a whole
|
||||
@@ -52,7 +59,7 @@ function parseArtistsFromMetadata(common: any): { main: string; featured: string
|
||||
}
|
||||
}
|
||||
|
||||
return parseArtists(common.artist || 'Unknown Artist', rawTitle);
|
||||
return parseArtists(common.artist || fallbackArtist, rawTitle);
|
||||
}
|
||||
|
||||
function hashFile(filePath: string): Promise<string> {
|
||||
@@ -136,8 +143,9 @@ export class ScannerService {
|
||||
const { common, format } = metadata;
|
||||
|
||||
// 1. Ensure Artist(s) exist.
|
||||
const trackTitle = common.title || path.basename(filePath);
|
||||
const { main: mainArtistRaw, featured: featuredArtistNames } = parseArtistsFromMetadata(common);
|
||||
const trackTitle = common.title || context.fallbackTitle || path.basename(filePath);
|
||||
const { main: mainArtistRaw, featured: featuredArtistNames } =
|
||||
parseArtistsFromMetadata(common, context.fallbackArtist || 'Unknown Artist');
|
||||
|
||||
const { id: artistId, name: resolvedArtist } = await this.resolveOrCreateArtist(mainArtistRaw);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user