Commit graph

2417 commits

Author SHA1 Message Date
Broque Thomas
e40eb213b4 Redesign SoulSync Discover cards to grid layout matching Server Playlists
Some checks failed
Compile the app and run tests / sanity-check (push) Has been cancelled
The current cards are full-width horizontal rows, which makes the tab feel
dense once a few playlists accumulate. Rework to a grid of compact cards
that match the Server Playlists visual language already used on the same
Sync page, so the two surfaces feel like the same app.

Changes (all visual — no impact on JohnBaumb's sync/poll logic):

- `.discover-sync-grid` wrapper renders cards as `repeat(auto-fill,
  minmax(240px, 1fr))`. 4-6 cards per row at desktop width.
- Per-card hue derived deterministically from playlist type so each card
  has a consistent color identity across reloads. Drives the icon tint,
  source badge color, toggle color, action-button gradient, and a soft
  radial glow in the top-right corner.
- Top row: 40x40 icon chip + source badge ("DEEZER" / "SPOTIFY") pushed
  to the top-right (mirrors the chevron placement on Server Playlists).
- Body: playlist name (15px bold, 2-line clamp), track count + status
  pill on one line, last-synced subtle line below.
- Compact toggles in a small bordered panel, stacked rows of
  label+switch. Smaller 34x18 switch matches the denser card size.
- Full-width gradient action button ("Sync Now") at the bottom.
- Hover: card lifts 2px, glow + border intensify.

Deliberately kept purely visual to avoid touching JohnBaumb's logic:
- Button content stays flat text ("Sync Now" / "Syncing...") so his
  `btn.textContent = ...` poll updates still work unchanged.
- Status text format matches the strings his sync-poll code writes
  ("Synced 10/15" / "Synced" / "Not synced" / "Syncing..." / "Failed"),
  so initial render and post-sync updates stay consistent.
- All ids and state-managed class hooks preserved: `discover-sync-card-${type}`,
  `discover-sync-btn-${type}`, `.discover-sync-status`, the
  syncing/synced/not-synced modifier classes, and the
  `discover-sync-card-highlight` deep-link animation class.

Staging branch for review — not targeting dev directly. JohnBaumb can
cherry-pick onto his PR branch if he wants the redesign, or skip it.
2026-04-24 11:21:49 -07:00
JohnBaumb
97b0ee960a fix: malformed CSS, seasonal source column, profile_id filter, discover tab stale flag
- style.css: move orphan color declaration inside .discover-sync-card-meta so downstream rules parse correctly
- seasonal_discovery.py: branch on deezer_track_id for deezer source instead of falling through to spotify_track_id
- web_server.py: add AND profile_id = ? to discovery pool count so other profiles don't inflate the result
- init.js: reset discoverSyncPlaylistsLoaded on page leave so the discover tab refetches on revisit
2026-04-24 01:04:50 -07:00
JohnBaumb
150dd046df remove plan file from branch 2026-04-23 13:43:05 -07:00
JohnBaumb
62797c68af discover: replace Force DL toggle with Any Quality placeholder (Phase 0)
Replace the misleading Force DL toggle on the Sync page Discover tab with
a disabled Any Quality toggle. Stop sending force_download_all from
discover sync so discover playlists run ownership analysis instead of
blindly re-downloading owned tracks.

Adds PLAN-download-quality-flags.md describing the full refactor plan.
2026-04-23 13:37:59 -07:00
JohnBaumb
016e8a4c16 fix: server push status NameError + discover icons as thumbnails
- Fix NameError in both completion handlers: 'database' was not defined
  at module scope, replaced with get_database() call
- Add playlist_id and playlist_name to download_missing batch dicts
  so push prefix check works for those code paths
- Replace generic music note placeholder with playlist-type emojis
  in batch history (Fresh Tape=note, Archives=books, etc.)
- Add source-based fallback icons for mirrored/wishlist/album entries
2026-04-23 12:05:37 -07:00
JohnBaumb
038a0189a4 fix: add placeholder thumbnail for history items without images 2026-04-23 11:39:26 -07:00
JohnBaumb
92077c9117 fix: add tooltip to source dot in batch history 2026-04-23 11:35:45 -07:00
JohnBaumb
1f944841a3 fix: add missing closing brace in style.css that broke discover sync CSS 2026-04-23 11:24:20 -07:00
JohnBaumb
a0eb289d63 Merge origin/fix/downloads-page into dev 2026-04-23 11:13:45 -07:00
JohnBaumb
ddae75f0f9 Merge origin/fix/batch-queue-system into dev 2026-04-23 11:13:34 -07:00
JohnBaumb
055c2c25c5 Merge remote-tracking branch 'origin/feat/discover-sync-tab' into dev 2026-04-23 11:13:15 -07:00
JohnBaumb
0fed25b51a Route discover batch card clicks to discover download modal 2026-04-23 11:13:07 -07:00
JohnBaumb
30e3ed621a Support Plex and Jellyfin in playlist server push, add Jellyfin search_tracks 2026-04-23 10:33:50 -07:00
JohnBaumb
c422c30c21 Skip playlist server push for non-navidrome servers instead of failing 2026-04-23 10:30:27 -07:00
JohnBaumb
d7e1cdeb83 Fix seasonal playlist track ID for non-spotify/itunes sources, fix polling interval leak 2026-04-23 10:26:17 -07:00
BoulderBadgeDad
b4cc3e8975
Merge pull request #363 from JohnBaumb/fix/fork-workflow-guards
Skip Docker/publish workflows on forks
2026-04-23 10:23:54 -07:00
JohnBaumb
7c0f9510c8 Skip Docker/publish workflows on forks
Add repository guard (github.repository == Nezreka/SoulSync) to
cleanup-dev-images, dev-nightly, and docker-publish workflows.
build-and-test stays available for fork contributors.
2026-04-23 09:54:51 -07:00
JohnBaumb
85ea2a810f Merge upstream/dev into feat/discover-sync-tab 2026-04-23 09:50:18 -07:00
JohnBaumb
5706f32ba2 Merge upstream/dev into fix/downloads-page 2026-04-23 09:41:54 -07:00
JohnBaumb
f79cb182d4 Merge upstream/dev into fix/batch-queue-system 2026-04-23 09:33:51 -07:00
BoulderBadgeDad
8f1e1666f2
Merge pull request #361 from Nezreka/feature/unify-search-and-artist-detail
Feature/unify search and artist detail
2026-04-23 08:35:37 -07:00
Broque Thomas
14893c85a9 Extract _build_source_only_artist_detail into core/artist_source_detail.py
JohnBaumb's review: "If we're going to refactor the web_server.py soon,
might as well start moving stuff away from web_server.py in our PRs.
_build_source_only_artist_detail, make it a module, it's perfect."

This continues the pattern the prior commit started with the source-ID
lookup helpers: move the pure data-building logic to a side-effect-free
core module, leave a thin wrapper in web_server.py that bridges the
Flask response and the module-global clients.

**core/artist_source_detail.py** — pure function that takes the artist id,
name, and source plus dependency-injected per-source clients (spotify,
deezer, itunes, discogs) and a Last.fm API key. Returns
(payload_dict, http_status) so it isn't coupled to Flask.

**web_server.py wrapper** — builds the client bag from the module globals
(checks Spotify auth, constructs the Discogs client from the configured
token, reads the Last.fm API key) and wraps the core return in jsonify.
147 lines of logic go away from web_server.py; the 24-line wrapper is
purely glue.

**tests/test_artist_source_detail.py** — 21 focused tests covering the
response envelope, the source-specific ID-field stamping for all six
supported sources, the dedup_variants=False contract (the behaviour
that originally motivated the split of MetadataLookupOptions), per-source
genre/follower extraction with safe handling of missing or throwing
clients, and the Last.fm enrichment branch including the no-key and
error-path cases. Runtime 0.26s.
2026-04-23 08:09:29 -07:00
Broque Thomas
b547909604 Address PR review feedback from JohnBaumb and Cin
Four fixes from the review:

**library.js — back button stack (JohnBaumb):**
Replace the single-slot `artistDetailPageState.originPage` with an origin
stack. Chained navigation like Search → Artist A → similar Artist B →
similar Artist C now walks back one step at a time (C → B → A → Search)
instead of jumping straight to Search and skipping A and B.

`navigateToArtistDetail` takes an optional `{skipOriginPush}` flag so the
back button can re-enter a prior artist without re-pushing onto the stack.
Fresh entries from a non-artist page clear any stale stack from a prior
chain. Duplicate-click detection avoids pushing the same target twice.
Label derivation (`_updateArtistDetailBackButtonLabel`) reads the stack top
so the button says "Back to <ArtistName>" mid-chain and "Back to Search"
at the root.

**library.js — checkArtistEnhanceEligibility after library upgrade (Cin):**
The quality-analysis endpoint only works on library PKs. After the
library-upgrade branch rewrites `currentArtistId` from the source ID to
the library PK, the check was still using the original closure arg, so
upgraded source artists never hit `/api/library/artist/<id>/quality-analysis`.
Use `artistDetailPageState.currentArtistId` so the call gets the resolved id.

**init.js — isPageAllowed + home page recursion (Cin):**
- artist-detail is reachable from both Library and Search results now, so
  permission check accepts either grant (plus legacy 'downloads'/'artists'
  aliases). Search-only profiles can open source artists; legacy artists-only
  profiles no longer recurse on the home redirect.
- `getProfileHomePage` rewrites 'artists' → 'search' (it already rewrote
  'downloads') so legacy home_page values resolve correctly.
- Legacy-compat expanded in isPageAllowed to treat 'artists' as equivalent
  to 'search' in both directions.

**init.js — profile edit forms dropping values on save (Cin):**
Both pageLabels maps (admin edit form + self-edit form) referenced the
legacy 'downloads'/'artists' keys. When editing a profile saved with
`home_page: 'search'` and `allowed_pages: ['search', 'library']`, the
home select didn't render a 'search' option, and the allowed_pages
checkboxes used 'downloads' as their value — so saving the form dropped
both values.

Update both maps to use 'search' as the canonical key. Add
`_normalizeLegacyAllowedPages` and `_normalizeLegacyHomePage` helpers that
migrate any legacy ids in allowed_pages/home_page on read, so a legacy
profile's first save upgrades its stored ids to the new canonical form.
2026-04-23 07:48:40 -07:00
JohnBaumb
735a0ebe58 Fix missing album art on downloads page with multi-layer fallback 2026-04-23 01:48:40 -07:00
JohnBaumb
1ef1a4284e Fire-and-forget discover sync with toast link to sync tab 2026-04-23 01:30:43 -07:00
JohnBaumb
f32da04fd1 Add sync tab deep-linking, redirect Discover sync to sync tab, add Force Download toggle 2026-04-23 01:18:34 -07:00
JohnBaumb
d2bd66c0ce Show server push status in batch history 2026-04-23 00:55:46 -07:00
JohnBaumb
ed013d79b9 Track server push status, expand push to all playlist types, rename to _push_playlist_to_server 2026-04-23 00:55:11 -07:00
JohnBaumb
11b74d7c14 Widen batch panel to 400px, shorten stat labels 2026-04-23 00:46:54 -07:00
JohnBaumb
9b3e65c3d8 Two-line batch history layout with descriptive stats 2026-04-23 00:44:59 -07:00
JohnBaumb
332392f338 Improve batch history: thumbnails, source badges, detailed stats 2026-04-23 00:40:33 -07:00
Broque Thomas
1b3751598d Stop sidebar nav items bleeding through the sticky header
The sticky .sidebar-header had two layered issues that let nav items
show through it while the user scrolled the sidebar:

  - its background was a single linear-gradient of rgba() stops,
    starting at ~14% accent on transparent — the upper portion of the
    header was effectively translucent
  - .sidebar > * sets z-index 1 on every sidebar child, so the header
    and the nav buttons share a stacking level. Sticky alone doesn't
    lift the header; with equal z-index the nav wins on DOM order

Layer the existing accent gradient over a solid rgb(18, 18, 18) base
(visual unchanged, fully opaque), and bump the header to z-index 2 so
it paints above the nav buttons as they scroll under it.
2026-04-22 22:49:34 -07:00
Broque Thomas
1aedc2ddcf Don't highlight sidebar Library when on /artist-detail
Cin: arriving at artist-detail from Search/Discover/Watchlist
highlighted the Library sidebar entry, which is misleading — the user
didn't navigate via Library. The hardcoded mapping was a holdover from
when artist-detail was reached only from the (now retired) Artists page.

Drop the special case so artist-detail behaves like playlist-explorer:
no [data-page] match in the sidebar, no highlight. The user's actual
origin page is already preserved on the back button.

The deep-link fallback in _getPageFromPath (artist-detail → library)
is left intact: if someone pastes /artist-detail in the URL bar with
no state to render, library is still the most sensible landing page,
and sidebar-highlighting Library in that scenario is correct because
they're literally on Library.
2026-04-22 22:25:06 -07:00
Broque Thomas
02f26bf338 Make ApiCallTracker.save() atomic to prevent corrupt history files
Cin observed that database/api_call_history.json was occasionally
landing on disk truncated mid-write — `_load()` would log
`History file is not valid JSON, starting fresh` and 24h of metrics
would be lost.

Root cause: `save()` opened the file in 'w' mode (which truncates to
0 bytes immediately) and then streamed JSON via `json.dump`. Any
SIGINT/SIGTERM/crash between truncate and final write left the file
half-formed — exactly Cin's symptom of the JSON cutting off mid-array.

Switch to the standard atomic pattern: write to a sibling .tmp file,
flush + fsync, then `os.replace` (atomic on every platform we run on).
Failed writes also clean up the leftover .tmp file. The canonical
file is now either the previous good copy or the new good copy —
never a partial one.
2026-04-22 22:15:09 -07:00
Broque Thomas
e66af77ff6 Make artist_name Optional in find_library_artist_for_source
Cin's review note: typing artist_name as plain `str` forced callers
that didn't have a name to pass `""` as a placeholder, which leaks the
parameter's emptiness contract into every call site and reads badly in
tests. Switching to `Optional[str] = None` lets callers omit it.

The function body's `if artist_name and active_server:` check already
handles None and "" identically, so no body changes were needed. Tests
that previously passed `artist_name=""` drop the argument; one new test
covers the omitted-arg path explicitly.

The web_server.py wrapper takes the same default for symmetry.
2026-04-22 22:15:03 -07:00
Broque Thomas
a097cf3d5a Extract source-artist lookup helpers from web_server.py to core module
Cin pointed out that the prior version of test_artist_source_lookup.py
AST-parsed web_server.py to verify a constant and to string-match a
function's response keys. That was a workaround for the fact that
web_server.py can't be imported at test time (it boots Spotify,
Soulseek, Plex, etc.) — the right answer is to move the logic into a
side-effect-free module so it can be imported and tested directly.

