Use strict album-title-artist existence matching and add exists/missing result filter
This commit is contained in:
parent
a42e5e2542
commit
b021df31b4
3 changed files with 114 additions and 11 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
const queryInput = document.getElementById('query');
|
const queryInput = document.getElementById('query');
|
||||||
const searchBtn = document.getElementById('searchBtn');
|
const searchBtn = document.getElementById('searchBtn');
|
||||||
const searchTypeSelect = document.getElementById('searchType');
|
const searchTypeSelect = document.getElementById('searchType');
|
||||||
|
const existsFilterSelect = document.getElementById('existsFilter');
|
||||||
const results = document.getElementById('results');
|
const results = document.getElementById('results');
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const dialog = document.getElementById('albumDialog');
|
const dialog = document.getElementById('albumDialog');
|
||||||
|
|
@ -23,6 +24,7 @@ let selectedAlbum = null;
|
||||||
let sendContext = null;
|
let sendContext = null;
|
||||||
let playlistTracks = [];
|
let playlistTracks = [];
|
||||||
let jobs = [];
|
let jobs = [];
|
||||||
|
let lastSearch = { type: null, items: [] };
|
||||||
|
|
||||||
const CLEANUP_KEY = 'cleanupExtras';
|
const CLEANUP_KEY = 'cleanupExtras';
|
||||||
cleanupToggle.checked = localStorage.getItem(CLEANUP_KEY) === 'true';
|
cleanupToggle.checked = localStorage.getItem(CLEANUP_KEY) === 'true';
|
||||||
|
|
@ -128,12 +130,14 @@ function createPlaylistTrackCard(track, idx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(type, items) {
|
function renderItems(type, items) {
|
||||||
|
lastSearch = { type, items: Array.isArray(items) ? items : [] };
|
||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
if (!items.length) {
|
const filtered = applyExistsFilter(lastSearch.items, type);
|
||||||
|
if (!filtered.length) {
|
||||||
results.innerHTML = '<p>Keine Treffer gefunden.</p>';
|
results.innerHTML = '<p>Keine Treffer gefunden.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const item of items) {
|
for (const item of filtered) {
|
||||||
let card;
|
let card;
|
||||||
if (type === 'track') card = createTrackCard(item);
|
if (type === 'track') card = createTrackCard(item);
|
||||||
if (type === 'artist') card = createArtistCard(item);
|
if (type === 'artist') card = createArtistCard(item);
|
||||||
|
|
@ -142,6 +146,19 @@ function renderItems(type, items) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyExistsFilter(items, type) {
|
||||||
|
const mode = existsFilterSelect.value || 'all';
|
||||||
|
if (mode === 'all') return items;
|
||||||
|
if (type === 'artist') return items;
|
||||||
|
|
||||||
|
return (items || []).filter((item) => {
|
||||||
|
const exists = Boolean(item.exists);
|
||||||
|
if (mode === 'exists') return exists;
|
||||||
|
if (mode === 'missing') return !exists;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateDialogActionButton() {
|
function updateDialogActionButton() {
|
||||||
const rows = Array.from(trackList.querySelectorAll('input[type="checkbox"]'));
|
const rows = Array.from(trackList.querySelectorAll('input[type="checkbox"]'));
|
||||||
const selected = rows.filter((x) => x.checked).map((x) => ({
|
const selected = rows.filter((x) => x.checked).map((x) => ({
|
||||||
|
|
@ -344,13 +361,16 @@ async function updateFrontendFromGit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlaylist() {
|
function renderPlaylist() {
|
||||||
|
lastSearch = { type: 'playlist-track', items: playlistTracks.slice() };
|
||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
if (!playlistTracks.length) {
|
const filtered = applyExistsFilter(playlistTracks, 'track');
|
||||||
|
if (!filtered.length) {
|
||||||
results.innerHTML = '<p>Keine Playlist-Tracks gefunden.</p>';
|
results.innerHTML = '<p>Keine Playlist-Tracks gefunden.</p>';
|
||||||
playlistActions.style.display = 'none';
|
playlistActions.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
playlistTracks.forEach((track, idx) => {
|
filtered.forEach((track) => {
|
||||||
|
const idx = playlistTracks.indexOf(track);
|
||||||
const card = createPlaylistTrackCard(track, idx);
|
const card = createPlaylistTrackCard(track, idx);
|
||||||
const cb = card.querySelector('input[type="checkbox"]');
|
const cb = card.querySelector('input[type="checkbox"]');
|
||||||
cb.addEventListener('change', () => {
|
cb.addEventListener('change', () => {
|
||||||
|
|
@ -433,5 +453,14 @@ updateLibraryBtn.addEventListener('click', updateLibrary);
|
||||||
updateFrontendBtn.addEventListener('click', updateFrontendFromGit);
|
updateFrontendBtn.addEventListener('click', updateFrontendFromGit);
|
||||||
importPlaylistBtn.addEventListener('click', importPlaylist);
|
importPlaylistBtn.addEventListener('click', importPlaylist);
|
||||||
sendPlaylistBtn.addEventListener('click', sendPlaylistSelection);
|
sendPlaylistBtn.addEventListener('click', sendPlaylistSelection);
|
||||||
|
existsFilterSelect.addEventListener('change', () => {
|
||||||
|
if (lastSearch.type === 'playlist-track') {
|
||||||
|
renderPlaylist();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lastSearch.type) {
|
||||||
|
renderItems(lastSearch.type, lastSearch.items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
renderJobs();
|
renderJobs();
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,11 @@
|
||||||
<option value="track">Tracks</option>
|
<option value="track">Tracks</option>
|
||||||
<option value="artist">Interpreten</option>
|
<option value="artist">Interpreten</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select id="existsFilter">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
<option value="exists">Nur vorhanden</option>
|
||||||
|
<option value="missing">Nur nicht vorhanden</option>
|
||||||
|
</select>
|
||||||
<input id="query" placeholder="Album oder Artist eingeben" />
|
<input id="query" placeholder="Album oder Artist eingeben" />
|
||||||
<button id="searchBtn">Suchen</button>
|
<button id="searchBtn">Suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
83
server.js
83
server.js
|
|
@ -30,6 +30,7 @@ let spotifyTokenCache = {
|
||||||
let libraryIndexCache = {
|
let libraryIndexCache = {
|
||||||
byTitle: new Map(),
|
byTitle: new Map(),
|
||||||
byAlbum: new Map(),
|
byAlbum: new Map(),
|
||||||
|
byTrackTriplet: new Map(),
|
||||||
trackById: new Map(),
|
trackById: new Map(),
|
||||||
updatedAt: 0
|
updatedAt: 0
|
||||||
};
|
};
|
||||||
|
|
@ -91,6 +92,13 @@ function normalizeTitle(title) {
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStrict(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function buildTrackTitleKey(title) {
|
function buildTrackTitleKey(title) {
|
||||||
return normalizeTitle(title);
|
return normalizeTitle(title);
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +107,10 @@ function buildAlbumKey(albumName, artistName) {
|
||||||
return `${normalize(albumName)}::${normalize(artistName)}`;
|
return `${normalize(albumName)}::${normalize(artistName)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTrackTripletKey(trackName, albumName, artistName) {
|
||||||
|
return `${normalizeStrict(trackName)}::${normalizeStrict(albumName)}::${normalizeStrict(artistName)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function hasFileForTrack(track) {
|
function hasFileForTrack(track) {
|
||||||
return Boolean(track?.hasFile || track?.trackFileId || track?.fileId);
|
return Boolean(track?.hasFile || track?.trackFileId || track?.fileId);
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +119,7 @@ function extractTrackName(track) {
|
||||||
return track?.title || track?.trackTitle || '';
|
return track?.title || track?.trackTitle || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTrackToLibraryIndex(track) {
|
function addTrackToLibraryIndex(track, albumMeta = null) {
|
||||||
const titleKey = buildTrackTitleKey(extractTrackName(track));
|
const titleKey = buildTrackTitleKey(extractTrackName(track));
|
||||||
if (!titleKey) return;
|
if (!titleKey) return;
|
||||||
|
|
||||||
|
|
@ -119,22 +131,42 @@ function addTrackToLibraryIndex(track) {
|
||||||
id: track.id,
|
id: track.id,
|
||||||
albumId: track.albumId,
|
albumId: track.albumId,
|
||||||
title: extractTrackName(track),
|
title: extractTrackName(track),
|
||||||
|
albumName: albumMeta?.albumName || '',
|
||||||
|
artistName: albumMeta?.artistName || '',
|
||||||
hasFile: hasFileForTrack(track)
|
hasFile: hasFileForTrack(track)
|
||||||
};
|
};
|
||||||
libraryIndexCache.byTitle.get(titleKey).push(entry);
|
libraryIndexCache.byTitle.get(titleKey).push(entry);
|
||||||
if (track.id) {
|
if (track.id) {
|
||||||
libraryIndexCache.trackById.set(track.id, entry);
|
libraryIndexCache.trackById.set(track.id, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tripletKey = buildTrackTripletKey(entry.title, entry.albumName, entry.artistName);
|
||||||
|
if (tripletKey) {
|
||||||
|
if (!libraryIndexCache.byTrackTriplet.has(tripletKey)) {
|
||||||
|
libraryIndexCache.byTrackTriplet.set(tripletKey, []);
|
||||||
|
}
|
||||||
|
libraryIndexCache.byTrackTriplet.get(tripletKey).push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildLibraryIndex() {
|
async function rebuildLibraryIndex() {
|
||||||
const albums = await lidarrRequest('get', '/api/v1/album');
|
const albums = await lidarrRequest('get', '/api/v1/album');
|
||||||
const byTitle = new Map();
|
const byTitle = new Map();
|
||||||
const byAlbum = new Map();
|
const byAlbum = new Map();
|
||||||
|
const byTrackTriplet = new Map();
|
||||||
const trackById = new Map();
|
const trackById = new Map();
|
||||||
|
|
||||||
const prev = libraryIndexCache;
|
const prev = libraryIndexCache;
|
||||||
libraryIndexCache = { byTitle, byAlbum, trackById, updatedAt: Date.now() };
|
libraryIndexCache = { byTitle, byAlbum, byTrackTriplet, trackById, updatedAt: Date.now() };
|
||||||
|
|
||||||
|
const albumMetaById = new Map();
|
||||||
|
for (const album of albums || []) {
|
||||||
|
if (!album?.id) continue;
|
||||||
|
albumMetaById.set(album.id, {
|
||||||
|
albumName: album.title || album.albumTitle || '',
|
||||||
|
artistName: album.artistName || album.artist?.artistName || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let trackCount = 0;
|
let trackCount = 0;
|
||||||
for (const album of albums || []) {
|
for (const album of albums || []) {
|
||||||
|
|
@ -146,7 +178,7 @@ async function rebuildLibraryIndex() {
|
||||||
tracks = [];
|
tracks = [];
|
||||||
}
|
}
|
||||||
for (const track of tracks || []) {
|
for (const track of tracks || []) {
|
||||||
addTrackToLibraryIndex(track);
|
addTrackToLibraryIndex(track, albumMetaById.get(album.id));
|
||||||
trackCount += 1;
|
trackCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +209,36 @@ async function ensureLibraryIndexFresh(maxAgeMs = 5 * 60 * 1000) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTrackLibraryState(trackName) {
|
function getTrackLibraryState(trackName, albumName, artistName) {
|
||||||
|
const tripletKey = buildTrackTripletKey(trackName, albumName, artistName);
|
||||||
|
const tripletMatches = libraryIndexCache.byTrackTriplet.get(tripletKey) || [];
|
||||||
|
if (tripletMatches.length > 0) {
|
||||||
|
const existing = tripletMatches.some((m) => m.hasFile);
|
||||||
|
return {
|
||||||
|
exists: existing,
|
||||||
|
matchCount: tripletMatches.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback only when no album/artist context exists.
|
||||||
|
if (!albumName && !artistName) {
|
||||||
|
const key = buildTrackTitleKey(trackName);
|
||||||
|
const matches = libraryIndexCache.byTitle.get(key) || [];
|
||||||
|
const existing = matches.some((m) => m.hasFile);
|
||||||
|
return {
|
||||||
|
exists: existing,
|
||||||
|
matchCount: matches.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict mode with context: no full triplet match means "not existing".
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
matchCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackLibraryStateLoose(trackName) {
|
||||||
const key = buildTrackTitleKey(trackName);
|
const key = buildTrackTitleKey(trackName);
|
||||||
const matches = libraryIndexCache.byTitle.get(key) || [];
|
const matches = libraryIndexCache.byTitle.get(key) || [];
|
||||||
const existing = matches.some((m) => m.hasFile);
|
const existing = matches.some((m) => m.hasFile);
|
||||||
|
|
@ -631,7 +692,11 @@ app.get('/api/spotify/search', async (req, res) => {
|
||||||
}
|
}
|
||||||
if (type === 'track') {
|
if (type === 'track') {
|
||||||
items = (data.tracks?.items || []).map((track) => ({
|
items = (data.tracks?.items || []).map((track) => ({
|
||||||
...getTrackLibraryState(track.name),
|
...getTrackLibraryState(
|
||||||
|
track.name,
|
||||||
|
track.album?.name || '',
|
||||||
|
track.album?.artists?.[0]?.name || track.artists?.[0]?.name || ''
|
||||||
|
),
|
||||||
id: track.id,
|
id: track.id,
|
||||||
trackName: track.name,
|
trackName: track.name,
|
||||||
artist: track.artists?.map((a) => a.name).join(', ') || 'Unbekannt',
|
artist: track.artists?.map((a) => a.name).join(', ') || 'Unbekannt',
|
||||||
|
|
@ -698,7 +763,11 @@ app.get('/api/spotify/album/:id', async (req, res) => {
|
||||||
name: track.name,
|
name: track.name,
|
||||||
durationMs: track.duration_ms,
|
durationMs: track.duration_ms,
|
||||||
trackNumber: track.track_number,
|
trackNumber: track.track_number,
|
||||||
exists: getTrackLibraryState(track.name).exists
|
exists: getTrackLibraryState(
|
||||||
|
track.name,
|
||||||
|
album.name,
|
||||||
|
album.artists?.[0]?.name || 'Unbekannt'
|
||||||
|
).exists
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -734,7 +803,7 @@ app.post('/api/import/playlist', async (req, res) => {
|
||||||
|
|
||||||
const enriched = tracks.map((t) => ({
|
const enriched = tracks.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
exists: getTrackLibraryState(t.trackName).exists
|
exists: getTrackLibraryState(t.trackName, t.albumName, t.albumArtist || t.artist).exists
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ success: true, tracks: enriched });
|
res.json({ success: true, tracks: enriched });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue