import type { DB } from './db.js' import { isConfidentMatch, normalize } from './matcher.js' export type RipStatus = 'ripped' | 'not_ripped' /** * Resolution order per spec: * 1. manual rip_override (non-null) wins * 2. else a stored match_link means ripped * 3. else confident fuzzy match against the user's digital_albums */ export function resolveRipStatus(db: DB, userId: number, itemId: number): RipStatus { const item = db .prepare('SELECT id, title, artist, rip_override FROM collection_items WHERE id = ? AND user_id = ?') .get(itemId, userId) as | { id: number; title: string; artist: string; rip_override: number | null } | undefined if (!item) return 'not_ripped' if (item.rip_override !== null && item.rip_override !== undefined) { return item.rip_override === 1 ? 'ripped' : 'not_ripped' } const link = db.prepare('SELECT album_id FROM match_links WHERE item_id = ?').get(itemId) if (link) return 'ripped' const albums = db .prepare('SELECT title, artist FROM digital_albums WHERE user_id = ?') .all(userId) as { title: string; artist: string }[] return albums.some((a) => isConfidentMatch(item, a)) ? 'ripped' : 'not_ripped' } /** * Batch resolution for list views: loads albums and match links once per * user instead of per item (GET /api/collection is otherwise O(items x albums)). * Resolution order matches resolveRipStatus. */ export function resolveRipStatusBatch( db: DB, userId: number, items: { id: number; title: string; artist: string; rip_override: number | null }[] ): RipStatus[] { const albums = db .prepare('SELECT title, artist FROM digital_albums WHERE user_id = ?') .all(userId) as { title: string; artist: string }[] const albumKeys = new Set( albums.map((a) => `${normalize(a.title)}|${normalize(a.artist)}`) ) const linked = new Set( ( db.prepare('SELECT item_id FROM match_links WHERE user_id = ?').all(userId) as { item_id: number }[] ).map((r) => r.item_id) ) return items.map((item) => { if (item.rip_override !== null && item.rip_override !== undefined) { return item.rip_override === 1 ? 'ripped' : 'not_ripped' } if (linked.has(item.id)) return 'ripped' const key = `${normalize(item.title)}|${normalize(item.artist)}` return albumKeys.has(key) ? 'ripped' : 'not_ripped' }) } export interface MatchedAlbum { id: number subsonicId: string lastPlayedAt: string | null } /** Album behind an item's rip match — link wins, else confident fuzzy match. Independent of rip_override. */ export function findMatchedAlbum(db: DB, userId: number, itemId: number): MatchedAlbum | null { const item = db .prepare('SELECT id, title, artist FROM collection_items WHERE id = ? AND user_id = ?') .get(itemId, userId) as { id: number; title: string; artist: string } | undefined if (!item) return null const link = db .prepare( `SELECT da.id, da.subsonic_id, da.last_played_at FROM match_links ml JOIN digital_albums da ON da.id = ml.album_id WHERE ml.item_id = ?` ) .get(itemId) as { id: number; subsonic_id: string; last_played_at: string | null } | undefined if (link) { return { id: link.id, subsonicId: link.subsonic_id, lastPlayedAt: link.last_played_at } } const album = db .prepare( `SELECT id, subsonic_id, last_played_at, title, artist FROM digital_albums WHERE user_id = ?` ) .all(userId) as { id: number; subsonic_id: string; last_played_at: string | null; title: string; artist: string }[] const match = album.find((a) => isConfidentMatch(item, a)) return match ? { id: match.id, subsonicId: match.subsonic_id, lastPlayedAt: match.last_played_at } : null }