This commit:
  - adds core/artist_source_lookup.py containing the SOURCE_ID_FIELD
    map, the SOURCE_ONLY_ARTIST_SOURCES set, and find_library_artist_for_source
  - replaces the inline definitions in web_server.py with imports +
    a thin wrapper that injects the active media server
  - rewrites the tests to import from the core module directly:
      * mapping correctness is now a plain equality assertion
      * lookup behaviour is exercised against a real MusicDatabase
      * the AST parse and the string-matching contract test class are
        gone
  - drops the _build_source_only_artist_detail contract test entirely
    (the weakest of the four — it was just string-matching the function
    body); when that function moves to core/ it can get a real
    behavioural test alongside.

Test runtime drops from ~161s to ~5.8s. All 18 tests pass.
2026-04-22 22:07:23 -07:00
Broque Thomas
f684bd603d Keep Search page dropdown open across result clicks and modal close
On the unified Search page the results dropdown was dismissed three
ways that didn't match user intent:

  - clicking an album row called hideDropdown() before opening the
    download modal, so the dropdown was already gone by the time the
    modal closed
  - clicking a track row did the same
  - clicking the play button on a track row did the same
  - the outside-click handler treated a click inside the download
    modal (its close button or backdrop) as a click outside the
    dropdown, dismissing it on modal close

