// ─── NEUE VERÖFFENTLICHUNGEN GEFOLGTER KÜNSTLER ─────────────────────────────── // Liefert zu einer Liste von Navidrome-Künstlern die jüngsten Deezer-Releases, damit die App eine // Zeitleiste "Neu von deinen Künstlern" zeigen kann. // // Warum serverseitig: bei 30 gefolgten Künstlern wären das 30 Deezer-Aufrufe — pro Gerät und pro // App-Start. Der Cache liegt deshalb hier und gilt für alle Clients gemeinsam. const { dzGet } = require('./deezer'); const _cache = new Map(); // deezerArtistId → { ts, alben } const _idCache = new Map(); // nameLower → { ts, id } const TTL = 6 * 60 * 60 * 1000; // 6 h — Alben erscheinen nicht minütlich const ID_TTL = 24 * 60 * 60 * 1000; // 24 h — die Zuordnung Name → Deezer ändert sich kaum const GLEICHZEITIG = 4; // höflich gegenüber Deezer, trotzdem zügig const coverXl = a => a?.cover_xl || a?.cover_big || a?.cover_medium || ''; // Künstler stilisieren ihren Namen mit führenden Zeichen (".diedlonely", "†…") — ohne das Abschneiden // findet Deezers Suche sie nicht. Gleiche Regel wie in deezer.js. const stripLeadSym = s => (s || '').replace(/^[^\p{L}\p{N}]+/u, '').trim(); /** Deezer-ID zu einem Künstler: erst die gepinnte, sonst über den Namen gesucht (und gemerkt). */ async function deezerIdFuer(name, pinId) { if (pinId) return String(pinId); const key = stripLeadSym(name).toLowerCase(); if (!key) return null; const c = _idCache.get(key); if (c && Date.now() - c.ts < ID_TTL) return c.id; try { const treffer = (await dzGet(`search/artist?q=${encodeURIComponent(stripLeadSym(name))}&limit=5`)).data || []; // Deezer sortiert nicht nach Bekanntheit — die Fan-Zahl trifft den gemeinten Künstler deutlich // öfter als der erste Treffer. Ein Pin schlägt das trotzdem, siehe oben. const best = treffer.slice().sort((a, b) => (b.nb_fan || 0) - (a.nb_fan || 0))[0]; const id = best ? String(best.id) : null; _idCache.set(key, { ts: Date.now(), id }); return id; } catch { return null; } } /** Albumliste eines Deezer-Künstlers, gecacht. */ async function albenFuer(deezerId) { const c = _cache.get(deezerId); if (c && Date.now() - c.ts < TTL) return c.alben; try { const alben = (await dzGet(`artist/${deezerId}/albums?limit=50`)).data || []; _cache.set(deezerId, { ts: Date.now(), alben }); return alben; } catch { // Bei einem Fehler lieber den alten Stand als gar nichts — abgelaufen, aber nicht falsch. return c ? c.alben : []; } } /** Führt eine Aufgabenliste mit begrenzter Gleichzeitigkeit aus. */ async function inHaeppchen(items, groesse, fn) { const raus = []; for (let i = 0; i < items.length; i += groesse) { raus.push(...await Promise.all(items.slice(i, i + groesse).map(fn))); } return raus; } /** * @param {Array<{id,name,deezerArtistId?}>} kuenstler Navidrome-Künstler (id + name) * @param {number} tage Zeitfenster * @returns {{releases:Array, ohneZuordnung:Array}} */ async function neueReleases(kuenstler, tage = 90) { const grenze = Date.now() - tage * 24 * 60 * 60 * 1000; const ohneZuordnung = []; const proKuenstler = await inHaeppchen(kuenstler, GLEICHZEITIG, async (k) => { const dzId = await deezerIdFuer(k.name, k.deezerArtistId); if (!dzId) { ohneZuordnung.push(k.name); return []; } const alben = await albenFuer(dzId); return alben .filter(a => a.release_date && new Date(a.release_date).getTime() >= grenze) .map(a => ({ artistId: k.id, artistName: k.name, deezerArtistId: dzId, albumId: String(a.id), title: a.title || '', cover: coverXl(a), releaseDate: a.release_date, recordType: a.record_type || 'album', trackCount: a.nb_tracks ?? null, explicit: !!a.explicit_lyrics, })); }); const releases = proKuenstler.flat() .sort((a, b) => new Date(b.releaseDate) - new Date(a.releaseDate)); return { releases, ohneZuordnung }; } module.exports = { neueReleases, deezerIdFuer, albenFuer, _cache, _idCache };