fix(web): 'Unknown Artist' download from artist page + rename itunes*→deezer* identifiers
Fix (the reported bug):
- Deezer's /artist/{id}/albums omits the artist object, so discography album cards had an
empty artist name → the download modal fell back to the literal "Unknown Artist", which
poisoned the yt-dlp search ("Unknown Artist <album>") and its artist/channel filter, failing
with "Nicht eindeutig gefunden".
- deezerLookupArtistAlbums now backfills the known artist name (we're on that artist's page).
- openDlModal now defaults album cards to an ALBUM download (was always Track), so a one-click
download no longer grabs an album as a single track named after the album.
Rename (requested cleanup, dependency-checked so nothing breaks):
- All frontend itunes* identifiers → deezer* (functions, the ItunesItem-shape field keys
itunesData/itunesArtistId/itunesFanCount/itunesId, the itunes-album-detail route + DOM ids,
the badge-itunes CSS class). The backend reads none of these fields, so the boundary is safe.
- Verified: build passes; route/DOM-id/CSS producer↔consumer all consistent; only the factual
"Apple's iTunes /search 403" comments keep the historical name. SW ns-v62.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
bc87a04da5
commit
1e206e8df3
13 files changed
+118
-108
No files matched your search
+2
-1
@@ -72,6 +72,7 @@ Big change: **the entire metadata/search stack moved from Apple's iTunes to Deez
|
|||||||
- The autofill endpoint moved `/api/itunes/lookup` → **`/api/meta/lookup`**
|
- The autofill endpoint moved `/api/itunes/lookup` → **`/api/meta/lookup`**
|
||||||
- **Web + Android** — All user-visible "iTunes" / "Apple Music" labels are now "Deezer" (search badge, charts badge, preview bar, editor autofill button, artist "populär" heading)
|
- **Web + Android** — All user-visible "iTunes" / "Apple Music" labels are now "Deezer" (search badge, charts badge, preview bar, editor autofill button, artist "populär" heading)
|
||||||
- **Backend** — Removed the dead iTunes `/search` proxy and the Apple charts RSS proxy; the Deezer proxy (`/api/deezer/*`) now also serves charts and the editor lookup
|
- **Backend** — Removed the dead iTunes `/search` proxy and the Apple charts RSS proxy; the Deezer proxy (`/api/deezer/*`) now also serves charts and the editor lookup
|
||||||
|
- **Web** — Renamed the leftover `itunes*` frontend identifiers to `deezer*` (`itunesSearch` → `deezerSearch`, `itunesLookup*` → `deezerLookup*`, `itunesData`/`itunesArtistId` → `deezerData`/`deezerArtistId`, the `itunes-album-detail` route/DOM ids, the `badge-itunes` CSS class, …). Purely internal — no behaviour change; only the factual "Apple's iTunes /search 403" comments keep the old name
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Web + Android** — **Same-name artist disambiguation**: Deezer's artist search isn't ranked by popularity, so a name collision (5 artists named "Joji") returned a niche one first — the artist page then loaded the wrong discography and owned songs didn't show as downloaded. Artists are now sorted by Deezer fan count, and on a local artist page (web) with a real collision the pick is tie-broken by **song overlap** with your library (each artist has different songs). The **fan count is shown** on search artist cards and on both the Deezer *and* local Navidrome artist pages (web + Android)
|
- **Web + Android** — **Same-name artist disambiguation**: Deezer's artist search isn't ranked by popularity, so a name collision (5 artists named "Joji") returned a niche one first — the artist page then loaded the wrong discography and owned songs didn't show as downloaded. Artists are now sorted by Deezer fan count, and on a local artist page (web) with a real collision the pick is tie-broken by **song overlap** with your library (each artist has different songs). The **fan count is shown** on search artist cards and on both the Deezer *and* local Navidrome artist pages (web + Android)
|
||||||
@@ -79,6 +80,7 @@ Big change: **the entire metadata/search stack moved from Apple's iTunes to Deez
|
|||||||
- **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`
|
- **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
|
### Fixed
|
||||||
|
- **Web** — Downloading a not-yet-owned album from an artist page searched YouTube for "**Unknown Artist** <album>" and failed. Deezer's `/artist/{id}/albums` omits the `artist` object, so the discography cards had an empty artist name → the modal fell back to the literal `"Unknown Artist"`. Fixed by backfilling the known artist name in `deezerLookupArtistAlbums` and defaulting the download modal to **Album** for album cards (was always **Track**)
|
||||||
- **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)
|
- **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
|
### Notes
|
||||||
@@ -415,7 +417,6 @@ Primary causes identified:
|
|||||||
- **Web + Android** — yt-dlp backend update / maintenance
|
- **Web + Android** — yt-dlp backend update / maintenance
|
||||||
|
|
||||||
### Known Bugs
|
### Known Bugs
|
||||||
- **Web (download)** — Downloading a not-yet-owned album **straight from an artist page** can fail with "Nicht eindeutig gefunden" and search YouTube for "**Unknown Artist** <album>". Three causes combine: (1) Deezer's `/artist/{id}/albums` omits the `artist` object, so the discography cards carry an **empty artist name** (`_dzAlbum` → `artistName: ''`); (2) `openDlModal` defaults to **type=track** even for album cards (the `isAlbum` flag is ignored), so a one-click download treats the album as a single track named after the album; (3) `downloads.js` substitutes the literal string `"Unknown Artist"` for the empty name, which then poisons the yt-dlp search query **and** the `artist ~= Unknown` / `channel ~= Unknown` filter. Workaround: switch the type to *Album* in the modal, or use the direct-link importer. 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.
|
- **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.)
|
- **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 (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.
|
||||||
|
|||||||
+11
-11
@@ -166,22 +166,22 @@
|
|||||||
<div id="artist-songs-list"></div>
|
<div id="artist-songs-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ITUNES ALBUM DETAIL -->
|
<!-- DEEZER ALBUM DETAIL -->
|
||||||
<div class="page" id="page-itunes-album-detail">
|
<div class="page" id="page-deezer-album-detail">
|
||||||
<button class="back-btn" id="itunes-album-back-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück</button>
|
<button class="back-btn" id="deezer-album-back-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück</button>
|
||||||
<div class="album-header">
|
<div class="album-header">
|
||||||
<div class="album-cover" id="itunes-album-cover-wrap"><div class="no-art">💿</div></div>
|
<div class="album-cover" id="deezer-album-cover-wrap"><div class="no-art">💿</div></div>
|
||||||
<div class="album-meta">
|
<div class="album-meta">
|
||||||
<div class="album-type">Deezer</div>
|
<div class="album-type">Deezer</div>
|
||||||
<div class="album-name" id="itunes-album-detail-name">—</div>
|
<div class="album-name" id="deezer-album-detail-name">—</div>
|
||||||
<div class="album-artist-name" id="itunes-album-detail-artist"></div>
|
<div class="album-artist-name" id="deezer-album-detail-artist"></div>
|
||||||
<div class="album-stats" id="itunes-album-detail-stats"></div>
|
<div class="album-stats" id="deezer-album-detail-stats"></div>
|
||||||
<div class="album-actions">
|
<div class="album-actions">
|
||||||
<button class="btn-outline" id="itunes-album-dl-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Album laden</button>
|
<button class="btn-outline" id="deezer-album-dl-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Album laden</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="itunes-album-songs"></div>
|
<div id="deezer-album-songs"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PARTIAL ALBUM DETAIL (incomplete album: downloaded + missing tracks) -->
|
<!-- PARTIAL ALBUM DETAIL (incomplete album: downloaded + missing tracks) -->
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
<div class="page" id="page-discover">
|
<div class="page" id="page-discover">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="page-title"><span class="blue">Global</span> Suche</div>
|
<div class="page-title"><span class="blue">Global</span> Suche</div>
|
||||||
<span class="badge badge-itunes" style="font-size:11px;padding:4px 10px">Deezer</span>
|
<span class="badge badge-deezer" style="font-size:11px;padding:4px 10px">Deezer</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-wrap" style="padding:0 0 20px">
|
<div class="search-wrap" style="padding:0 0 20px">
|
||||||
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
|
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
<div class="page" id="page-new-releases">
|
<div class="page" id="page-new-releases">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="page-title">Neue <em>Releases</em></div>
|
<div class="page-title">Neue <em>Releases</em></div>
|
||||||
<span class="badge badge-itunes" style="font-size:11px;padding:4px 10px">Deezer Charts</span>
|
<span class="badge badge-deezer" style="font-size:11px;padding:4px 10px">Deezer Charts</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-header" style="margin-top:0"><span class="section-title">Top Alben weltweit</span></div>
|
<div class="section-header" style="margin-top:0"><span class="section-title">Top Alben weltweit</span></div>
|
||||||
<div id="nr-albums" class="grid grid-4"></div>
|
<div id="nr-albums" class="grid grid-4"></div>
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'ns-v61';
|
const CACHE = 'ns-v62';
|
||||||
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
|
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
|
||||||
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
|
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
|
||||||
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
|
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
|
||||||
|
|||||||
+20
-14
@@ -271,7 +271,7 @@ export async function romajiConvert(texts) {
|
|||||||
// Apple's iTunes /search returns 403 from server IPs, so Deezer is the metadata backend for
|
// Apple's iTunes /search returns 403 from server IPs, so Deezer is the metadata backend for
|
||||||
// everything: search, album/artist lookups AND the "Neu & Beliebt" charts. Free, no key, returns
|
// everything: search, album/artist lookups AND the "Neu & Beliebt" charts. Free, no key, returns
|
||||||
// explicit versions + artist images + track durations. Proxied through the backend to avoid any
|
// explicit versions + artist images + track durations. Proxied through the backend to avoid any
|
||||||
// CORS surprises. All results are mapped to the legacy "ItunesItem" shape so the UI + download
|
// CORS surprises. All results are mapped to the legacy "DeezerItem" shape so the UI + download
|
||||||
// flow stay unchanged.
|
// flow stay unchanged.
|
||||||
function _deezerBase() {
|
function _deezerBase() {
|
||||||
return S.cfg.backendUrl ? `${S.cfg.backendUrl}/api/deezer` : 'https://api.deezer.com';
|
return S.cfg.backendUrl ? `${S.cfg.backendUrl}/api/deezer` : 'https://api.deezer.com';
|
||||||
@@ -306,23 +306,23 @@ function _dzArtist(a) {
|
|||||||
artistId: a.id, artistName: a.name,
|
artistId: a.id, artistName: a.name,
|
||||||
artworkUrl100: a.picture_big || a.picture_medium || '',
|
artworkUrl100: a.picture_big || a.picture_medium || '',
|
||||||
// Deezer's artist search does NOT rank by popularity — several artists can share a name
|
// Deezer's artist search does NOT rank by popularity — several artists can share a name
|
||||||
// (5× "Joji"). fanCount lets us pick the one the user means (see itunesSearch + detail.js).
|
// (5× "Joji"). fanCount lets us pick the one the user means (see deezerSearch + detail.js).
|
||||||
fanCount: a.nb_fan ?? undefined,
|
fanCount: a.nb_fan ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last search outcome: null = ok, else HTTP status or 'network'. Read by the search view to tell
|
// Last search outcome: null = ok, else HTTP status or 'network'. Read by the search view to tell
|
||||||
// "no results" apart from "search backend down".
|
// "no results" apart from "search backend down".
|
||||||
let _itunesLastError = null;
|
let _deezerLastError = null;
|
||||||
export function itunesLastError() { return _itunesLastError; }
|
export function deezerLastError() { return _deezerLastError; }
|
||||||
|
|
||||||
// Keeps the old name so all callers stay unchanged. entity: musicTrack | album | musicArtist.
|
// Keeps the old name so all callers stay unchanged. entity: musicTrack | album | musicArtist.
|
||||||
export async function itunesSearch(query, entity='musicTrack', limit=25) {
|
export async function deezerSearch(query, entity='musicTrack', limit=25) {
|
||||||
try {
|
try {
|
||||||
const path = entity === 'album' ? 'search/album' : entity === 'musicArtist' ? 'search/artist' : 'search';
|
const path = entity === 'album' ? 'search/album' : entity === 'musicArtist' ? 'search/artist' : 'search';
|
||||||
const r = await fetch(`${_deezerBase()}/${path}?q=${encodeURIComponent(query)}&limit=${limit}`);
|
const r = await fetch(`${_deezerBase()}/${path}?q=${encodeURIComponent(query)}&limit=${limit}`);
|
||||||
if (!r.ok) { _itunesLastError = r.status; return []; }
|
if (!r.ok) { _deezerLastError = r.status; return []; }
|
||||||
_itunesLastError = null;
|
_deezerLastError = null;
|
||||||
const txt = await r.text();
|
const txt = await r.text();
|
||||||
const data = (txt ? JSON.parse(txt) : {}).data || [];
|
const data = (txt ? JSON.parse(txt) : {}).data || [];
|
||||||
if (entity === 'album') return data.map(_dzAlbum);
|
if (entity === 'album') return data.map(_dzAlbum);
|
||||||
@@ -330,12 +330,12 @@ export async function itunesSearch(query, entity='musicTrack', limit=25) {
|
|||||||
// (Deezer returns them in an unhelpful order — the real "Joji" isn't first).
|
// (Deezer returns them in an unhelpful order — the real "Joji" isn't first).
|
||||||
if (entity === 'musicArtist') return data.map(_dzArtist).sort((x, y) => (y.fanCount || 0) - (x.fanCount || 0));
|
if (entity === 'musicArtist') return data.map(_dzArtist).sort((x, y) => (y.fanCount || 0) - (x.fanCount || 0));
|
||||||
return data.map(_dzTrack);
|
return data.map(_dzTrack);
|
||||||
} catch { _itunesLastError = 'network'; return []; }
|
} catch { _deezerLastError = 'network'; return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Album tracks (for the album detail view + album download). Deezer's /album/{id} embeds tracks +
|
// Album tracks (for the album detail view + album download). Deezer's /album/{id} embeds tracks +
|
||||||
// album-level release/genre/cover, so one request gives everything.
|
// album-level release/genre/cover, so one request gives everything.
|
||||||
export async function itunesLookupAlbum(collectionId) {
|
export async function deezerLookupAlbum(collectionId) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${_deezerBase()}/album/${collectionId}`);
|
const r = await fetch(`${_deezerBase()}/album/${collectionId}`);
|
||||||
const a = await r.json();
|
const a = await r.json();
|
||||||
@@ -351,17 +351,23 @@ export async function itunesLookupAlbum(collectionId) {
|
|||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function itunesLookupArtistSongs(artistId, limit=15) {
|
export async function deezerLookupArtistSongs(artistId, limit=15) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${_deezerBase()}/artist/${artistId}/top?limit=${limit}`);
|
const r = await fetch(`${_deezerBase()}/artist/${artistId}/top?limit=${limit}`);
|
||||||
return ((await r.json()).data || []).map(_dzTrack);
|
return ((await r.json()).data || []).map(_dzTrack);
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function itunesLookupArtistAlbums(artistId, limit=100) {
|
// Deezer's /artist/{id}/albums list OMITS the artist object, so _dzAlbum yields artistName:''.
|
||||||
|
// We already know whose page this is → backfill artistName so downstream download/detail cards
|
||||||
|
// don't fall back to the literal "Unknown Artist" (which poisoned the yt-dlp search).
|
||||||
|
export async function deezerLookupArtistAlbums(artistId, limit=100, artistName='') {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${_deezerBase()}/artist/${artistId}/albums?limit=${limit}`);
|
const r = await fetch(`${_deezerBase()}/artist/${artistId}/albums?limit=${limit}`);
|
||||||
return ((await r.json()).data || []).map(_dzAlbum).filter(a => a.collectionType === 'Album');
|
return ((await r.json()).data || [])
|
||||||
|
.map(_dzAlbum)
|
||||||
|
.filter(a => a.collectionType === 'Album')
|
||||||
|
.map(a => (a.artistName ? a : { ...a, artistName }));
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +381,7 @@ export async function deezerArtistImage(artistId) {
|
|||||||
|
|
||||||
// "Neu & Beliebt" — Deezer's global chart (Apple's charts RSS needed a proxy and the search
|
// "Neu & Beliebt" — Deezer's global chart (Apple's charts RSS needed a proxy and the search
|
||||||
// fallback is 403 from server IPs). Mapped to { name, artistName, artworkUrl, artworkUrl100 }.
|
// fallback is 403 from server IPs). Mapped to { name, artistName, artworkUrl, artworkUrl100 }.
|
||||||
export async function itunesTopCharts() {
|
export async function deezerTopCharts() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${_deezerBase()}/chart/0/albums?limit=50`);
|
const r = await fetch(`${_deezerBase()}/chart/0/albums?limit=50`);
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
@@ -386,7 +392,7 @@ export async function itunesTopCharts() {
|
|||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function itunesTopSongs() {
|
export async function deezerTopSongs() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${_deezerBase()}/chart/0/tracks?limit=50`);
|
const r = await fetch(`${_deezerBase()}/chart/0/tracks?limit=50`);
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
navigate, _navGoBack, _navOnPopState, _navLoad, onHeaderSearch, closeHeaderSearch,
|
navigate, _navGoBack, _navOnPopState, _navLoad, onHeaderSearch, closeHeaderSearch,
|
||||||
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
|
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
|
||||||
searchSimilarArtist, searchAndPreview, loadItunesAlbum, closeEventsModal,
|
searchSimilarArtist, searchAndPreview, loadDeezerAlbum, closeEventsModal,
|
||||||
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
|
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
|
||||||
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
|
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
|
||||||
} from './views/views.js';
|
} from './views/views.js';
|
||||||
@@ -314,7 +314,7 @@ Object.assign(window, {
|
|||||||
// Navigation
|
// Navigation
|
||||||
navigate, onHeaderSearch, closeHeaderSearch, onGlobalSearch, s_goArtist, s_goAlbum,
|
navigate, onHeaderSearch, closeHeaderSearch, onGlobalSearch, s_goArtist, s_goAlbum,
|
||||||
playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview,
|
playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview,
|
||||||
loadItunesAlbum, closeEventsModal,
|
loadDeezerAlbum, closeEventsModal,
|
||||||
// Player
|
// Player
|
||||||
playQueue, togglePlay, prevSong, nextSong, toggleShuffle, toggleRepeat,
|
playQueue, togglePlay, prevSong, nextSong, toggleShuffle, toggleRepeat,
|
||||||
toggleQueue, setVolume, seekTo, starCurrent, downloadCurrent, playPreview,
|
toggleQueue, setVolume, seekTo, starCurrent, downloadCurrent, playPreview,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function makeSongItem(s, idx, list, { noArtistLink = false, singlePlay =
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── GLOBAL TRACK ITEM (iTunes / Spotify results) ───
|
// ─── GLOBAL TRACK ITEM (Deezer / Spotify results) ───
|
||||||
export function makeGlobalTrackItem(t, idx, list) {
|
export function makeGlobalTrackItem(t, idx, list) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'gtrack-item';
|
el.className = 'gtrack-item';
|
||||||
|
|||||||
+8
-5
@@ -1,13 +1,16 @@
|
|||||||
import { S } from '../state.js';
|
import { S } from '../state.js';
|
||||||
import { esc, toast } from '../utils.js';
|
import { esc, toast } from '../utils.js';
|
||||||
import { api, fetchRomajiIndex, romajiConvert, getRomajiOverrides, invalidateRomajiIndex, itunesLookupAlbum } from '../api.js';
|
import { api, fetchRomajiIndex, romajiConvert, getRomajiOverrides, invalidateRomajiIndex, deezerLookupAlbum } from '../api.js';
|
||||||
import { _addNotif } from './notify.js';
|
import { _addNotif } from './notify.js';
|
||||||
|
|
||||||
// ─── DOWNLOAD MODAL ───
|
// ─── DOWNLOAD MODAL ───
|
||||||
export function openDlModal(track) {
|
export function openDlModal(track) {
|
||||||
if(!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert. Bitte in Einstellungen eintragen.'); return; }
|
if(!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert. Bitte in Einstellungen eintragen.'); return; }
|
||||||
S.downloadTarget = track; S.dlType = 'track';
|
// Album cards pass isAlbum → default the modal to an album download, not a single track named
|
||||||
document.querySelectorAll('.modal-opt').forEach(o => o.classList.toggle('selected', o.dataset.type==='track'));
|
// after the album (a one-click download of an album card used to grab it as one "track").
|
||||||
|
const _defType = track.isAlbum ? 'album' : 'track';
|
||||||
|
S.downloadTarget = track; S.dlType = _defType;
|
||||||
|
document.querySelectorAll('.modal-opt').forEach(o => o.classList.toggle('selected', o.dataset.type===_defType));
|
||||||
const info = document.getElementById('dl-modal-info');
|
const info = document.getElementById('dl-modal-info');
|
||||||
const art = track.artworkUrl100 || track.artworkUrl60 || '';
|
const art = track.artworkUrl100 || track.artworkUrl60 || '';
|
||||||
info.innerHTML = `${art ? `<div class="modal-art"><img src="${art}"></div>` : '<div class="modal-art" style="display:flex;align-items:center;justify-content:center;font-size:28px">🎵</div>'}
|
info.innerHTML = `${art ? `<div class="modal-art"><img src="${art}"></div>` : '<div class="modal-art" style="display:flex;align-items:center;justify-content:center;font-size:28px">🎵</div>'}
|
||||||
@@ -35,7 +38,7 @@ export async function executeDownload() {
|
|||||||
title: _title,
|
title: _title,
|
||||||
album: _album,
|
album: _album,
|
||||||
artworkUrl: S.downloadTarget.artworkUrl100 || S.downloadTarget.artworkUrl60 || '',
|
artworkUrl: S.downloadTarget.artworkUrl100 || S.downloadTarget.artworkUrl60 || '',
|
||||||
itunesId: S.downloadTarget.trackId || S.downloadTarget.collectionId || null,
|
deezerId: S.downloadTarget.trackId || S.downloadTarget.collectionId || null,
|
||||||
collectionId: S.downloadTarget.collectionId || null,
|
collectionId: S.downloadTarget.collectionId || null,
|
||||||
explicit: S.downloadTarget.trackExplicitness === 'explicit',
|
explicit: S.downloadTarget.trackExplicitness === 'explicit',
|
||||||
trackNum: S.downloadTarget.trackNumber || undefined,
|
trackNum: S.downloadTarget.trackNumber || undefined,
|
||||||
@@ -51,7 +54,7 @@ export async function executeDownload() {
|
|||||||
// request — the backend downloads that list directly (Apple's /search is 403, so no iTunes lookup).
|
// request — the backend downloads that list directly (Apple's /search is 403, so no iTunes lookup).
|
||||||
if (S.dlType === 'album' && payload.collectionId) {
|
if (S.dlType === 'album' && payload.collectionId) {
|
||||||
try {
|
try {
|
||||||
const albumTracks = await itunesLookupAlbum(payload.collectionId);
|
const albumTracks = await deezerLookupAlbum(payload.collectionId);
|
||||||
if (albumTracks.length) payload.tracks = albumTracks;
|
if (albumTracks.length) payload.tracks = albumTracks;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-40
@@ -1,6 +1,6 @@
|
|||||||
import { S } from '../state.js';
|
import { S } from '../state.js';
|
||||||
import { esc, toast, fmtCount } from '../utils.js';
|
import { esc, toast, fmtCount } from '../utils.js';
|
||||||
import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, deezerArtistImage, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap } from '../api.js';
|
import { api, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap } from '../api.js';
|
||||||
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
|
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
|
||||||
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
|
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
|
||||||
import { showCtxMenu, openDlModal } from '../ui/ui.js';
|
import { showCtxMenu, openDlModal } from '../ui/ui.js';
|
||||||
@@ -87,8 +87,8 @@ export async function loadArtistDetail(artist) {
|
|||||||
if(albEl) albEl.innerHTML = '<div class="spinner"></div>';
|
if(albEl) albEl.innerHTML = '<div class="spinner"></div>';
|
||||||
|
|
||||||
const hasND = !!artist.id;
|
const hasND = !!artist.id;
|
||||||
const knownItunes = artist.itunesArtistId || null;
|
const knownDeezer = artist.deezerArtistId || null;
|
||||||
const knownFans = artist.itunesFanCount ?? null;
|
const knownFans = artist.deezerFanCount ?? null;
|
||||||
if (knownFans) _showFans(knownFans); // from the global-search card — show immediately
|
if (knownFans) _showFans(knownFans); // from the global-search card — show immediately
|
||||||
|
|
||||||
// Fire all requests immediately so they run in parallel.
|
// Fire all requests immediately so they run in parallel.
|
||||||
@@ -96,7 +96,7 @@ export async function loadArtistDetail(artist) {
|
|||||||
const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null);
|
const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null);
|
||||||
const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null);
|
const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null);
|
||||||
const topP = hasND ? api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null);
|
const topP = hasND ? api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null);
|
||||||
const iArtistP = knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5);
|
const iArtistP = knownDeezer ? Promise.resolve([{ artistId: knownDeezer }]) : deezerSearch(artist.name, 'musicArtist', 5);
|
||||||
const eventsP = fetchEvents(artist.name);
|
const eventsP = fetchEvents(artist.name);
|
||||||
|
|
||||||
// Phase 1 — hero: only wait for albumP (fast local Navidrome call, no external API).
|
// Phase 1 — hero: only wait for albumP (fast local Navidrome call, no external API).
|
||||||
@@ -138,7 +138,7 @@ export async function loadArtistDetail(artist) {
|
|||||||
document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none');
|
document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none');
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
// Phase 2 — content (may be slow: Last.fm bio, iTunes, events)
|
// Phase 2 — content (may be slow: Last.fm bio, Deezer, events)
|
||||||
const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]);
|
const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]);
|
||||||
|
|
||||||
const _normWords = s => new Set((s || '').toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(Boolean));
|
const _normWords = s => new Set((s || '').toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(Boolean));
|
||||||
@@ -163,7 +163,7 @@ export async function loadArtistDetail(artist) {
|
|||||||
// artist we own songs for, prefer the Deezer candidate whose top tracks actually overlap our
|
// artist we own songs for, prefer the Deezer candidate whose top tracks actually overlap our
|
||||||
// library — each artist has different songs, so this nails the right person. Only runs on a real
|
// library — each artist has different songs, so this nails the right person. Only runs on a real
|
||||||
// collision (≥2 name matches) and costs a few calls just for those.
|
// collision (≥2 name matches) and costs a few calls just for those.
|
||||||
if (!knownItunes && hasND && nameMatches.length > 1) {
|
if (!knownDeezer && hasND && nameMatches.length > 1) {
|
||||||
const _nt = s => (s || '').toLowerCase().replace(/\([^)]*\)/g, '').replace(/[^a-z0-9]/g, '');
|
const _nt = s => (s || '').toLowerCase().replace(/\([^)]*\)/g, '').replace(/[^a-z0-9]/g, '');
|
||||||
const refTitles = new Set(
|
const refTitles = new Set(
|
||||||
((topRes.status === 'fulfilled' ? topRes.value?.topSongs?.song : []) || [])
|
((topRes.status === 'fulfilled' ? topRes.value?.topSongs?.song : []) || [])
|
||||||
@@ -172,7 +172,7 @@ export async function loadArtistDetail(artist) {
|
|||||||
if (refTitles.size) {
|
if (refTitles.size) {
|
||||||
const cands = nameMatches.slice(0, 3);
|
const cands = nameMatches.slice(0, 3);
|
||||||
const scored = await Promise.all(cands.map(async c => {
|
const scored = await Promise.all(cands.map(async c => {
|
||||||
const tt = await itunesLookupArtistSongs(c.artistId, 15).catch(() => []);
|
const tt = await deezerLookupArtistSongs(c.artistId, 15).catch(() => []);
|
||||||
const overlap = tt.reduce((n, t) => n + (refTitles.has(_nt(t.trackName)) ? 1 : 0), 0);
|
const overlap = tt.reduce((n, t) => n + (refTitles.has(_nt(t.trackName)) ? 1 : 0), 0);
|
||||||
return { c, overlap };
|
return { c, overlap };
|
||||||
}));
|
}));
|
||||||
@@ -181,12 +181,12 @@ export async function loadArtistDetail(artist) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null;
|
const deezerArtistId = knownDeezer ?? iArtistMatch?.artistId ?? null;
|
||||||
if (!knownFans) _showFans(iArtistMatch?.fanCount); // local artist: show the matched Deezer artist's fans
|
if (!knownFans) _showFans(iArtistMatch?.fanCount); // local artist: show the matched Deezer artist's fans
|
||||||
|
|
||||||
// Non-local (Deezer) artist has no Navidrome photo — fetch the Deezer artist picture for the hero.
|
// Non-local (Deezer) artist has no Navidrome photo — fetch the Deezer artist picture for the hero.
|
||||||
if (!hasND && itunesArtistId && pfpWrap) {
|
if (!hasND && deezerArtistId && pfpWrap) {
|
||||||
const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(itunesArtistId);
|
const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(deezerArtistId);
|
||||||
if (dzImg) {
|
if (dzImg) {
|
||||||
const pfpImg = document.createElement('img');
|
const pfpImg = document.createElement('img');
|
||||||
pfpImg.className = 'artist-hero-pfp';
|
pfpImg.className = 'artist-hero-pfp';
|
||||||
@@ -197,10 +197,10 @@ export async function loadArtistDetail(artist) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [iAlbumsRes, iTracksRes] = itunesArtistId
|
const [iAlbumsRes, iTracksRes] = deezerArtistId
|
||||||
? await Promise.allSettled([
|
? await Promise.allSettled([
|
||||||
itunesLookupArtistAlbums(itunesArtistId),
|
deezerLookupArtistAlbums(deezerArtistId, 100, artist.name),
|
||||||
itunesLookupArtistSongs(itunesArtistId, 15),
|
deezerLookupArtistSongs(deezerArtistId, 15),
|
||||||
])
|
])
|
||||||
: [{ status: 'rejected' }, { status: 'rejected' }];
|
: [{ status: 'rejected' }, { status: 'rejected' }];
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
|
|||||||
await Promise.all(entries
|
await Promise.all(entries
|
||||||
.filter(e => e.match && !(e.ia.trackCount > 0) && e.ia.collectionId)
|
.filter(e => e.match && !(e.ia.trackCount > 0) && e.ia.collectionId)
|
||||||
.map(async e => {
|
.map(async e => {
|
||||||
const tracks = await itunesLookupAlbum(e.ia.collectionId).catch(() => []);
|
const tracks = await deezerLookupAlbum(e.ia.collectionId).catch(() => []);
|
||||||
if (tracks.length) e.ia = { ...e.ia, trackCount: tracks.length };
|
if (tracks.length) e.ia = { ...e.ia, trackCount: tracks.length };
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -342,11 +342,11 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
|
|||||||
if(!match) { notDl.push(ia); continue; }
|
if(!match) { notDl.push(ia); continue; }
|
||||||
const have = match.songCount || 0;
|
const have = match.songCount || 0;
|
||||||
const total = ia.trackCount || 0;
|
const total = ia.trackCount || 0;
|
||||||
if(total > have) { partial.push({ nd: match, itunes: ia, have, total }); usedNdIds.add(match.id); }
|
if(total > have) { partial.push({ nd: match, deezer: ia, have, total }); usedNdIds.add(match.id); }
|
||||||
else { downloaded.push({ nd: match, itunes: ia }); usedNdIds.add(match.id); }
|
else { downloaded.push({ nd: match, deezer: ia }); usedNdIds.add(match.id); }
|
||||||
}
|
}
|
||||||
for(const na of ndAlbums) {
|
for(const na of ndAlbums) {
|
||||||
if(!usedNdIds.has(na.id)) downloaded.push({ nd: na, itunes: null });
|
if(!usedNdIds.has(na.id)) downloaded.push({ nd: na, deezer: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
el.innerHTML = '';
|
el.innerHTML = '';
|
||||||
@@ -370,20 +370,20 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
|
|||||||
}
|
}
|
||||||
if(partial.length) {
|
if(partial.length) {
|
||||||
const g = addGrid('Unvollständig', partial.length);
|
const g = addGrid('Unvollständig', partial.length);
|
||||||
partial.forEach(({ nd, itunes, have, total }) => {
|
partial.forEach(({ nd, deezer, have, total }) => {
|
||||||
const art = itunes?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512);
|
const art = deezer?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512);
|
||||||
const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, '💿');
|
const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, '💿');
|
||||||
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); };
|
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); };
|
||||||
c.querySelector('.card-info').onclick = () => itunes ? navigate('partial-album-detail', { nd, itunes }) : navigate('album-detail', { ...nd, _fromArtist: artist });
|
c.querySelector('.card-info').onclick = () => deezer ? navigate('partial-album-detail', { nd, deezer }) : navigate('album-detail', { ...nd, _fromArtist: artist });
|
||||||
const artEl = c.querySelector('.card-art');
|
const artEl = c.querySelector('.card-art');
|
||||||
const badge = document.createElement('div');
|
const badge = document.createElement('div');
|
||||||
badge.className = 'album-partial-badge'; badge.textContent = `${have}/${total}`;
|
badge.className = 'album-partial-badge'; badge.textContent = `${have}/${total}`;
|
||||||
artEl.appendChild(badge);
|
artEl.appendChild(badge);
|
||||||
if(itunes) {
|
if(deezer) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'card-dl-btn'; btn.title = 'Fehlende Tracks laden';
|
btn.className = 'card-dl-btn'; btn.title = 'Fehlende Tracks laden';
|
||||||
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||||
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: artist.name, trackName: nd.name, collectionName: nd.name, artworkUrl100: itunes.artworkUrl100||'', isAlbum: true, itunesData: itunes, collectionId: itunes.collectionId }); };
|
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: artist.name, trackName: nd.name, collectionName: nd.name, artworkUrl100: deezer.artworkUrl100||'', isAlbum: true, deezerData: deezer, collectionId: deezer.collectionId }); };
|
||||||
artEl.appendChild(btn);
|
artEl.appendChild(btn);
|
||||||
}
|
}
|
||||||
g.appendChild(c);
|
g.appendChild(c);
|
||||||
@@ -394,12 +394,12 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
|
|||||||
notDl.forEach(ia => {
|
notDl.forEach(ia => {
|
||||||
const art = ia.artworkUrl100?.replace('100x100','300x300');
|
const art = ia.artworkUrl100?.replace('100x100','300x300');
|
||||||
const c = makeCard(ia.collectionName, ia.releaseDate?.slice(0,4)||'', art, '💿');
|
const c = makeCard(ia.collectionName, ia.releaseDate?.slice(0,4)||'', art, '💿');
|
||||||
c.onclick = () => navigate('itunes-album-detail', ia);
|
c.onclick = () => navigate('deezer-album-detail', ia);
|
||||||
const artEl = c.querySelector('.card-art');
|
const artEl = c.querySelector('.card-art');
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'card-dl-btn'; btn.title = 'Album laden';
|
btn.className = 'card-dl-btn'; btn.title = 'Album laden';
|
||||||
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||||
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: ia.artistName, trackName: ia.collectionName, collectionName: ia.collectionName, artworkUrl100: ia.artworkUrl100||'', isAlbum: true, itunesData: ia, collectionId: ia.collectionId }); };
|
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: ia.artistName, trackName: ia.collectionName, collectionName: ia.collectionName, artworkUrl100: ia.artworkUrl100||'', isAlbum: true, deezerData: ia, collectionId: ia.collectionId }); };
|
||||||
artEl.appendChild(btn);
|
artEl.appendChild(btn);
|
||||||
g.appendChild(c);
|
g.appendChild(c);
|
||||||
});
|
});
|
||||||
@@ -438,32 +438,32 @@ export async function loadArtistAllSongs({ albums, artist }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadItunesAlbumDetail(album) {
|
export async function loadDeezerAlbumDetail(album) {
|
||||||
document.getElementById('itunes-album-back-btn').onclick = () => _navGoBack();
|
document.getElementById('deezer-album-back-btn').onclick = () => _navGoBack();
|
||||||
document.getElementById('itunes-album-detail-name').textContent = album.collectionName || '—';
|
document.getElementById('deezer-album-detail-name').textContent = album.collectionName || '—';
|
||||||
document.getElementById('itunes-album-detail-artist').textContent = album.artistName || '';
|
document.getElementById('deezer-album-detail-artist').textContent = album.artistName || '';
|
||||||
const cw = document.getElementById('itunes-album-cover-wrap');
|
const cw = document.getElementById('deezer-album-cover-wrap');
|
||||||
const art = (album.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600');
|
const art = (album.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600');
|
||||||
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
|
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
|
||||||
const dlBtn = document.getElementById('itunes-album-dl-btn');
|
const dlBtn = document.getElementById('deezer-album-dl-btn');
|
||||||
dlBtn.onclick = () => openDlModal({ artistName: album.artistName, trackName: album.collectionName, collectionName: album.collectionName, artworkUrl100: album.artworkUrl100||'', isAlbum: true, itunesData: album, collectionId: album.collectionId });
|
dlBtn.onclick = () => openDlModal({ artistName: album.artistName, trackName: album.collectionName, collectionName: album.collectionName, artworkUrl100: album.artworkUrl100||'', isAlbum: true, deezerData: album, collectionId: album.collectionId });
|
||||||
const sl = document.getElementById('itunes-album-songs');
|
const sl = document.getElementById('deezer-album-songs');
|
||||||
sl.innerHTML = '<div class="spinner"></div>';
|
sl.innerHTML = '<div class="spinner"></div>';
|
||||||
try {
|
try {
|
||||||
const tracks = await itunesLookupAlbum(album.collectionId);
|
const tracks = await deezerLookupAlbum(album.collectionId);
|
||||||
const year = album.releaseDate ? new Date(album.releaseDate).getFullYear() : '';
|
const year = album.releaseDate ? new Date(album.releaseDate).getFullYear() : '';
|
||||||
document.getElementById('itunes-album-detail-stats').textContent = `${tracks.length} Titel${year ? ' · ' + year : ''}`;
|
document.getElementById('deezer-album-detail-stats').textContent = `${tracks.length} Titel${year ? ' · ' + year : ''}`;
|
||||||
sl.innerHTML = '';
|
sl.innerHTML = '';
|
||||||
if(!tracks.length) { sl.innerHTML = '<div style="padding:16px;color:var(--text3)">Keine Tracks gefunden</div>'; return; }
|
if(!tracks.length) { sl.innerHTML = '<div style="padding:16px;color:var(--text3)">Keine Tracks gefunden</div>'; return; }
|
||||||
tracks.forEach((t, i) => sl.appendChild(makeGlobalTrackItem(t, i, tracks)));
|
tracks.forEach((t, i) => sl.appendChild(makeGlobalTrackItem(t, i, tracks)));
|
||||||
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPartialAlbumDetail({ nd, itunes }) {
|
export async function loadPartialAlbumDetail({ nd, deezer }) {
|
||||||
document.getElementById('partial-album-back-btn').onclick = () => _navGoBack();
|
document.getElementById('partial-album-back-btn').onclick = () => _navGoBack();
|
||||||
document.getElementById('partial-album-detail-name').textContent = nd.name || '—';
|
document.getElementById('partial-album-detail-name').textContent = nd.name || '—';
|
||||||
document.getElementById('partial-album-detail-artist').textContent = nd.artist || itunes?.artistName || '';
|
document.getElementById('partial-album-detail-artist').textContent = nd.artist || deezer?.artistName || '';
|
||||||
const art = (itunes?.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600')
|
const art = (deezer?.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600')
|
||||||
|| (nd.coverArt ? coverUrl(nd.coverArt, 512) : '');
|
|| (nd.coverArt ? coverUrl(nd.coverArt, 512) : '');
|
||||||
const cw = document.getElementById('partial-album-cover-wrap');
|
const cw = document.getElementById('partial-album-cover-wrap');
|
||||||
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
|
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
|
||||||
@@ -476,7 +476,7 @@ export async function loadPartialAlbumDetail({ nd, itunes }) {
|
|||||||
try {
|
try {
|
||||||
const [ndRes, iTracks] = await Promise.all([
|
const [ndRes, iTracks] = await Promise.all([
|
||||||
api('getAlbum', `&id=${nd.id}`),
|
api('getAlbum', `&id=${nd.id}`),
|
||||||
itunes?.collectionId ? itunesLookupAlbum(itunes.collectionId) : Promise.resolve([]),
|
deezer?.collectionId ? deezerLookupAlbum(deezer.collectionId) : Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
const ndSongs = (ndRes.album?.song || []).sort((a,b) => (a.track||999)-(b.track||999));
|
const ndSongs = (ndRes.album?.song || []).sort((a,b) => (a.track||999)-(b.track||999));
|
||||||
|
|
||||||
@@ -492,9 +492,9 @@ export async function loadPartialAlbumDetail({ nd, itunes }) {
|
|||||||
if(ndSongs.length) playQueue(ndSongs, 0);
|
if(ndSongs.length) playQueue(ndSongs, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Download button offers missing via iTunes
|
// Download button offers missing via Deezer
|
||||||
document.getElementById('partial-album-dl-btn').onclick = () => {
|
document.getElementById('partial-album-dl-btn').onclick = () => {
|
||||||
if(itunes) openDlModal({ artistName: itunes.artistName, trackName: itunes.collectionName, collectionName: itunes.collectionName, artworkUrl100: itunes.artworkUrl100||'', isAlbum: true, itunesData: itunes, collectionId: itunes.collectionId });
|
if(deezer) openDlModal({ artistName: deezer.artistName, trackName: deezer.collectionName, collectionName: deezer.collectionName, artworkUrl100: deezer.artworkUrl100||'', isAlbum: true, deezerData: deezer, collectionId: deezer.collectionId });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Section: downloaded
|
// Section: downloaded
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { S } from '../state.js';
|
import { S } from '../state.js';
|
||||||
import { esc } from '../utils.js';
|
import { esc } from '../utils.js';
|
||||||
import { itunesSearch, itunesTopCharts, itunesTopSongs } from '../api.js';
|
import { deezerSearch, deezerTopCharts, deezerTopSongs } from '../api.js';
|
||||||
import { makeCard, makeGlobalTrackItem } from '../ui/components.js';
|
import { makeCard, makeGlobalTrackItem } from '../ui/components.js';
|
||||||
import { playPreview } from '../player/player.js';
|
import { playPreview } from '../player/player.js';
|
||||||
import { openDlModal } from '../ui/ui.js';
|
import { openDlModal } from '../ui/ui.js';
|
||||||
// Circular — safe (called inside event handlers)
|
// Circular — safe (called inside event handlers)
|
||||||
import { loadItunesAlbum, searchAndPreview } from './search.js';
|
import { loadDeezerAlbum, searchAndPreview } from './search.js';
|
||||||
|
|
||||||
// ─── NEW RELEASES ───
|
// ─── NEW RELEASES ───
|
||||||
export async function loadNewReleases() {
|
export async function loadNewReleases() {
|
||||||
@@ -13,12 +13,12 @@ export async function loadNewReleases() {
|
|||||||
const songEl = document.getElementById('nr-songs');
|
const songEl = document.getElementById('nr-songs');
|
||||||
albumEl.innerHTML = '<div class="spinner"></div>'; songEl.innerHTML = '';
|
albumEl.innerHTML = '<div class="spinner"></div>'; songEl.innerHTML = '';
|
||||||
try {
|
try {
|
||||||
const [albums, songs] = await Promise.all([itunesTopCharts(), itunesTopSongs()]);
|
const [albums, songs] = await Promise.all([deezerTopCharts(), deezerTopSongs()]);
|
||||||
albumEl.innerHTML = '';
|
albumEl.innerHTML = '';
|
||||||
albums.slice(0,30).forEach(a => {
|
albums.slice(0,30).forEach(a => {
|
||||||
const art = a.artworkUrl100?.replace('100x100','300x300') || a.artworkUrl;
|
const art = a.artworkUrl100?.replace('100x100','300x300') || a.artworkUrl;
|
||||||
const c = makeCard(a.name, a.artistName, art, '💿');
|
const c = makeCard(a.name, a.artistName, art, '💿');
|
||||||
c.onclick = () => itunesSearch(a.name+' '+a.artistName,'album',5).then(res => { if(res[0]) loadItunesAlbum(res[0]); });
|
c.onclick = () => deezerSearch(a.name+' '+a.artistName,'album',5).then(res => { if(res[0]) loadDeezerAlbum(res[0]); });
|
||||||
albumEl.appendChild(c);
|
albumEl.appendChild(c);
|
||||||
});
|
});
|
||||||
songs.slice(0,40).forEach((s, i) => {
|
songs.slice(0,40).forEach((s, i) => {
|
||||||
@@ -36,7 +36,7 @@ export async function loadNewReleases() {
|
|||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
el.onclick = () => { itunesSearch(s.name+' '+s.artistName,'musicTrack',1).then(res => { if(res[0]?.previewUrl) playPreview(res[0]); }); };
|
el.onclick = () => { deezerSearch(s.name+' '+s.artistName,'musicTrack',1).then(res => { if(res[0]?.previewUrl) playPreview(res[0]); }); };
|
||||||
songEl.appendChild(el);
|
songEl.appendChild(el);
|
||||||
});
|
});
|
||||||
} catch(e) { albumEl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
} catch(e) { albumEl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||||
@@ -62,7 +62,7 @@ export async function loadRecommendations() {
|
|||||||
if(S.cfg.lastfmKey) {
|
if(S.cfg.lastfmKey) {
|
||||||
await _loadLastfmRecommendations(el, artist, title, html);
|
await _loadLastfmRecommendations(el, artist, title, html);
|
||||||
} else {
|
} else {
|
||||||
await _loadItunesFallbackRecommendations(el, artist, title, html);
|
await _loadDeezerFallbackRecommendations(el, artist, title, html);
|
||||||
const note = document.createElement('div');
|
const note = document.createElement('div');
|
||||||
note.style.cssText = 'margin-top:16px;padding:12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text3)';
|
note.style.cssText = 'margin-top:16px;padding:12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text3)';
|
||||||
note.innerHTML = '💡 <strong>Tipp:</strong> Füge einen Last.fm API Key in den Einstellungen hinzu für bessere Empfehlungen & Similar Artists.';
|
note.innerHTML = '💡 <strong>Tipp:</strong> Füge einen Last.fm API Key in den Einstellungen hinzu für bessere Empfehlungen & Similar Artists.';
|
||||||
@@ -123,11 +123,11 @@ async function _loadLastfmRecommendations(el, artist, title, html) {
|
|||||||
});
|
});
|
||||||
sec.appendChild(list); el.appendChild(sec);
|
sec.appendChild(list); el.appendChild(sec);
|
||||||
}
|
}
|
||||||
} catch { await _loadItunesFallbackRecommendations(el, artist, title, html); }
|
} catch { await _loadDeezerFallbackRecommendations(el, artist, title, html); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _loadItunesFallbackRecommendations(el, artist, title, html) {
|
async function _loadDeezerFallbackRecommendations(el, artist, title, html) {
|
||||||
const results = await itunesSearch(artist, 'musicTrack', 20);
|
const results = await deezerSearch(artist, 'musicTrack', 20);
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
const sec = document.createElement('div');
|
const sec = document.createElement('div');
|
||||||
sec.innerHTML = `<div class="section-title" style="margin-bottom:10px">Mehr von ${esc(artist)}</div>`;
|
sec.innerHTML = `<div class="section-title" style="margin-bottom:10px">Mehr von ${esc(artist)}</div>`;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { loadSpotifyImporter } from './spotify.js';
|
|||||||
import { loadSoundCloudImporter } from './soundcloud.js';
|
import { loadSoundCloudImporter } from './soundcloud.js';
|
||||||
// Circular — safe (all calls happen inside function bodies)
|
// Circular — safe (all calls happen inside function bodies)
|
||||||
import { loadHomeData, loadAlbumRow, loadArtists, loadAlbums, loadGenreDetail } from './home.js';
|
import { loadHomeData, loadAlbumRow, loadArtists, loadAlbums, loadGenreDetail } from './home.js';
|
||||||
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadItunesAlbumDetail, loadPartialAlbumDetail } from './detail.js';
|
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadDeezerAlbumDetail, loadPartialAlbumDetail } from './detail.js';
|
||||||
import { loadPlaylists, loadPlaylistDetail, loadStarred, loadRandom } from './playlists.js';
|
import { loadPlaylists, loadPlaylistDetail, loadStarred, loadRandom } from './playlists.js';
|
||||||
import { loadNewReleases, loadRecommendations } from './discover.js';
|
import { loadNewReleases, loadRecommendations } from './discover.js';
|
||||||
import { invalidateApiCache } from '../api.js';
|
import { invalidateApiCache } from '../api.js';
|
||||||
@@ -79,7 +79,7 @@ export function navigate(page, data=null) {
|
|||||||
if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
|
if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
|
||||||
if(page==='album-detail' && data) loadAlbumDetail(data);
|
if(page==='album-detail' && data) loadAlbumDetail(data);
|
||||||
if(page==='genre-detail' && data) loadGenreDetail(data);
|
if(page==='genre-detail' && data) loadGenreDetail(data);
|
||||||
if(page==='itunes-album-detail' && data) loadItunesAlbumDetail(data);
|
if(page==='deezer-album-detail' && data) loadDeezerAlbumDetail(data);
|
||||||
if(page==='partial-album-detail' && data) loadPartialAlbumDetail(data);
|
if(page==='partial-album-detail' && data) loadPartialAlbumDetail(data);
|
||||||
if(page==='new-releases') loadNewReleases();
|
if(page==='new-releases') loadNewReleases();
|
||||||
if(page==='playlists') loadPlaylists();
|
if(page==='playlists') loadPlaylists();
|
||||||
|
|||||||
+17
-17
@@ -1,6 +1,6 @@
|
|||||||
import { S } from '../state.js';
|
import { S } from '../state.js';
|
||||||
import { esc, toast, fmtCount } from '../utils.js';
|
import { esc, toast, fmtCount } from '../utils.js';
|
||||||
import { api, itunesSearch, itunesLastError, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
|
import { api, deezerSearch, deezerLastError, deezerLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
|
||||||
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js';
|
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js';
|
||||||
import { playPreview } from '../player/player.js';
|
import { playPreview } from '../player/player.js';
|
||||||
import { openDlModal } from '../ui/ui.js';
|
import { openDlModal } from '../ui/ui.js';
|
||||||
@@ -106,7 +106,7 @@ export function onHeaderSearch(val) {
|
|||||||
|
|
||||||
export function onMobileSearch(val) { onHeaderSearch(val); }
|
export function onMobileSearch(val) { onHeaderSearch(val); }
|
||||||
|
|
||||||
// ─── GLOBAL (ITUNES) SEARCH ───
|
// ─── GLOBAL (DEEZER) SEARCH ───
|
||||||
export function onGlobalSearch(val) {
|
export function onGlobalSearch(val) {
|
||||||
clearTimeout(S.globalSearchTimer);
|
clearTimeout(S.globalSearchTimer);
|
||||||
const el = document.getElementById('global-search-results');
|
const el = document.getElementById('global-search-results');
|
||||||
@@ -119,9 +119,9 @@ export async function doGlobalSearch(q) {
|
|||||||
const el = document.getElementById('global-search-results');
|
const el = document.getElementById('global-search-results');
|
||||||
try {
|
try {
|
||||||
const [tracks, albums, artists] = await Promise.all([
|
const [tracks, albums, artists] = await Promise.all([
|
||||||
itunesSearch(q,'musicTrack',25),
|
deezerSearch(q,'musicTrack',25),
|
||||||
itunesSearch(q,'album',10),
|
deezerSearch(q,'album',10),
|
||||||
itunesSearch(q,'musicArtist',6),
|
deezerSearch(q,'musicArtist',6),
|
||||||
]);
|
]);
|
||||||
el.innerHTML = '';
|
el.innerHTML = '';
|
||||||
const trackResults = tracks.filter(t => t.wrapperType === 'track');
|
const trackResults = tracks.filter(t => t.wrapperType === 'track');
|
||||||
@@ -132,10 +132,10 @@ export async function doGlobalSearch(q) {
|
|||||||
artists.forEach(a => {
|
artists.forEach(a => {
|
||||||
const sub = a.fanCount ? `${fmtCount(a.fanCount)} Fans` : 'Deezer Künstler';
|
const sub = a.fanCount ? `${fmtCount(a.fanCount)} Fans` : 'Deezer Künstler';
|
||||||
const c = makeCard(a.artistName, sub, a.artworkUrl100 || null, '🎤');
|
const c = makeCard(a.artistName, sub, a.artworkUrl100 || null, '🎤');
|
||||||
c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName, a.fanCount);
|
c.onclick = () => loadDeezerArtistResults(a.artistId, a.artistName, a.fanCount);
|
||||||
row.appendChild(c);
|
row.appendChild(c);
|
||||||
if (a.artworkUrl100) return; // Deezer artists already carry a photo — no extra lookup
|
if (a.artworkUrl100) return; // Deezer artists already carry a photo — no extra lookup
|
||||||
itunesSearch(a.artistName,'album',1).then(res => {
|
deezerSearch(a.artistName,'album',1).then(res => {
|
||||||
if(!res[0]) return;
|
if(!res[0]) return;
|
||||||
const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300');
|
const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300');
|
||||||
if(!imgUrl) return;
|
if(!imgUrl) return;
|
||||||
@@ -157,15 +157,15 @@ export async function doGlobalSearch(q) {
|
|||||||
albums.forEach(a => {
|
albums.forEach(a => {
|
||||||
const art = a.artworkUrl100?.replace('100x100','300x300');
|
const art = a.artworkUrl100?.replace('100x100','300x300');
|
||||||
const c = makeCard(a.collectionName, a.artistName, art, '💿');
|
const c = makeCard(a.collectionName, a.artistName, art, '💿');
|
||||||
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadItunesAlbum(a); };
|
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadDeezerAlbum(a); };
|
||||||
c.querySelector('.dl-btn-overlay')?.remove();
|
c.querySelector('.dl-btn-overlay')?.remove();
|
||||||
const dlOverlay = document.createElement('button');
|
const dlOverlay = document.createElement('button');
|
||||||
dlOverlay.className = 'dl-btn-overlay';
|
dlOverlay.className = 'dl-btn-overlay';
|
||||||
dlOverlay.title = 'Auf Server laden';
|
dlOverlay.title = 'Auf Server laden';
|
||||||
dlOverlay.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
dlOverlay.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||||
dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,itunesData:a}); };
|
dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,deezerData:a}); };
|
||||||
c.querySelector('.play-overlay').appendChild(dlOverlay);
|
c.querySelector('.play-overlay').appendChild(dlOverlay);
|
||||||
c.onclick = () => loadItunesAlbum(a);
|
c.onclick = () => loadDeezerAlbum(a);
|
||||||
row.appendChild(c);
|
row.appendChild(c);
|
||||||
});
|
});
|
||||||
s.appendChild(row); el.appendChild(s);
|
s.appendChild(row); el.appendChild(s);
|
||||||
@@ -177,7 +177,7 @@ export async function doGlobalSearch(q) {
|
|||||||
s.appendChild(list); el.appendChild(s);
|
s.appendChild(list); el.appendChild(s);
|
||||||
}
|
}
|
||||||
if(!artists.length && !albums.length && !trackResults.length) {
|
if(!artists.length && !albums.length && !trackResults.length) {
|
||||||
const err = itunesLastError();
|
const err = deezerLastError();
|
||||||
if (err) {
|
if (err) {
|
||||||
// Distinguish "genuinely no results" from "search backend is down" so the user knows where
|
// Distinguish "genuinely no results" from "search backend is down" so the user knows where
|
||||||
// the problem is.
|
// the problem is.
|
||||||
@@ -192,9 +192,9 @@ export async function doGlobalSearch(q) {
|
|||||||
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">Suche fehlgeschlagen: ${e.message}</p>`; }
|
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">Suche fehlgeschlagen: ${e.message}</p>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadItunesAlbum(album) {
|
export async function loadDeezerAlbum(album) {
|
||||||
try {
|
try {
|
||||||
const tracks = await itunesLookupAlbum(album.collectionId);
|
const tracks = await deezerLookupAlbum(album.collectionId);
|
||||||
if(!tracks.length) { toast('Keine Tracks gefunden'); return; }
|
if(!tracks.length) { toast('Keine Tracks gefunden'); return; }
|
||||||
const el = document.getElementById('global-search-results');
|
const el = document.getElementById('global-search-results');
|
||||||
const s = document.createElement('div'); s.className = 'search-section';
|
const s = document.createElement('div'); s.className = 'search-section';
|
||||||
@@ -204,7 +204,7 @@ export async function loadItunesAlbum(album) {
|
|||||||
<div style="font-size:15px;font-weight:700">${esc(album.collectionName)}</div>
|
<div style="font-size:15px;font-weight:700">${esc(album.collectionName)}</div>
|
||||||
<div style="font-size:12px;color:var(--text2)">${esc(album.artistName)}</div>
|
<div style="font-size:12px;color:var(--text2)">${esc(album.artistName)}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-outline blue" style="font-size:12px;padding:7px 12px" onclick="openDlModal({artistName:'${esc(album.artistName)}',trackName:'${esc(album.collectionName)}',artworkUrl100:'${album.artworkUrl100||''}',collectionName:'${esc(album.collectionName)}',isAlbum:true,itunesData:${JSON.stringify(album).replace(/"/g,'"')}})">⬇ Album laden</button>
|
<button class="btn-outline blue" style="font-size:12px;padding:7px 12px" onclick="openDlModal({artistName:'${esc(album.artistName)}',trackName:'${esc(album.collectionName)}',artworkUrl100:'${album.artworkUrl100||''}',collectionName:'${esc(album.collectionName)}',isAlbum:true,deezerData:${JSON.stringify(album).replace(/"/g,'"')}})">⬇ Album laden</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-section-title">Tracks</div>`;
|
<div class="search-section-title">Tracks</div>`;
|
||||||
const list = document.createElement('div'); list.className = 'song-list';
|
const list = document.createElement('div'); list.className = 'song-list';
|
||||||
@@ -214,8 +214,8 @@ export async function loadItunesAlbum(album) {
|
|||||||
} catch(e) { toast('Fehler: ' + e.message); }
|
} catch(e) { toast('Fehler: ' + e.message); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadItunesArtistResults(artistId, artistName, fanCount) {
|
export function loadDeezerArtistResults(artistId, artistName, fanCount) {
|
||||||
navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId, itunesFanCount: fanCount });
|
navigate('artist-detail', { id: null, name: artistName, deezerArtistId: artistId, deezerFanCount: fanCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── SEARCH HELPERS ───
|
// ─── SEARCH HELPERS ───
|
||||||
@@ -227,7 +227,7 @@ export function searchSimilarArtist(artistName) {
|
|||||||
|
|
||||||
export async function searchAndPreview(trackName, artistName) {
|
export async function searchAndPreview(trackName, artistName) {
|
||||||
try {
|
try {
|
||||||
const res = await itunesSearch(trackName+' '+artistName, 'musicTrack', 1);
|
const res = await deezerSearch(trackName+' '+artistName, 'musicTrack', 1);
|
||||||
if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar');
|
if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar');
|
||||||
} catch { toast('Suche fehlgeschlagen'); }
|
} catch { toast('Suche fehlgeschlagen'); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { S } from '../state.js';
|
import { S } from '../state.js';
|
||||||
import { esc, toast } from '../utils.js';
|
import { esc, toast } from '../utils.js';
|
||||||
import { itunesSearch } from '../api.js';
|
import { deezerSearch } from '../api.js';
|
||||||
// Circular imports — safe (only used inside function bodies)
|
// Circular imports — safe (only used inside function bodies)
|
||||||
import { makeGlobalTrackItem } from '../ui/components.js';
|
import { makeGlobalTrackItem } from '../ui/components.js';
|
||||||
import { openDlModal } from '../ui/ui.js';
|
import { openDlModal } from '../ui/ui.js';
|
||||||
@@ -236,7 +236,7 @@ function _renderSpotifyPreview(data) {
|
|||||||
(data.tracks || []).forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, data.tracks)));
|
(data.tracks || []).forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, data.tracks)));
|
||||||
section.style.display = '';
|
section.style.display = '';
|
||||||
|
|
||||||
// Lazy-load correct per-track artwork and canonical names via iTunes in the background
|
// Lazy-load correct per-track artwork and canonical names via Deezer in the background
|
||||||
if (count > 0 && S.cfg.backendUrl) {
|
if (count > 0 && S.cfg.backendUrl) {
|
||||||
_loadSpotifyTrackArtwork(data.tracks, list);
|
_loadSpotifyTrackArtwork(data.tracks, list);
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,7 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
|
|||||||
const idx = i + bi;
|
const idx = i + bi;
|
||||||
try {
|
try {
|
||||||
const firstArtist = (t.artistName || '').split(/\s*[,/•]\s*/)[0].trim();
|
const firstArtist = (t.artistName || '').split(/\s*[,/•]\s*/)[0].trim();
|
||||||
const results = await itunesSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1);
|
const results = await deezerSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1);
|
||||||
const match = results[0];
|
const match = results[0];
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
// Always safe to enrich: artwork, preview, explicit, canonical artist name
|
// Always safe to enrich: artwork, preview, explicit, canonical artist name
|
||||||
@@ -305,7 +305,7 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
|
|||||||
if (match.collectionName) t.collectionName = match.collectionName;
|
if (match.collectionName) t.collectionName = match.collectionName;
|
||||||
if (match.releaseDate) t.releaseDate = match.releaseDate;
|
if (match.releaseDate) t.releaseDate = match.releaseDate;
|
||||||
if (match.trackNumber) t.trackNumber = match.trackNumber;
|
if (match.trackNumber) t.trackNumber = match.trackNumber;
|
||||||
// Only use iTunes duration if it looks like a full song (> 30s) — jingles/previews
|
// Only use Deezer duration if it looks like a full song (> 30s) — jingles/previews
|
||||||
// returning a small trackTimeMillis would corrupt the yt-dlp duration filter.
|
// returning a small trackTimeMillis would corrupt the yt-dlp duration filter.
|
||||||
if (match.trackTimeMillis > 30000) t.durationMs = match.trackTimeMillis;
|
if (match.trackTimeMillis > 30000) t.durationMs = match.trackTimeMillis;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
|
|||||||
/* ── BADGES ── */
|
/* ── BADGES ── */
|
||||||
.badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 8px;border-radius:20px;letter-spacing:.3px}
|
.badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 8px;border-radius:20px;letter-spacing:.3px}
|
||||||
.badge-new{background:var(--blue-dim);color:var(--blue)}
|
.badge-new{background:var(--blue-dim);color:var(--blue)}
|
||||||
.badge-itunes{background:rgba(252,100,100,.12);color:#fc6464}
|
.badge-deezer{background:rgba(252,100,100,.12);color:#fc6464}
|
||||||
.badge-local{background:var(--green-dim);color:var(--green)}
|
.badge-local{background:var(--green-dim);color:var(--green)}
|
||||||
|
|
||||||
/* ── GLOBAL SEARCH RESULTS ── */
|
/* ── GLOBAL SEARCH RESULTS ── */
|
||||||
|
|||||||
Reference in new issue
Block a user