Reported by Cin: "clicking an album in the search results opens a
download modal as expected, but closing said modal also hides the
search results in the same go."

Drop the explicit hideDropdown() calls from those three handlers and
whitelist .download-missing-modal in the outside-click handler. The
dropdown now persists across the click + modal lifecycle so the user
can pick another result without re-running the search. Artist clicks
still dismiss because they navigate to /artist-detail.

The global search popover keeps its existing dismiss-on-click
behaviour — its high z-index conflicts with the modal stack and
auto-dismiss is the right pattern for a Spotlight-style popover.
2026-04-22 21:55:13 -07:00
Broque Thomas
12c23b6b89 Add regression tests for source-artist lookup + dedup_variants flag
Four targeted backend tests for behaviour added during the Search/Artists
unification work:

1. _SOURCE_ID_FIELD mapping is parsed out of web_server.py via AST and
   compared against an explicit expectation, so silent renames break the
   test instead of silently breaking library-upgrade detection.

2. Every column in _SOURCE_ID_FIELD must exist on the real artists table
   after migrations run. This is the schema-vs-query contract that the
   `deezer_artist_id` typo would have failed instantly.

3. The two queries from the watchlist-config enrichment path execute
   verbatim against a fresh DB — separate ones for the artists table
   (deezer_id / discogs_id) and the watchlist_artists join (deezer_artist_id).
   Documents the column-name split that caused the original bug.

