Add release art fallback for artist detail hero
Use the first available album, EP, or single artwork when an artist portrait is missing or fails to load, keeping artist detail pages visually populated across library and source-only artists. Refresh the PR description for the artist detail deep-link branch.
This commit is contained in:
parent
a6282b3009
commit
098c787861
2 changed files with 66 additions and 41 deletions
|
|
@ -1,56 +1,57 @@
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Adds a new **Manual Library Match** tool that lets users map a wishlist/sync source track to an existing library track. Once saved, SoulSync can treat that source track as already owned/found instead of repeatedly trying to download it.
|
Adds source-aware artist detail deep links so artist pages can be opened directly as `/artist-detail/:source/:id`, including metadata-source artists from Spotify, Deezer, iTunes, Discogs, Amazon, Hydrabase, and existing library artists.
|
||||||
|
|
||||||
|
This also fixes Discover/download modal artist links that were falling back to `/artist-detail/library/<artist name>` or using the wrong album/card ID as an artist ID.
|
||||||
|
|
||||||
## What Changed
|
## What Changed
|
||||||
|
|
||||||
- Added a centralized Manual Library Match tool on the Tools page and a shortcut from the Sync page.
|
- Added canonical artist detail routes in the SPA:
|
||||||
- Added side-by-side source-track and library-track search UI, plus an existing matches table with removal support.
|
- `/artist-detail/library/<library_artist_id>`
|
||||||
- Added `manual_library_track_matches` persistence with profile/source/source-track scoping.
|
- `/artist-detail/spotify/<spotify_artist_id>`
|
||||||
- Added backend search/list/save/delete endpoints for manual matches.
|
- `/artist-detail/deezer/<deezer_artist_id>`
|
||||||
- Integrated manual matches into download analysis so matched source tracks are marked found and skipped.
|
- `/artist-detail/itunes/<itunes_artist_id>`
|
||||||
- Preserved the distinction between wishlist internal force mode and explicit user Force Download All:
|
- plus other supported metadata sources.
|
||||||
- Wishlist/internal `force_download_all` still honors manual matches.
|
- Preserved legacy `/artist-detail/<id>` behavior as a library fallback.
|
||||||
- User-facing Force Download All ignores manual matches and downloads anyway.
|
- Updated shell routing and deep-link activation so refresh/direct navigation works for nested artist-detail URLs.
|
||||||
- Updated wishlist cleanup so manual matches are removed from the wishlist through:
|
- Updated artist detail navigation to carry `artistSource` through the SPA instead of relying only on artist name/id.
|
||||||
- manual wishlist cleanup,
|
- Improved source-only artist detail loading so provider-fetched artist names are used when the URL only contains the source ID.
|
||||||
- automatic cleanup after DB update,
|
- Prevented source-only artist pages from running library-only ownership/enhancement checks.
|
||||||
- wishlist download analysis.
|
- Treats unknown ownership on source-only discographies as missing/clickable instead of leaving cards stuck on "still checking ownership."
|
||||||
- Prevented manually matched source tracks from being re-added to the wishlist in the common database add path.
|
- Uses release artwork as a generic artist-detail hero fallback when an artist portrait is missing or fails to load.
|
||||||
- Polished the Tools page card so Manual Library Match visually matches the Discovery Pool tool card.
|
- Preserved source/artist IDs from Discover album modals, seasonal albums, cached discovery albums, recent releases, and download modal hero links.
|
||||||
|
- Prevented modal artist links from falling back to fake library routes when a real source artist ID is unavailable.
|
||||||
|
- Returned seasonal album `source` from cached seasonal album rows so seasonal modal links retain their provider context.
|
||||||
|
|
||||||
## Behavior
|
## Behavior
|
||||||
|
|
||||||
- Manual match saved:
|
- Clicking a Spotify artist result can now land on:
|
||||||
- `source + source_track_id + profile_id -> library_track_id`
|
- `/artist-detail/spotify/2YZyLoL8N0Wb9xBt1NhZWg`
|
||||||
- Wishlist cleanup:
|
- Clicking a Deezer artist result can now land on:
|
||||||
- removes the item if a manual match exists.
|
- `/artist-detail/deezer/525046`
|
||||||
- Wishlist add:
|
- Existing library artist links continue to resolve through the library path.
|
||||||
- skips inserting the item if a manual match already exists, returning a harmless success/no-op.
|
- If a source artist resolves to an existing library artist, the page upgrades to the library-backed artist and keeps library-only tools/checks available.
|
||||||
- Wishlist download:
|
- If a source artist is not in the library, the page shows source discography as missing/clickable and skips library-only endpoints.
|
||||||
- checks manual match first, marks found, skips download, and attempts wishlist removal.
|
- If a modal lacks a trustworthy source artist ID, it shows a warning instead of navigating to an invalid library artist URL.
|
||||||
- Normal download with Force Download All off:
|
|
||||||
- checks manual match before normal library matching.
|
|
||||||
- Normal download with Force Download All on:
|
|
||||||
- skips manual matches and downloads selected tracks intentionally.
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
Verified by user:
|
Frontend route tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./.venv/bin/python -m pytest tests/test_manual_library_match.py tests/downloads/test_downloads_master.py
|
cd webui
|
||||||
|
npm.cmd test -- --run src/platform/shell/route-manifest.test.ts src/platform/shell/bridge.test.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Result:
|
Result:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
43 passed in 10.29s
|
2 test files passed
|
||||||
|
10 tests passed
|
||||||
```
|
```
|
||||||
|
|
||||||
Additional local verification:
|
Recommended backend verification:
|
||||||
|
|
||||||
```text
|
```bash
|
||||||
py_compile passed for touched Python files
|
./.venv/bin/python -m pytest tests/test_spa_deep_linking.py tests/metadata/test_artist_source_detail.py
|
||||||
node --check passed for touched JS
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1349,15 +1349,34 @@ function updateArtistHeaderStats(albumCount, trackCount) {
|
||||||
console.log("📊 Using new hero section instead of old header stats");
|
console.log("📊 Using new hero section instead of old header stats");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _isUsableArtistHeroImageUrl(url) {
|
||||||
|
return typeof url === 'string' && url.trim() !== '' && url !== 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getArtistHeroReleaseImage(discography) {
|
||||||
|
for (const bucket of ['albums', 'eps', 'singles']) {
|
||||||
|
for (const release of (discography?.[bucket] || [])) {
|
||||||
|
if (_isUsableArtistHeroImageUrl(release?.image_url)) {
|
||||||
|
return release.image_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function updateArtistHeroSection(artist, discography) {
|
function updateArtistHeroSection(artist, discography) {
|
||||||
console.log("🖼️ Updating artist hero section");
|
console.log("🖼️ Updating artist hero section");
|
||||||
|
|
||||||
|
const artistImageUrl = _isUsableArtistHeroImageUrl(artist.image_url) ? artist.image_url : '';
|
||||||
|
const releaseImageUrl = _getArtistHeroReleaseImage(discography);
|
||||||
|
const primaryHeroImageUrl = artistImageUrl || releaseImageUrl;
|
||||||
|
|
||||||
// Blurred background image (inline-Artists hero treatment) — set whenever
|
// Blurred background image (inline-Artists hero treatment) — set whenever
|
||||||
// we have an image_url; falls back to clearing the bg if not.
|
// we have an image_url; falls back to clearing the bg if not.
|
||||||
const heroBg = document.getElementById("artist-detail-hero-bg");
|
const heroBg = document.getElementById("artist-detail-hero-bg");
|
||||||
if (heroBg) {
|
if (heroBg) {
|
||||||
if (artist.image_url && artist.image_url.trim() !== "" && artist.image_url !== "null") {
|
if (primaryHeroImageUrl) {
|
||||||
heroBg.style.backgroundImage = `url('${artist.image_url}')`;
|
heroBg.style.backgroundImage = `url('${primaryHeroImageUrl}')`;
|
||||||
} else {
|
} else {
|
||||||
heroBg.style.backgroundImage = '';
|
heroBg.style.backgroundImage = '';
|
||||||
}
|
}
|
||||||
|
|
@ -1374,9 +1393,11 @@ function updateArtistHeroSection(artist, discography) {
|
||||||
console.log(` - Image element:`, imageElement);
|
console.log(` - Image element:`, imageElement);
|
||||||
console.log(` - Fallback element:`, fallbackElement);
|
console.log(` - Fallback element:`, fallbackElement);
|
||||||
|
|
||||||
if (artist.image_url && artist.image_url.trim() !== "" && artist.image_url !== "null") {
|
if (primaryHeroImageUrl) {
|
||||||
console.log(`✅ Setting image src to: ${artist.image_url}`);
|
console.log(`✅ Setting image src to: ${primaryHeroImageUrl}`);
|
||||||
imageElement.src = artist.image_url;
|
imageElement.dataset.triedDeezer = '';
|
||||||
|
imageElement.dataset.triedReleaseFallback = artistImageUrl ? '' : 'true';
|
||||||
|
imageElement.src = primaryHeroImageUrl;
|
||||||
imageElement.alt = artist.name;
|
imageElement.alt = artist.name;
|
||||||
imageElement.style.display = "block";
|
imageElement.style.display = "block";
|
||||||
if (fallbackElement) {
|
if (fallbackElement) {
|
||||||
|
|
@ -1388,11 +1409,14 @@ function updateArtistHeroSection(artist, discography) {
|
||||||
};
|
};
|
||||||
|
|
||||||
imageElement.onerror = () => {
|
imageElement.onerror = () => {
|
||||||
console.error(`❌ Failed to load artist image: ${artist.image_url}`);
|
console.error(`❌ Failed to load artist image: ${imageElement.src}`);
|
||||||
// Try Deezer fallback before emoji
|
// Try Deezer fallback, then release art, before the generic icon.
|
||||||
if (artist.deezer_id && !imageElement.dataset.triedDeezer) {
|
if (artist.deezer_id && !imageElement.dataset.triedDeezer) {
|
||||||
imageElement.dataset.triedDeezer = 'true';
|
imageElement.dataset.triedDeezer = 'true';
|
||||||
imageElement.src = `https://api.deezer.com/artist/${artist.deezer_id}/image?size=big`;
|
imageElement.src = `https://api.deezer.com/artist/${artist.deezer_id}/image?size=big`;
|
||||||
|
} else if (releaseImageUrl && imageElement.src !== releaseImageUrl && !imageElement.dataset.triedReleaseFallback) {
|
||||||
|
imageElement.dataset.triedReleaseFallback = 'true';
|
||||||
|
imageElement.src = releaseImageUrl;
|
||||||
} else {
|
} else {
|
||||||
imageElement.style.display = "none";
|
imageElement.style.display = "none";
|
||||||
if (fallbackElement) {
|
if (fallbackElement) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue