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:
@@ -69,6 +69,22 @@ import type {
|
||||
import { AUDIO_PREFERENCE_BUCKETS, FEEDBACK_ACTIONS } from '../db/types.js';
|
||||
export * from '../db/types.js';
|
||||
|
||||
/**
|
||||
* Every play_history write goes through this statement.
|
||||
*
|
||||
* The title/artist copy is taken in the same round trip via LEFT JOIN, so a
|
||||
* play stays readable after its track is hard-deleted, and the join never
|
||||
* suppresses the insert when the track id is unknown.
|
||||
*/
|
||||
const PLAY_HISTORY_INSERT = `
|
||||
INSERT INTO play_history (
|
||||
user_id, track_id, batch_id, completed, listened_ms, track_title, track_artist
|
||||
)
|
||||
SELECT $1::uuid, $2::uuid, $3::uuid, $4::boolean, $5::int, t.title, t.artist
|
||||
FROM (SELECT 1) AS always
|
||||
LEFT JOIN tracks t ON t.id = $2::uuid
|
||||
RETURNING id`;
|
||||
|
||||
export class DbService {
|
||||
/** Exposed so route handlers (e.g. settings) can query the database directly. */
|
||||
readonly pgClient: Pool;
|
||||
@@ -483,11 +499,17 @@ export class DbService {
|
||||
// (permanentlyDeleteTrack, below) and exactly one unlink site in the whole
|
||||
// system (workers/src/cleanup.service.ts).
|
||||
|
||||
async recordPlay(userId: string, trackId: string, completed: boolean, batchId?: string): Promise<string> {
|
||||
async recordPlay(
|
||||
userId: string,
|
||||
trackId: string,
|
||||
completed: boolean,
|
||||
batchId?: string,
|
||||
listenedMs?: number,
|
||||
): Promise<string> {
|
||||
if (!completed) {
|
||||
const res = await this.pgClient.query(
|
||||
'INSERT INTO play_history (user_id, track_id, batch_id, completed) VALUES ($1, $2, $3, $4) RETURNING id',
|
||||
[userId, trackId, batchId ?? null, false]
|
||||
PLAY_HISTORY_INSERT,
|
||||
[userId, trackId, batchId ?? null, false, listenedMs ?? null]
|
||||
);
|
||||
return res.rows[0].id as string;
|
||||
}
|
||||
@@ -497,8 +519,8 @@ export class DbService {
|
||||
return this.withTransaction(async (client) => {
|
||||
// 1. Record play history
|
||||
const insertRes = await client.query(
|
||||
'INSERT INTO play_history (user_id, track_id, batch_id, completed) VALUES ($1, $2, $3, $4) RETURNING id',
|
||||
[userId, trackId, batchId ?? null, true]
|
||||
PLAY_HISTORY_INSERT,
|
||||
[userId, trackId, batchId ?? null, true, listenedMs ?? null]
|
||||
);
|
||||
const historyId = insertRes.rows[0].id as string;
|
||||
|
||||
@@ -1936,10 +1958,17 @@ export class DbService {
|
||||
const occurredAt = event.occurred_at?.toISOString?.() ?? new Date().toISOString();
|
||||
switch (event.type) {
|
||||
case 'completed':
|
||||
// The Vibe player is the one caller that knows how much was actually
|
||||
// heard. position_ms is where playback reached; duration_ms is the
|
||||
// fallback for a client that only reported the track length.
|
||||
await client.query(
|
||||
`INSERT INTO play_history (user_id, track_id, completed, played_at)
|
||||
VALUES ($1, $2, true, $3::timestamptz)`,
|
||||
[event.user_id, event.track_id, occurredAt],
|
||||
`INSERT INTO play_history (
|
||||
user_id, track_id, completed, played_at, listened_ms, track_title, track_artist
|
||||
)
|
||||
SELECT $1::uuid, $2::uuid, true, $3::timestamptz, $4::int, t.title, t.artist
|
||||
FROM (SELECT 1) AS always
|
||||
LEFT JOIN tracks t ON t.id = $2::uuid`,
|
||||
[event.user_id, event.track_id, occurredAt, event.position_ms ?? event.duration_ms ?? null],
|
||||
);
|
||||
await client.query(
|
||||
`UPDATE tracks
|
||||
|
||||
Reference in New Issue
Block a user