4. Static contract test for _build_source_only_artist_detail's response
   shape: every JSON key the frontend reads (success/artist/discography/
   image_url/server_source/genres/lastfm_*) must appear in the function
   source, plus the dynamic source-id stamp and the dedup_variants=False
   opt-out.

Plus a behavioural test for MetadataLookupOptions.dedup_variants=False
in test_metadata_service_discography.py — proves the flag actually keeps
variant releases that get_artist_detail_discography would otherwise
collapse to a single canonical entry.
2026-04-22 21:14:11 -07:00
Broque Thomas
7625362c49 Fix date-dependent watchlist scanner tests failing on CI
The three discovery-pool tests hardcoded release_date strings
("2026-04-01", "2026-04-16") that were checked against a rolling
`datetime.now() - timedelta(days=7)` (or 21-60 day) cutoff in the
scanner. Once the wall clock advanced past the cutoff window the
releases were filtered out and the assertions failed — Python 3.11
Linux CI was already past 2026-04-23 UTC.

Replace the hardcoded values with a module-level
`_RECENT_RELEASE_DATE = now - 2 days` so the fixtures stay inside
every cutoff window regardless of when the suite runs.
2026-04-22 20:56:10 -07:00
Broque Thomas
adcfd2db70 Fix 'no such column: deezer_artist_id' on watchlist artist config GET
The library-enrichment query inside /api/watchlist/artist/<id>/config
queries the `artists` table but used the column names from the
watchlist_artists table:

  WHERE spotify_artist_id = ? OR itunes_artist_id = ?
        OR deezer_artist_id = ? OR discogs_artist_id = ?

