feat: ranked library search with match-quality tiers
Library cache search was returning results in alphabetical order with no ranking. 'massive' showed Blanck Mass before Massive Attack because B comes before M. Now all matches are collected, scored by match quality, and sorted: Artists: exact match = 100, starts-with = 90, substring = 70, fuzzy = 50 Albums (same tiers as remote rgMatchTier): artist-exact = 100, artist-contains = 85, title-exact = 80, title-starts-with = 75, title-contains = 60, fuzzy = 40 Results are sorted by score descending, then alphabetically as tiebreaker. Cap increased from 5 to 10 per entity type to show more library content.
This commit is contained in:
@@ -637,47 +637,89 @@ export class ExploreView extends LitElement {
|
|||||||
private searchLibraryCache(query: string): MBSearchResult | null {
|
private searchLibraryCache(query: string): MBSearchResult | null {
|
||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
|
|
||||||
const artists: MBArtist[] = [];
|
// Collect all matching artists with match-quality scores.
|
||||||
|
const artistMatches: Array<{ artist: any; score: number }> = [];
|
||||||
const cachedArtists = libraryStore.cachedArtists;
|
const cachedArtists = libraryStore.cachedArtists;
|
||||||
if (cachedArtists) {
|
if (cachedArtists) {
|
||||||
for (const a of cachedArtists) {
|
for (const a of cachedArtists) {
|
||||||
if (fuzzyMatch(q, a.Name.toLowerCase())) {
|
const name = a.Name.toLowerCase();
|
||||||
artists.push({
|
if (!fuzzyMatch(q, name)) continue;
|
||||||
mbid: a.MBID || '',
|
|
||||||
name: a.Name,
|
// Score by match quality (same tiers as remote search).
|
||||||
sortName: '',
|
let score: number;
|
||||||
type: '',
|
if (name === q) {
|
||||||
country: '',
|
score = 100; // exact
|
||||||
disambiguation: '',
|
} else if (name.startsWith(q)) {
|
||||||
score: 100,
|
score = 90; // starts with
|
||||||
_imageSmall: a.ImageSmall || '',
|
} else if (name.includes(q)) {
|
||||||
_imageMedium: a.ImageMedium || '',
|
score = 70; // substring
|
||||||
_inLibrary: true,
|
} else {
|
||||||
} as MBArtist & { _imageSmall: string; _imageMedium: string; _inLibrary: boolean });
|
score = 50; // fuzzy/word match
|
||||||
if (artists.length >= 5) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
artistMatches.push({ artist: a, score });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseGroups: MBReleaseGroup[] = [];
|
// Sort by score descending, then alphabetically.
|
||||||
|
artistMatches.sort((a, b) => b.score - a.score || a.artist.Name.localeCompare(b.artist.Name));
|
||||||
|
|
||||||
|
const artists: MBArtist[] = artistMatches.slice(0, 10).map((m) => ({
|
||||||
|
mbid: m.artist.MBID || '',
|
||||||
|
name: m.artist.Name,
|
||||||
|
sortName: '',
|
||||||
|
type: '',
|
||||||
|
country: '',
|
||||||
|
disambiguation: '',
|
||||||
|
score: m.score,
|
||||||
|
_imageSmall: m.artist.ImageSmall || '',
|
||||||
|
_imageMedium: m.artist.ImageMedium || '',
|
||||||
|
_inLibrary: true,
|
||||||
|
} as MBArtist & { _imageSmall: string; _imageMedium: string; _inLibrary: boolean }));
|
||||||
|
|
||||||
|
// Collect all matching albums with match-quality scores.
|
||||||
|
const albumMatches: Array<{ album: any; score: number }> = [];
|
||||||
const cachedAlbums = libraryStore.cachedAlbums;
|
const cachedAlbums = libraryStore.cachedAlbums;
|
||||||
if (cachedAlbums) {
|
if (cachedAlbums) {
|
||||||
for (const a of cachedAlbums) {
|
for (const a of cachedAlbums) {
|
||||||
if (fuzzyMatch(q, a.Name.toLowerCase()) || fuzzyMatch(q, a.ArtistName.toLowerCase())) {
|
const name = a.Name.toLowerCase();
|
||||||
releaseGroups.push({
|
const artist = a.ArtistName.toLowerCase();
|
||||||
mbid: a.MBID || '',
|
const matchesName = fuzzyMatch(q, name);
|
||||||
title: a.Name,
|
const matchesArtist = fuzzyMatch(q, artist);
|
||||||
primaryType: 'Album',
|
if (!matchesName && !matchesArtist) continue;
|
||||||
artistCredit: a.ArtistName,
|
|
||||||
firstReleaseDate: a.Year ? String(a.Year) : '',
|
let score: number;
|
||||||
_coverArt: a.CoverArtMedium || a.CoverArtSmall || '',
|
// Artist name match is strongest (same as remote rgMatchTier).
|
||||||
_inLibrary: true,
|
if (artist === q) {
|
||||||
} as MBReleaseGroup & { _coverArt: string; _inLibrary: boolean });
|
score = 100;
|
||||||
if (releaseGroups.length >= 5) break;
|
} else if (artist.startsWith(q) || artist.includes(q)) {
|
||||||
|
score = 85;
|
||||||
|
} else if (name === q) {
|
||||||
|
score = 80;
|
||||||
|
} else if (name.startsWith(q)) {
|
||||||
|
score = 75;
|
||||||
|
} else if (name.includes(q)) {
|
||||||
|
score = 60;
|
||||||
|
} else {
|
||||||
|
score = 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
albumMatches.push({ album: a, score });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
albumMatches.sort((a, b) => b.score - a.score || a.album.Name.localeCompare(b.album.Name));
|
||||||
|
|
||||||
|
const releaseGroups: MBReleaseGroup[] = albumMatches.slice(0, 10).map((m) => ({
|
||||||
|
mbid: m.album.MBID || '',
|
||||||
|
title: m.album.Name,
|
||||||
|
primaryType: 'Album',
|
||||||
|
artistCredit: m.album.ArtistName,
|
||||||
|
firstReleaseDate: m.album.Year ? String(m.album.Year) : '',
|
||||||
|
_coverArt: m.album.CoverArtMedium || m.album.CoverArtSmall || '',
|
||||||
|
_inLibrary: true,
|
||||||
|
} as MBReleaseGroup & { _coverArt: string; _inLibrary: boolean }));
|
||||||
|
|
||||||
if (artists.length === 0 && releaseGroups.length === 0) {
|
if (artists.length === 0 && releaseGroups.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user