2026-08-29 16:48:15 +02:00
|
|
|
export interface Matchable {
|
|
|
|
|
title: string
|
|
|
|
|
artist: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalization for matching: lowercase, strip accents, strip punctuation,
|
|
|
|
|
* strip leading articles (the/a/an anywhere as standalone words), collapse
|
|
|
|
|
* whitespace. Spec definition of "normalized artist/title equal".
|
|
|
|
|
*/
|
|
|
|
|
export function normalize(input: string): string {
|
|
|
|
|
return input
|
|
|
|
|
.toLowerCase()
|
2026-08-29 16:54:59 +02:00
|
|
|
.replace(/æ/g, 'ae')
|
|
|
|
|
.replace(/œ/g, 'oe')
|
2026-08-29 16:48:15 +02:00
|
|
|
.normalize('NFKD')
|
|
|
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
|
|
|
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
|
|
|
.replace(/\b(the|a|an)\b/g, ' ')
|
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
|
.trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isConfidentMatch(a: Matchable, b: Matchable): boolean {
|
|
|
|
|
return normalize(a.title) === normalize(b.title) && normalize(a.artist) === normalize(b.artist)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Albums whose normalized title equals the release's normalized title —
|
|
|
|
|
* the "ambiguous" candidate list shown when no confident match exists.
|
|
|
|
|
* Candidates with matching artist sort first. Capped at 20.
|
|
|
|
|
*/
|
|
|
|
|
export function candidateAlbums<T extends Matchable>(release: Matchable, albums: T[]): T[] {
|
|
|
|
|
const title = normalize(release.title)
|
|
|
|
|
return albums
|
|
|
|
|
.filter((a) => normalize(a.title) === title)
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
const am = normalize(a.artist) === normalize(release.artist) ? 0 : 1
|
|
|
|
|
const bm = normalize(b.artist) === normalize(release.artist) ? 0 : 1
|
|
|
|
|
return am - bm
|
|
|
|
|
})
|
|
|
|
|
.slice(0, 20)
|
|
|
|
|
}
|