The `artists` table actually uses `deezer_id` and `discogs_id` for
those two columns (only `watchlist_artists` uses the `_artist_id`
suffix). The mismatch threw `no such column: deezer_artist_id` on
every config GET, which was caught by the surrounding try/except and
logged — releases came back empty and Spotify/genres etc. fell back
to defaults.

Visible side effects: the request that LOOKED slow ('1420.2ms') and
the recurring ERROR line in app.log every time a watchlist artist
overlay opened.

Watchlist-config GET now returns proper banner_url / summary / style
/ mood / label / genres for Deezer- and Discogs-source artists too.
The other watchlist queries in this endpoint (42302 / 42315 / 42379)
correctly target watchlist_artists and stay as-is.
2026-04-22 20:42:56 -07:00
Broque Thomas
b0a07f993a Clean up stale Artists-page references in helper.js + docs.js
helper.js click-for-help annotations had ~10 entries pointing at DOM
elements from the retired inline Artists page (#artists-search-input,
#artists-back-button, #artists-results-state, #artists-cards-container,
#artists-hero-section, .artists-hero-name/badges/genres/bio/stats,
#artist-detail-watchlist-btn/-settings-btn, .artist-detail-tabs,
#albums-tab, #singles-tab, #album-cards-container, #singles-cards-
container) plus #similar-artists-section pointing at the legacy id
(now #ad-similar-artists-section on the standalone page).

Replaced the dead Artists-page block with annotations that target
elements actually present on the standalone /artist-detail page:
.album-card, .completion-overlay, #ad-similar-artists-section,
.similar-artist-bubble, plus a new entry for .search-source-picker-
container on the unified Search page.

docs.js: 'How to: Set Up Auto-Downloads' step 1 used to read 'Search
for artists on the Artists page and click the Watch button on each
one'. Updated to 'Find artists via the Search page (or click an
artist anywhere in the app), then click the Watch button on the
artist detail page' — matches the post-unification flow.

Backend API endpoint references in docs.js (/library/artists, etc.)
are unrelated to the retired frontend page and stay as-is.
2026-04-22 20:29:32 -07:00
Broque Thomas
135f6b9ea1 playStatsTrack: fall back to streaming sources when not in library
The Top Tracks sidebar play button on the artist-detail page (and the
same buttons on the Stats page) called /api/stats/resolve-track and
gave up with a 'Track not found in library' toast on a miss.

Now when the library lookup misses, falls through to /api/enhanced-
search/stream-track — the same Soulseek/YouTube/streaming-source
pipeline the search-results play button uses. So Last.fm popular
tracks, recent plays, and stats artist top tracks all play even if
you don't own the track yet.

Library hit still wins (faster, full quality). Only on miss does it
escalate to streaming. Final error toast updated to reflect both
paths having been tried.
2026-04-22 20:07:28 -07:00
Broque Thomas
20dfcf10a5 Bump artist-detail card size 25% (180→225px min, 150→190px on small screens) 2026-04-22 20:02:49 -07:00
Broque Thomas
648e46d460 Update streaming completion handler to target the new big-photo card overlay
The library completion stream calls updateLibraryReleaseCard once per
release as ownership resolves. The handler was still updating the OLD
card markup (.completion-text + .completion-fill / .completion-bar),
so cards rendered with the new .completion-overlay badge stayed stuck
in the pulsing 'Checking…' state forever.

Now updates the new structure in place:
  - Toggles the .completion-overlay state class (checking → completed
    / nearly_complete / partial / missing) which the existing CSS uses
    to colour and stop the checking-pulse animation.
  - Rewrites the inner .completion-status text:
      Owned          → '✓ Owned'
      Partial        → 'X/Y' (75%+ → nearly_complete badge, else partial)
      Missing        → 'Missing'
  - Sets a tooltip on the overlay with detailed track counts.

Per-card .release-card.checking class also gets removed when state
resolves (stops the whole-card opacity pulse).
2026-04-22 19:59:35 -07:00
Broque Thomas
59298b5b17 Restore big-photo album cards on artist-detail page
The standalone /artist-detail page rendered releases via createReleaseCard
in a stacked layout: square image on top, then title, then year, then a
completion bar — all inside a 300px-tall card with internal padding. The
inline Artists page (now retired) used a richer treatment: full-bleed
artwork with a dark gradient overlay and the title + year pinned at the
bottom. This commit brings that look to the standalone page.

Card markup (still .release-card so all the existing JS filter +
state hooks work, plus .album-card for the visual):

  <div class="release-card album-card" ...>
    <div class="album-card-image" data-bg-src="..."></div>
    <div class="completion-overlay [state]">
        <span class="completion-status">...</span>
    </div>
    <div class="album-card-content">
        <div class="album-card-name">title</div>
        <div class="album-card-year">year</div>
    </div>
    [optional .mb-card-icon]
  </div>

Image loads lazily via the existing observeLazyBackgrounds /
data-bg-src plumbing in core.js — call moved into populateRelease-
Section so each batch of new cards gets observed.

Completion overlay (top-right floating badge):
  - Library artists: 'Checking…' / '✓ Owned' / 'N/M' / 'X%' / 'Missing'
    based on release.owned + track_completion shape (existing logic
    preserved, just rendered as a badge instead of a bar).
  - Source artists (no library data): omitted entirely. The card just
    shows artwork + title + year, which is what the user asked for.

CSS: scoped overrides under #artist-detail-page .release-card.album-card
neutralize the old release-card background gradient, internal padding,
fixed 300px height, and flex column layout. Cards become aspect-ratio:1
square with overflow:hidden so the image fills and the gradient + text
sit on top.

Filter state (data-is-live / data-is-compilation / data-is-featured)
still tagged on each card so the Include filter group keeps working.

Smoke: library Kendrick Lamar should now look like the inline Artists
page used to — square cards, big artwork, name + year on the bottom.
Source-clicked artist (Schoolboy Q from Deezer) shows the same
visual without the completion overlay.
2026-04-22 19:53:56 -07:00
Broque Thomas
9b52c1d94f Smart back button on artist-detail — labels and routes by origin
The "← Back to Library" button on /artist-detail was hardcoded to
navigate back to the Library page regardless of where the user came
from. Now it captures the originating page and labels/routes
accordingly.

navigateToArtistDetail captures currentPage at call time (before the
swap to artist-detail) and stashes it on
artistDetailPageState.originPage. Falls back to library when the
origin can't be determined or when chaining detail-to-detail (e.g.
clicking a Similar Artist on the detail page).

