fix: owned albums wrongly shown as "Unvollständig 1/0" on artist page
Deezer's /artist/{id}/albums list omits nb_tracks, so the album total read as 0 and every
owned album (e.g. N!GHT's "Moonlight"/"Yume") was classified as incomplete "1 / 0 Tracks".
- Web (detail.js): resolve the real Deezer track count via the album endpoint for MATCHED
(owned) albums only — a couple of calls, not the whole discography — and treat an unknown
total as complete. Genuine partials still work when the true count is known.
- Android (categorizeAlbums): mark partial only when a known total exceeds what you own;
unknown total counts as complete (never a false "1 / 0").
- CHANGELOG: documented the fix; corrected the Android lyrics known bug (auto-scroll follows
playback but isn't smooth — it jumps rather than glides). SW ns-v55.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5528947a93
commit
640e475c51
4 files changed
+41
-20
No files matched your search
+4
-1
@@ -78,6 +78,9 @@ Big change: **the entire metadata/search stack moved from Apple's iTunes to Deez
|
||||
- **Android** — **Global 30s-preview control bar**: a preview started on an album/artist/Explore screen used to keep playing with no way to stop it once you navigated away (the local artist page had its *own* uncontrollable preview too). All previews now run through one app-level player with a bar (above the mini player, on every screen) to pause/resume/stop it; starting real playback cancels the preview
|
||||
- **Backend** — Unit tests for the Deezer mapping/matching logic (`backend/test/deezer.test.js`, run with `npm test`), incl. per-track numbering, mixed-artist album-artist stability, explicit flags and the Japanese-title match guard. A live smoke test runs with `RUN_LIVE=1`
|
||||
|
||||
### Fixed
|
||||
- **Web + Android** — Owned albums were wrongly shown as **"Unvollständig 1 / 0 Tracks"** on the artist page. Deezer's `/artist/{id}/albums` list omits `nb_tracks`, so the total read as 0 and every matched album looked incomplete. Web now resolves the real track count from the Deezer album endpoint for **matched (owned) albums only** and treats an unknown total as complete; Android treats an unknown total as complete (never a false partial)
|
||||
|
||||
### Notes
|
||||
- Deploy: `git pull`, then rebuild the frontend (`docker compose up -d --build novasonic-frontend`) and restart the backend (`docker compose restart novasonic-backend`). Service worker bumped to `ns-v54`
|
||||
- Titles a user picked stay as chosen; only derived fields (album, album-artist, genre, year, artwork) follow Deezer
|
||||
@@ -412,7 +415,7 @@ Primary causes identified:
|
||||
- **Web + Android** — yt-dlp backend update / maintenance
|
||||
|
||||
### Known Bugs
|
||||
- **Android (lyrics)** — Synced-lyrics **auto-scroll still does not follow playback**: the current line is highlighted but the list doesn't scroll to keep it centered, so you have to scroll by hand. Web autoscroll works. Not yet fixed.
|
||||
- **Android (lyrics)** — Synced-lyrics auto-scroll is **not smooth**: it follows playback and highlights the right line, but the list jumps to it rather than gliding. Web autoscroll is smooth. Not yet fixed.
|
||||
- **Web (PWA, desktop)** — The Windows media tile (SMTC) briefly closes and reopens when skipping tracks. Swapping the `<audio>` element's source leaves a momentary gap with no active media output, so Windows tears down and rebuilds the tile. A real fix needs one continuous source (MSE), which requires streaming an MSE-compatible format. The native Android app is unaffected. (The two-element gapless handoff added in 1.1 makes skips effectively instant but does **not** fix this — switching elements still ends and restarts the OS session.)
|
||||
- **Backend (download)** — The duplicate check requires at least a `firstArtist` folder match. If the same song was previously stored under a very different artist name (e.g. before an iTunes-name fix), it will not be detected as a duplicate and will be re-downloaded.
|
||||
- **Backend (metadata/sorting)** — MusicBrainz `release['artist-credit']` is often absent in the standard recording search response (requires `inc=artist-credits` which can slow the endpoint). The albumArtist field from MusicBrainz autofill may fall back to the recording artist rather than the release artist.
|
||||
|
||||
@@ -58,6 +58,9 @@ fun categorizeAlbums(ndAlbums: List<Album>, iAlbums: List<ItunesItem>): ArtistAl
|
||||
|
||||
for (ia in uniqueIAlbums) {
|
||||
val iName = normAlbum(ia.collectionName)
|
||||
// Deezer's /artist/{id}/albums omits nb_tracks, so trackCount is usually unknown (0) here.
|
||||
// Only mark "Unvollständig" when we KNOW the album has more tracks than we own — an unknown
|
||||
// total counts as complete, never a false "1 / 0".
|
||||
val iCount = ia.trackCount ?: 0
|
||||
val match = ndAlbums.find { a ->
|
||||
val n = normAlbum(a.name)
|
||||
@@ -65,11 +68,11 @@ fun categorizeAlbums(ndAlbums: List<Album>, iAlbums: List<ItunesItem>): ArtistAl
|
||||
}
|
||||
when {
|
||||
match == null -> notDl.add(ia)
|
||||
iCount > 0 && match.songCount >= iCount -> {
|
||||
downloaded.add(Pair(match, ia)); usedNdIds.add(match.id)
|
||||
iCount > match.songCount -> {
|
||||
partial.add(Triple(match, ia, Pair(match.songCount, iCount))); usedNdIds.add(match.id)
|
||||
}
|
||||
else -> {
|
||||
partial.add(Triple(match, ia, Pair(match.songCount, iCount))); usedNdIds.add(match.id)
|
||||
downloaded.add(Pair(match, ia)); usedNdIds.add(match.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'ns-v54';
|
||||
const CACHE = 'ns-v55';
|
||||
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.
|
||||
|
||||
+30
-15
@@ -282,36 +282,51 @@ export async function loadArtistDetail(artist) {
|
||||
actionBar.appendChild(evBtn);
|
||||
}
|
||||
|
||||
if(albEl) { albEl.innerHTML = ''; _renderArtistAlbums(albEl, ndAlbums, iAlbums, artist); }
|
||||
if(albEl) _renderArtistAlbums(albEl, ndAlbums, iAlbums, artist);
|
||||
}
|
||||
|
||||
function _normAlbum(s) {
|
||||
return (s || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
|
||||
const downloaded = [], partial = [], notDl = [];
|
||||
const usedNdIds = new Set();
|
||||
async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
|
||||
el.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
for(const ia of iAlbums) {
|
||||
const iName = _normAlbum(ia.collectionName);
|
||||
const iCount = ia.trackCount || 0;
|
||||
const match = ndAlbums.find(a => {
|
||||
// Match each Deezer album to a Navidrome album by normalized name.
|
||||
const entries = iAlbums.map(ia => {
|
||||
const iName = _normAlbum(ia.collectionName);
|
||||
const match = ndAlbums.find(a => {
|
||||
const n = _normAlbum(a.name);
|
||||
return n === iName || (n.length > 3 && iName.startsWith(n)) || (iName.length > 3 && n.startsWith(iName));
|
||||
});
|
||||
if(!match) {
|
||||
notDl.push(ia);
|
||||
} else if(iCount > 0 && match.songCount >= iCount) {
|
||||
downloaded.push({ nd: match, itunes: ia }); usedNdIds.add(match.id);
|
||||
} else {
|
||||
partial.push({ nd: match, itunes: ia, have: match.songCount || 0, total: iCount }); usedNdIds.add(match.id);
|
||||
}
|
||||
return { ia, match };
|
||||
});
|
||||
|
||||
// Deezer's /artist/{id}/albums list omits nb_tracks, so the total is unknown here — which made every
|
||||
// owned album look "incomplete" (e.g. "Moonlight 1 / 0"). Resolve the real track count from the
|
||||
// album endpoint, but ONLY for matched (owned) albums, so it's a couple of calls, not the whole
|
||||
// discography. An unknown total is then treated as complete, never a false "Unvollständig".
|
||||
await Promise.all(entries
|
||||
.filter(e => e.match && !(e.ia.trackCount > 0) && e.ia.collectionId)
|
||||
.map(async e => {
|
||||
const tracks = await itunesLookupAlbum(e.ia.collectionId).catch(() => []);
|
||||
if (tracks.length) e.ia = { ...e.ia, trackCount: tracks.length };
|
||||
}));
|
||||
|
||||
const downloaded = [], partial = [], notDl = [];
|
||||
const usedNdIds = new Set();
|
||||
for(const { ia, match } of entries) {
|
||||
if(!match) { notDl.push(ia); continue; }
|
||||
const have = match.songCount || 0;
|
||||
const total = ia.trackCount || 0;
|
||||
if(total > have) { partial.push({ nd: match, itunes: ia, have, total }); usedNdIds.add(match.id); }
|
||||
else { downloaded.push({ nd: match, itunes: ia }); usedNdIds.add(match.id); }
|
||||
}
|
||||
for(const na of ndAlbums) {
|
||||
if(!usedNdIds.has(na.id)) downloaded.push({ nd: na, itunes: null });
|
||||
}
|
||||
|
||||
el.innerHTML = '';
|
||||
downloaded.sort((a, b) => (b.nd.year||0) - (a.nd.year||0));
|
||||
partial.sort((a, b) => (b.nd.year||0) - (a.nd.year||0));
|
||||
notDl.sort((a, b) => new Date(b.releaseDate||0) - new Date(a.releaseDate||0));
|
||||
|
||||
Reference in new issue
Block a user