Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e96e5fd87 |
@@ -6,6 +6,7 @@ The MVP reports truth Spooty already owns without a schema migration:
|
|||||||
|
|
||||||
- track state from `TrackEntity.status`
|
- track state from `TrackEntity.status`
|
||||||
- selected YouTube URL from `TrackEntity.youtubeUrl`
|
- selected YouTube URL from `TrackEntity.youtubeUrl`
|
||||||
|
- selected YouTube candidate title, author, score, and reason from `TrackEntity`
|
||||||
- raw failure evidence from `TrackEntity.error`
|
- raw failure evidence from `TrackEntity.error`
|
||||||
- download retry count from `TrackEntity.downloadAttemptCount`
|
- download retry count from `TrackEntity.downloadAttemptCount`
|
||||||
- rejected YouTube candidates from `TrackEntity.rejectedYoutubeUrls`
|
- rejected YouTube candidates from `TrackEntity.rejectedYoutubeUrls`
|
||||||
@@ -13,11 +14,10 @@ The MVP reports truth Spooty already owns without a schema migration:
|
|||||||
- queue depth and active jobs from BullMQ queues `track-search-processor` and `track-download-processor`
|
- queue depth and active jobs from BullMQ queues `track-search-processor` and `track-download-processor`
|
||||||
- Spotify connection presence from the stored Spotify user token
|
- Spotify connection presence from the stored Spotify user token
|
||||||
|
|
||||||
The endpoint intentionally does not infer candidate scoring history. Today, selected candidate title, author, score, and reason are logged during search but are not persisted. Rejected candidates are persisted only as URL strings.
|
The endpoint intentionally does not infer candidate scoring history. Selected candidate metadata is persisted when Spooty chooses a candidate. Rejected candidates are still persisted only as URL strings.
|
||||||
|
|
||||||
Future persistence work is required for:
|
Future persistence work is required for:
|
||||||
|
|
||||||
- selected candidate title, author, score, and scoring reason
|
|
||||||
- full searched candidate history
|
- full searched candidate history
|
||||||
- structured rejected candidate metadata beyond URL
|
- structured rejected candidate metadata beyond URL
|
||||||
- structured error class fields stored at write time instead of classified from the persisted raw error string
|
- structured error class fields stored at write time instead of classified from the persisted raw error string
|
||||||
|
|||||||
@@ -176,6 +176,25 @@ export class OperationsService {
|
|||||||
status: TrackStatusEnum[track.status] || String(track.status),
|
status: TrackStatusEnum[track.status] || String(track.status),
|
||||||
...(track.spotifyUrl ? { spotifyUrl: track.spotifyUrl } : {}),
|
...(track.spotifyUrl ? { spotifyUrl: track.spotifyUrl } : {}),
|
||||||
...(track.youtubeUrl ? { youtubeUrl: track.youtubeUrl } : {}),
|
...(track.youtubeUrl ? { youtubeUrl: track.youtubeUrl } : {}),
|
||||||
|
...(track.youtubeUrl
|
||||||
|
? {
|
||||||
|
selectedCandidate: {
|
||||||
|
url: track.youtubeUrl,
|
||||||
|
...(track.selectedYoutubeTitle
|
||||||
|
? { title: track.selectedYoutubeTitle }
|
||||||
|
: {}),
|
||||||
|
...(track.selectedYoutubeAuthor
|
||||||
|
? { author: track.selectedYoutubeAuthor }
|
||||||
|
: {}),
|
||||||
|
...(typeof track.selectedYoutubeScore === 'number'
|
||||||
|
? { score: track.selectedYoutubeScore }
|
||||||
|
: {}),
|
||||||
|
...(track.selectedYoutubeReason
|
||||||
|
? { reason: track.selectedYoutubeReason }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(typeof track.downloadAttemptCount === 'number'
|
...(typeof track.downloadAttemptCount === 'number'
|
||||||
? { downloadAttemptCount: track.downloadAttemptCount }
|
? { downloadAttemptCount: track.downloadAttemptCount }
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ export interface TrackTruth {
|
|||||||
status: string;
|
status: string;
|
||||||
spotifyUrl?: string;
|
spotifyUrl?: string;
|
||||||
youtubeUrl?: string;
|
youtubeUrl?: string;
|
||||||
|
selectedCandidate?: {
|
||||||
|
url: string;
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
score?: number;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
downloadAttemptCount?: number;
|
downloadAttemptCount?: number;
|
||||||
errorClass?: ErrorClass;
|
errorClass?: ErrorClass;
|
||||||
errorSummary?: string;
|
errorSummary?: string;
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ export class TrackEntity {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
youtubeUrl?: string;
|
youtubeUrl?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
selectedYoutubeTitle?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
selectedYoutubeAuthor?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
selectedYoutubeScore?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text' })
|
||||||
|
selectedYoutubeReason?: string;
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'text' })
|
@Column({ nullable: true, type: 'text' })
|
||||||
rejectedYoutubeUrls?: string;
|
rejectedYoutubeUrls?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ export class TrackService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(track.rejectedYoutubeUrls);
|
const parsed = JSON.parse(track.rejectedYoutubeUrls);
|
||||||
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
|
return Array.isArray(parsed)
|
||||||
|
? parsed.filter((item) => typeof item === 'string')
|
||||||
|
: [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -125,6 +127,10 @@ export class TrackService {
|
|||||||
await this.update(id, {
|
await this.update(id, {
|
||||||
...track,
|
...track,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
selectedYoutubeTitle: null,
|
||||||
|
selectedYoutubeAuthor: null,
|
||||||
|
selectedYoutubeScore: null,
|
||||||
|
selectedYoutubeReason: null,
|
||||||
rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls),
|
rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls),
|
||||||
downloadAttemptCount: 0,
|
downloadAttemptCount: 0,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -161,6 +167,10 @@ export class TrackService {
|
|||||||
updatedTrack = {
|
updatedTrack = {
|
||||||
...track,
|
...track,
|
||||||
youtubeUrl: youtubeMatch.url,
|
youtubeUrl: youtubeMatch.url,
|
||||||
|
selectedYoutubeTitle: youtubeMatch.title,
|
||||||
|
selectedYoutubeAuthor: youtubeMatch.author,
|
||||||
|
selectedYoutubeScore: youtubeMatch.score,
|
||||||
|
selectedYoutubeReason: youtubeMatch.reason,
|
||||||
status: TrackStatusEnum.Queued,
|
status: TrackStatusEnum.Queued,
|
||||||
};
|
};
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@@ -193,7 +203,9 @@ export class TrackService {
|
|||||||
const existingJob = await this.trackSearchQueue.getJob(jobId);
|
const existingJob = await this.trackSearchQueue.getJob(jobId);
|
||||||
|
|
||||||
if (existingJob) {
|
if (existingJob) {
|
||||||
this.logger.warn(`Removing existing search job for track ${id} before enqueue`);
|
this.logger.warn(
|
||||||
|
`Removing existing search job for track ${id} before enqueue`,
|
||||||
|
);
|
||||||
await existingJob.remove();
|
await existingJob.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +224,9 @@ export class TrackService {
|
|||||||
const existingJob = await this.trackDownloadQueue.getJob(jobId);
|
const existingJob = await this.trackDownloadQueue.getJob(jobId);
|
||||||
|
|
||||||
if (existingJob) {
|
if (existingJob) {
|
||||||
this.logger.warn(`Removing existing download job for track ${id} before enqueue`);
|
this.logger.warn(
|
||||||
|
`Removing existing download job for track ${id} before enqueue`,
|
||||||
|
);
|
||||||
await existingJob.remove();
|
await existingJob.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +310,10 @@ export class TrackService {
|
|||||||
await this.update(track.id, {
|
await this.update(track.id, {
|
||||||
...track,
|
...track,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
selectedYoutubeTitle: null,
|
||||||
|
selectedYoutubeAuthor: null,
|
||||||
|
selectedYoutubeScore: null,
|
||||||
|
selectedYoutubeReason: null,
|
||||||
rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls),
|
rejectedYoutubeUrls: this.stringifyRejectedYoutubeUrls(rejectedUrls),
|
||||||
downloadAttemptCount: nextAttemptCount,
|
downloadAttemptCount: nextAttemptCount,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -320,7 +338,8 @@ export class TrackService {
|
|||||||
const safeArtist = track.artist || 'unknown_artist';
|
const safeArtist = track.artist || 'unknown_artist';
|
||||||
const safeName = track.name || 'unknown_track';
|
const safeName = track.name || 'unknown_track';
|
||||||
const fileName = `${safeArtist} - ${safeName}`;
|
const fileName = `${safeArtist} - ${safeName}`;
|
||||||
const format = this.configService.get<string>(EnvironmentEnum.FORMAT) || 'mp3';
|
const format =
|
||||||
|
this.configService.get<string>(EnvironmentEnum.FORMAT) || 'mp3';
|
||||||
return `${this.utilsService.stripFileIllegalChars(fileName)}.${format}`;
|
return `${this.utilsService.stripFileIllegalChars(fileName)}.${format}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user