Back button label now adapts:
  - From Library card click → "← Back to Library"
  - From Search result → "← Back to Search"
  - From Discover hero / Your Artists → "← Back to Discover"
  - From Watchlist artist detail → "← Back to Watchlist"
  - From Wishlist / Stats / Explorer / Automations / Dashboard etc.
    → corresponding labels
  - Unknown origin → "← Back to Library"

Click handler navigates to the captured origin instead of always
going to library. State is cleared on click so a fresh artist-detail
view starts clean next time.
2026-04-22 19:35:17 -07:00
Broque Thomas
77567a5eda Sync currentArtistId on library upgrade + hide Similar Artists in Enhanced view
Two bugs from the source-artist click flow:

1. After the backend's library upgrade kicks in (clicking a Deezer
   result for an artist you already own routes through the library
   path), the response's data.artist.id is the library PK while
   artistDetailPageState.currentArtistId still held the source id.
   Toggling Enhanced view then fired
   /api/library/artist/<source_id>/enhanced which 404'd because the
   source id isn't a library PK.
   loadArtistDetailData now updates currentArtistId from
   data.artist.id whenever they differ — Enhanced view, completion
   checks, server sync etc. all use the right id.

2. The Similar Artists section is part of the standard view and was
   staying visible when Enhanced view toggled on. toggleEnhancedView
   now hides #ad-similar-artists-section in the same flow that
   hides .discography-sections.
