Files
NovaSonic/public/sw.js
T
HoneyandClaude Opus 4.8 1e69831dd8 feat: move the whole metadata/search stack from iTunes to Deezer (1.3)
Apple's iTunes /search returns 403 from server/datacenter IPs, which had broken global
search, charts, album/artist/single downloads and the metadata-editor autofill in
production. Everything now sources metadata from Deezer (free, no key, more requests,
returns explicit versions + real artist images + durations). Web + Android share one
source, so an artist resolves to one consistent name everywhere. Deezer results are
mapped to the legacy "ItunesItem" shape, so the UI + download flow are unchanged.

Backend:
- New backend/lib/deezer.js: lookupTrackMeta, fetchAlbumTracks, searchAlbumTracks,
  fetchArtistAlbums (+ pure pickTrackMatch/mapAlbumTrack for tests).
- metadata.js drops the iTunes lookups; novasonic-server download paths (track, album,
  artist, playlist enrich, spotify artwork) + autofill now use Deezer.
- Artist download uses Deezer discography (real albums only), not the iTunes album search.
- Editor autofill endpoint /api/itunes/lookup -> /api/meta/lookup; removed the dead
  iTunes /search proxy and the Apple charts RSS proxy.
- backend/test/deezer.test.js: unit tests for numbering, mixed-artist album-artist
  stability, explicit flags, the Japanese-title match guard (npm test; RUN_LIVE=1 for live).

Web:
- api.js search/lookups already on Deezer; charts now use Deezer /chart. Artist cards
  show the real Deezer photo. Visible "iTunes"/"Apple Music" labels -> "Deezer"
  (search badge, charts badge, preview bar, editor button, artist heading). SW ns-v51.

Android:
- New DeezerModels.kt with Dz* models + mappers to ItunesItem/ItunesRssEntry.
- ItunesClient is Deezer-backed (search = tracks+albums+artists in parallel; album/artist
  lookups; /chart charts), same method signatures. Visible strings -> Deezer.

Docs: README, CLAUDE.md and CHANGELOG (1.3) updated; Platform Parity Matrix reflects the
shared Deezer source + Android non-local artist page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 12:48:33 +02:00

78 lines
2.6 KiB
JavaScript

const CACHE = 'ns-v51';
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
// The actual fetch still uses the full URL (with token); only the stored key is normalized.
const STRIP_PARAMS = ['t', 's', 'u', 'v', 'c', 'f'];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)));
self.skipWaiting();
});
self.addEventListener('activate', e => {
e.waitUntil(
caches.keys()
.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
.then(() => self.clients.claim())
.then(() => self.clients.matchAll({ type: 'window', includeUncontrolled: true }))
.then(clients => clients.forEach(c => c.postMessage({ type: 'SW_UPDATED', version: CACHE })))
);
});
self.addEventListener('message', e => {
if (e.data?.type === 'GET_VERSION') {
e.source.postMessage({ type: 'VERSION', version: CACHE });
}
});
// Build a stable cache key by stripping Navidrome auth params
function _cacheKey(url) {
try {
const u = new URL(url);
STRIP_PARAMS.forEach(p => u.searchParams.delete(p));
return u.toString();
} catch {
return url;
}
}
self.addEventListener('fetch', e => {
if (!e.request.url.startsWith(self.location.origin)) return;
if (SKIP_PATHS.some(p => e.request.url.includes(p))) return;
const isCoverArt = e.request.url.includes('/rest/getCoverArt');
if (isCoverArt) {
// Cache-first for artwork — serves instantly from SW cache even when Doze blocks network.
// Key is token-stripped so it survives session restarts with new auth tokens.
const keyUrl = _cacheKey(e.request.url);
const keyReq = new Request(keyUrl);
e.respondWith(
caches.open(CACHE).then(cache =>
cache.match(keyReq).then(cached => {
if (cached) return cached;
return fetch(e.request).then(res => {
if (res.status === 200) cache.put(keyReq, res.clone());
return res;
}).catch(() => Response.error());
})
)
);
return;
}
// All other requests: network-first, fall back to cache
e.respondWith(
fetch(e.request)
.then(res => {
if (res.status === 200 && e.request.method === 'GET') {
const clone = res.clone();
caches.open(CACHE).then(c => c.put(e.request, clone));
}
return res;
})
.catch(() => caches.match(e.request).then(r => r || Response.error()))
);
});