1
0
Files
record-shop/server/src/ripstatus.ts

99 lines
3.6 KiB
TypeScript
Raw Normal View History

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
}