2026-04-22 19:27:17 -07:00
Broque Thomas
9106617538 Show collection bars + Top Tracks for source artists, upgrade source clicks to library when possible
Three issues from screenshots:

1. .collection-overview was hidden for source artists (CSS rule too
   aggressive). It actually renders fine — just shows 0/N "missing"
   for each release type, which is useful info. Removed from the hide
   rules.

2. #artist-hero-sidebar (Top Tracks "Popular on Last.fm") was also
   hidden. The renderer (_loadArtistTopTracks in library.js) already
   fetches by artist name via /api/artist/0/lastfm-top-tracks, so it
   works for source artists too. Removed from the hide rules.

3. Clicking a source-artist result for someone you ALREADY have in
   the library was loading the bare source view instead of the
   library view (bug). Backend now does a "library upgrade" lookup
   in get_artist_detail: when the direct ID lookup misses but a
   source param is provided, search the artists table by the source-
   specific ID column (deezer_id / spotify_artist_id / etc.). If a
   match exists, use that library PK and the rest of the library
   path runs normally — owned releases, enrichment, completion
   bars, all the goodies you'd see if you'd clicked from Library.
   Falls back to a name match within the active server, then to the
   source-only response if nothing matches.

The remaining library-only items (artist-enrichment-coverage, Radio
button, Enhance Quality button) stay hidden for source artists since
they all require owned tracks.
2026-04-22 19:19:48 -07:00
Broque Thomas
5c94b87aea Pass lastfm api_key when enriching source-only artist response
LastFMClient() with no args has no api_key -> get_artist_info silently
returns None -> source artists never see bio/listeners/playcount even
though my previous commit was supposedly fetching them.

Now reads config_manager.get('lastfm.api_key') and only attempts the
enrichment when the key is configured. Users without Last.fm credentials
in Settings simply get image+name+badges+genres on source artists
(everything except bio + stats), which is fine.
2026-04-22 17:22:11 -07:00
Broque Thomas
f936b8cb12 Enrich source-only artist-detail response and skip discography dedup for source artists
Source artists landing on /artist-detail were rendering an almost-blank
hero — image + name + a tiny Download button — because the backend
response only had {id, name, image_url, server_source: null, genres: []}.
The library.js renderers do their best with what they have, and that
wasn't much.

Backend changes (_build_source_only_artist_detail):
  - Set the source-specific ID field (deezer_id / spotify_artist_id /
    itunes_artist_id / discogs_id / soul_id / musicbrainz_id) on
    artist_info so the corresponding service badge renders on the hero.
  - Try the source's own get_artist_info / get_artist for genres +
    followers (Spotify always; Deezer/iTunes/Discogs when available).
    Spotify also fills image_url if metadata_service.get_artist_image_url
    came up empty.
  - Last.fm enrichment by artist name — bio + listeners + playcount +
    lastfm_url. Mirrors what library artists get from the cached
    enrichment workers but on demand for source artists.
  - All enrichment lookups are wrapped in try/except so a 500 from any
    one source doesn't break the whole response.

Frontend (library.js populateArtistDetailPage):
  - Watchlist button now initialises for source artists too. Falls back
    to artist.id + artist.name when there's no canonical Spotify
    identity (which is the common case for non-library artists).

Discography dedup opt-out:
  - Added dedup_variants flag to MetadataLookupOptions (default True so
    library artists are unchanged). Source-only path now passes
    dedup_variants=False so every "Deluxe Edition" / "Remastered" /
    "Anniversary" variant the source returns is shown — matches the
    inline /artists page behaviour the user was comparing against.

Result: source artists' hero now shows badges + bio + listeners +
playcount + watchlist button + genres in addition to image and name.
Discography lists every release the source returns, not the deduped
canonical view.
2026-04-22 17:14:39 -07:00