Commit graph

1178 commits

Author SHA1 Message Date
Broque Thomas
86e6d8df49 Fix source-picker review items: real logos, cached click close, Soulseek clip
Three follow-up fixes after browser testing:

1. Clicking a source whose results are already cached was closing the
   results dropdown. The outside-click handler treated the icon click
   as "outside" because the icon row lives above the input wrapper, not
   inside it. The icon click handler now calls stopPropagation so the
   document handler never runs. Also added an `#enh-source-row`
   whitelist to the search-page outside-click handler as a second
   layer of defense.

2. The icon chips used generic emojis (🎵, 🍎, 🎶, etc.) which don't
   convey brand identity. SOURCE_LABELS now carries a `logo` URL per
   source (mirroring the existing constants in core.js): the real
   Spotify / Apple Music / Deezer / Discogs / MusicBrainz / Hydrabase /
   Soulseek brand logos render as <img> inside the chip. Music Videos
   stays on emoji since the codebase has no YouTube-specific logo
   constant. renderSourceRow (Search page) and _gsSourceRowHtml (global
   widget) both honor the new field; loading state still overrides
   with an hourglass.

3. When Soulseek was selected, the icon row appeared clipped at the
   top of the page. Caused by the flex parent (.downloads-main-panel)
   compressing the row when .search-section.active competes for space
   with flex-grow:1. Added `flex-shrink: 0` + explicit `overflow-y: visible`
   on both .enh-source-row and .gsearch-source-row so the row keeps
   its natural height even under layout pressure. Logo <img> elements
   got explicit 22x22 / 18x18 containers so they render at chip scale
   without the inline font-size hack.
2026-04-23 17:28:15 -07:00
Broque Thomas
553ede8de9 Update interactive help + What's New for the source-picker redesign
The existing 2.40 WHATS_NEW entry described the short-lived "Search
from" dropdown that preceded this redesign. Updated to describe the
icon row + per-query cache + rate-limit fallback banner + global widget
parity that actually ships.

Click-for-help annotations and the "First Download" tour now point at
`#enh-source-row` (the new icon container) instead of the deleted
`.search-source-picker-container` dropdown and the deleted
`.enh-source-tabs` post-search tab bar. Adjusted the enhanced-search
tips so "multi-source tabs compare results" doesn't mislead — the
icons above the bar are how you compare now.

Version stays at 2.39 — the 2.40 WHATS_NEW section is accumulating
under the "Search & Artists unification" umbrella and will publish
when the whole 2.40 cycle ships.
2026-04-23 17:28:15 -07:00
Broque Thomas
9ddfcf254f Global search widget: same source-picker icon row + per-source cache
Matches the Search page redesign so both surfaces behave identically.
The sidebar popover previously always fan-out-fetched all sources on
every keystroke (via _gsFetchSourceStream streaming NDJSON for every
alternate) and exposed a post-search tab bar to switch views.

Now:

- The popover renders an always-visible source icon row at the top, one
  icon per source (Spotify, iTunes, Deezer, Discogs, Hydrabase,
  MusicBrainz, Music Videos, Soulseek).
- Typing fetches only the currently-selected source. No fan-out.
- Clicking a different icon: cache hit -> instant re-render; cache miss
  -> single-source fetch + render.
- Per-query cache cleared on query change; cache dots on icons show
  which sources already have results for the current query.
- Default active source read from /api/settings (metadata.fallback_source)
  on first focus; falls back to Spotify.
- Fallback banner shown when the backend served a different source than
  the one clicked (rate-limit auto-fallback).
- Soulseek icon click navigates to /search with the query pre-filled,
  since the raw file list doesn't fit the popover. The Search page
  takes over rendering from there.

Gone: _gsFetchSourceStream (fan-out), _gsRenderTabs, _gsSwitchSource,
_gsState.altAbortCtrl, per-section _loading sets.
Added: _gsInitDefaultSource, _gsFetchSource, _gsFetchYouTubeVideos,
_gsSourceRowHtml, _gsFallbackBannerHtml, _gsSetActiveSource,
_gsNavigateToSearchPage.
2026-04-23 17:28:15 -07:00
Broque Thomas
a72810ce22 Search page: replace fan-out with source-picker icon row + per-source cache
The Search page previously fired a primary /api/enhanced-search request
plus a fan-out loop (_queueAlternateSourceFetches / _fetchAlternateSource)
that streamed NDJSON from /api/enhanced-search/source/<src> for every
other configured source. One search = 7 API calls across Spotify, iTunes,
Deezer, Discogs, Hydrabase, MusicBrainz, and YouTube Music Videos. The
post-search tab bar then let users switch views between the results that
had already been fetched.

This changes the default to explicit per-source selection:

- The old <select id="search-source-select"> dropdown and the
  <div id="enh-source-tabs"> post-search tab bar are replaced by a
  single always-visible icon row (#enh-source-row) above the search
  bar. One button per source, horizontal-scroll on narrow screens.
- Typing fetches only the currently-selected source. No fan-out.
- Clicking a different icon switches to that source and fetches it
  on demand, unless results for this query are already cached.
- Per-query cache (Map keyed by source) is cleared whenever the query
  changes; cached icons show a small dot, loading icons show a spinner.
- Soulseek is a first-class icon in the row — selecting it routes to
  the existing raw-file basic search, no change to that renderer.
- YouTube Music Videos is its own icon, still uses the NDJSON stream
  endpoint for incremental rendering.
- Default active icon reads metadata.fallback_source from /api/settings
  on init; falls back to Spotify.
- Rate-limit fallback (backend serves Deezer when Spotify is banned)
  surfaces as an amber banner above results plus an amber border on the
  clicked icon, so users understand why the returned results don't
  match the source they picked.

SOURCE_LABELS in shared-helpers.js gains an 'icon' field per source and
a new SOURCE_ORDER constant for the canonical picker order. The fan-out
functions (_queueAlternateSourceFetches, _fetchAlternateSource,
renderSourceTabs, window._switchEnhSourceTab) are gone.

Backend untouched — POST /api/enhanced-search already supported a
`source` param for single-source mode; we were just never using it by
default. Global widget redesign to match is the next commit.
2026-04-23 17:28:15 -07:00
Broque Thomas
f85564a2de Move enhancedSearchFetch, SOURCE_LABELS, renderCompactSection to shared-helpers
These three utilities lived inside search.js — the fetch helper at module
scope, and SOURCE_LABELS plus renderCompactSection as closures inside
initializeSearchModeToggle. The global search widget in downloads.js
already depends on enhancedSearchFetch via global scope and re-implements
the rendering inline.

Hoist all three to shared-helpers.js so both surfaces share the same
implementations. No behavior change — this is the refactor step that
precedes the source-picker redesign.

Also adds a 'soulseek' entry to SOURCE_LABELS for the upcoming icon row.
2026-04-23 17:28:15 -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
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
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
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
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
Broque Thomas
93f1941829 Unify artist detail: route source artists to standalone page, retire inline Artists page
Completes the artist-detail unification. Source artists now land on
the same /artist-detail page as library artists (with the source-aware
backend endpoint from earlier this session handling the data fetch).
The inline Artists page is gone — artists.js deleted, #artists-page
HTML block removed, /artists URL aliases to /search.

  Source-artist callsites re-migrated from selectArtistForDetail to
  navigateToArtistDetail (search results, global widget, download
  modal, Discover hero / Your Artists cards / artmap context / genre
  deep-dive, watchlist artist detail).

  Visual upgrade to standalone hero: added .artist-detail-hero-bg +
  .artist-detail-hero-overlay (blurred image bg, dark gradient — same
  treatment as the inline page). library.js sets the bg image when
  loading an artist.

  Library-only UI hidden via CSS for source artists (existing rules
  from the previous commit cover Enhanced toggle, Status filter,
  completion bars, enrichment coverage, Top Tracks sidebar, Radio /
  Enhance buttons).

  Final 2 helpers (lazyLoadArtistImages used by wishlist-tools,
  showCompletionError used by completion checker) moved from
  artists.js into shared-helpers.js. The inline-page candidate set
  was dropped from _resolveSimilarArtistsTargets.

  init.js: 'artists' alias added at top of navigateToPage (same
  pattern as the existing 'downloads' alias). 'case artists:' handler
  removed from loadPageData. _getPageFromPath now maps artist-detail
  to library as its parent (matches the existing nav highlight at
  init.js:2161).

  tests/test_script_split_integrity.py: artists.js removed from
  SPLIT_MODULES; KNOWN_CROSS_FILE_DUPES updated to point escapeHtml
  at shared-helpers.js instead of artists.js. 354/354 tests pass.

  Net delta: -1700 lines.

Stays at 2.39. Once you've verified end-to-end (library artist ->
hero looks like inline visual; source artist from Search -> same
page, similar artists works, no 404s; /artists URL -> /search), a
follow-up commit bumps to 2.40 with the full WHATS_NEW entry that's
already prepped.
2026-04-22 17:00:52 -07:00
Broque Thomas
5219780c01 Page-aware click on similar-artist bubbles
The bubble click handler hardcoded selectArtistForDetail (the inline
Artists page navigator). On the standalone /artist-detail page it
fired but couldn't actually navigate anywhere — the page just
re-rendered the similar-artists section for the same artist instead
of moving to the clicked artist.

Now: detect whether the standalone page is active. If yes,
navigateToArtistDetail(id, name, source). Otherwise fall back to
selectArtistForDetail for the inline page (unchanged behaviour
there). Both surfaces work correctly without the caller having to
know which page they're on.
2026-04-22 16:43:36 -07:00
Broque Thomas
6a76405444 Add Similar Artists to standalone /artist-detail page, hide library-only UI for source artists
First increment of the artist-detail unification redesign. Delivers
the two most-visible missing pieces for source artists without touching
the hero layout — that's a later commit.

Changes:
  - HTML: new #ad-similar-artists-section inside #artist-detail-main
    (scoped IDs with 'ad-' prefix so they don't collide with the inline
    Artists page, which has the same section using base IDs).
  - shared-helpers.js: similar-artists helpers (loadSimilarArtists +
    display/progressive/createBubble + lazy image loader) moved out of
    artists.js. New _resolveSimilarArtistsTargets() resolver picks
    whichever candidate set has a `.page.active` ancestor, so the same
    function works on both the inline Artists page and the standalone
    artist-detail page without caller changes.
  - library.js populateArtistDetailPage: sets
    document.body.dataset.artistSource = 'library' | 'source' before
    rendering, and fires loadSimilarArtists(artist.name) after
    populating the rest of the page.
  - style.css: body[data-artist-source='source'] rules hide
    library-only UI on the artist-detail page — Enhanced view toggle,
    Status (owned/missing) filter, completion bars, enrichment
    coverage, Top Tracks sidebar, Radio / Enhance Quality buttons,
    "X owned / Y missing" section-stats counts. CSS-only, additive,
    library artists completely unaffected.

Impact today:
  - Library artists: Similar Artists section now appears at the
    bottom of their detail page (previously only the inline Artists
    page had it). All other UI unchanged.
  - Source artists: still route to the inline Artists page (Part B
    reverted earlier this session). The standalone page is now
    source-ready infrastructure-wise, but source artists don't reach
    it yet. A later commit will re-migrate source callers to the
    standalone page once the hero rendering is also source-ready.

artists.js shrinks from 1903 -> 1584 lines (similar-artists block
extracted). shared-helpers.js grows correspondingly. 357/357 tests
still pass. No version bump — this is still 2.39 pending.
2026-04-22 16:19:25 -07:00
Broque Thomas
18146098a7 Revert "Route source-artist clicks to standalone /artist-detail page"
This reverts commit 1c345e4eb5.
2026-04-22 15:52:54 -07:00
Broque Thomas
1a8071d6ec Revert "Retire artists.js and inline Artists page, ship unification at 2.40"
This reverts commit 71ff5cb5c3.
2026-04-22 15:52:53 -07:00
Broque Thomas
71ff5cb5c3 Retire artists.js and inline Artists page, ship unification at 2.40
Part D + E of the deferred cleanup + the final version bump that
publishes the whole Search/Artists unification project.

Deletions:
  - webui/static/artists.js (1903 lines) — removed entirely. The 2
    remaining externally-referenced helpers (lazyLoadArtistImages +
    showCompletionError) moved into shared-helpers.js first.
  - webui/index.html — 140-line #artists-page HTML block and the
    <script src="artists.js"> tag both removed.

init.js wiring:
  - 'case artists:' removed from loadPageData switch (no page to init).
  - navigateToPage top-level alias extended: 'artists' → 'search'
    (same pattern as the existing 'downloads' → 'search' alias).
    Legacy /artists bookmarks land on the unified Search page, the
    natural place to find an artist now.
  - _getPageFromPath now maps artist-detail → library as its parent
    (was artists). Matches the existing library-nav-highlight at
    init.js:2161.

Version bump:
  - _SOULSYNC_BASE_VERSION 2.39 → 2.40.
  - WHATS_NEW entries lose the 'unreleased' scaffolding and gain a
    new top entry summarizing the unified artist-detail page + the
    final artists.js retirement.
  - version-info modal gets a 'Search & Artists Unification' section
    at the top.
  - The _getLatestWhatsNewVersion filter added during the unreleased-
    tracking phase is rolled back — entries now display as soon as
    they land in WHATS_NEW, matching the pre-unification behaviour.

Test suite:
  - tests/test_script_split_integrity.py SPLIT_MODULES updated:
    'artists.js' dropped, 'shared-helpers.js' added. escapeHtml's
    cross-file dupe list entry updated to reference shared-helpers.
  - 354/354 tests pass.

User-visible result after this commit:
  - Sidebar: Search, Downloads, Discover, Library, Wishlist, etc. —
    no more Artists entry.
  - Click any artist anywhere: lands on the same /artist-detail page.
  - Search page has a source dropdown; Soulseek is just another option.
  - Legacy /downloads and /artists URLs alias to /search.
  - Version button shows v2.3 (Docker major); "What's New" panel
    opens to the unification summary.

Closes the project Cin requested in Discord. Future work: source-aware
/api/artist-detail could be extended to fall back through the whole
source priority chain when a specific source is given but returns no
discography. Not needed for the current flows.
2026-04-22 15:38:32 -07:00
Broque Thomas
1c345e4eb5 Route source-artist clicks to standalone /artist-detail page
Part B of the deferred unification cleanup. Now that Part A teaches
/api/artist-detail/<id> to fall back to a metadata-source lookup when
the library DB lookup misses, source-artist clicks can finally land
on the standalone page without 404ing — the goal Phase 4a aimed for
and had to roll back in commit 19e9174.

Re-migrating the seven callsites reverted earlier in this session:
  - search.js enhanced-search source-artist onClick
  - downloads.js _gsClickArtist (global widget non-library branch)
  - downloads.js _navigateToArtistFromModal fallback
  - discover.js viewRecommendedArtistDiscography
  - discover.js viewDiscoverHeroDiscography
  - discover.js 'Your Artists' card navAction inline onclick
  - discover.js 'Your Artists' info-modal 'View All' button
  - discover.js artist-map context menu
  - discover.js genre-deep-dive artist click
  - api-monitor.js watchlist discography view

Each replaces the navigateToPage('artists')+setTimeout+selectArtist-
ForDetail dance with a single navigateToArtistDetail(id, name,
source) call. The third arg seeds artistDetailPageState.currentArtist-
Source, which library.js now reads and forwards as ?source= to the
backend (added in Part A).

Effect: clicking an artist in any of these surfaces now lands on the
standalone /artist-detail page with a stable URL, source context
preserved, and owned-library data merged in when available. Library
artist clicks (unchanged) and media-player / stats links (unchanged)
all continue to use navigateToArtistDetail too, so they now
consistently share one destination.

The inline Artists page (#artists-page + selectArtistForDetail in
artists.js) still exists but has no external callers left — only the
page's own internal search-result click handler references the
function now. Parts D + E will delete the dead inline page and
finally remove artists.js.
2026-04-22 15:30:40 -07:00
Broque Thomas
89754480be Source-aware /api/artist-detail: fall back to metadata source when not in library
Part A of the deferred unification cleanup. The standalone artist-
detail endpoint used to 404 whenever `artist_id` wasn't a local library
primary key, which is exactly what source artists (Deezer/Spotify/
iTunes/etc.) have. That forced the Phase 4a revert: source artists had
to use the inline Artists page because this endpoint couldn't handle
them.

New behaviour:
  - Library PK path — unchanged. Existing callers see the same response.
  - `/api/artist-detail/<id>?source=<src>&name=<name>` with source in
    (spotify, itunes, deezer, discogs, hydrabase, musicbrainz) — when
    the library DB lookup misses, synthesize a response by:
      • fetching artist image via metadata_service.get_artist_image_url
        with source_override (the helper already backing /api/artist/
        <id>/image)
      • fetching discography via metadata_service.get_artist_detail_
        discography with MetadataLookupOptions(source_override=source,
        artist_source_ids={source: artist_id})
      • returning { success, artist: {id, name, image_url, server_source:
        null, genres: []}, discography, enrichment_coverage: {} }
  - Library PK missing AND no source — preserves the 404 (caller didn't
    give enough info to fall back).

Frontend plumbing: library.js loadArtistDetailData now appends
?source=<src>&name=<name> to the fetch URL when
artistDetailPageState.currentArtistSource is set. The field is already
seeded by navigateToArtistDetail's third arg (added during the earlier
unification work), so no new state plumbing is needed.

populateArtistDetailPage gracefully handles the missing-library-data
case per earlier exploration — owned_releases empty is fine,
enrichment_coverage optional, spotify_artist_id optional.

Part B will re-route the source-artist callsites (Search / Discover /
Watchlist / etc.) back through navigateToArtistDetail so they actually
exercise this new fallback path.
2026-04-22 15:28:28 -07:00
Broque Thomas
a5d97261e4 Extract shared helpers from artists.js to shared-helpers.js
Part C of the deferred unification cleanup. The Artists page is no
longer in the sidebar, but its JS file can't be deleted yet because
it houses ~20 general-purpose helpers that other modules depend on
(escapeHtml used in 229 places, service-status polling, image-colour
extraction, download-bubble infrastructure, discography completion
checking, enrichment card rendering).

Moved all non-page-specific code from artists.js into the new
webui/static/shared-helpers.js — pure copy/paste, zero logic change.
Two contiguous blocks extracted:

  Block A (lines 1097..1398 of original artists.js): discography
    completion suite — checkDiscographyCompletion, handleStreaming-
    CompletionUpdate, cacheCompletionData, updateAlbumCompletion-
    Overlay, getCompletionStatusText, setAlbumDownloadedStatus,
    setAlbumDownloadingStatus.

  Block B (lines 2206..EOF of original artists.js): download-bubble
    infrastructure (artist + search + Beatport clusters with their
    snapshot/hydrate/modal/monitor helpers), openDownloadMissingModal-
    ForArtistAlbum, image-colour extractor and dynamic-glow helper,
    escapeHtml, service-status polling, renderEnrichmentCards.

Function declarations in a plain <script> tag are auto-global, so all
existing callers continue to resolve without any import/export
changes. Load order in index.html: shared-helpers.js loads right
after core.js (which defines the artistDownloadBubbles / search-
DownloadBubbles / beatportDownloadBubbles globals these helpers use).

Stats:
  artists.js:       4638 → 1903 lines (-2735)
  shared-helpers.js: new, 2762 lines
  No function duplicated between the two files
  All 357 tests pass (3 new from split-integrity parametrization)

What's left in artists.js is purely the Artists page — search UI,
detail view, state switching, watchlist button, discography loading.
All of that is reachable only by typing /artists in the URL bar
since the sidebar entry was retired in Phase 4b. Parts D + E will
delete that remainder and the file itself.
2026-04-22 15:25:59 -07:00
Broque Thomas
3c7dc4de6e Artist detail back button falls back to Search, not Dashboard
When the user reached the inline Artists detail view from outside
the Artists page and no browser history is available (direct
bookmark), fall back to the Search page instead of Dashboard.
Search is the natural next step for finding a different artist.
2026-04-22 14:29:30 -07:00
Broque Thomas
78c14d3084 Hold version at 2.39 and fold unification changelog into one 2.40 entry
Reverts the 2.40→2.49 version spam from this session — every phase
commit was bumping the display version when the whole Search/Artists
unification project should really be a single release.

Changes:
  - _SOULSYNC_BASE_VERSION back to 2.39
  - All session-level version-info sections consolidated — the endpoint
    response is back to the pre-session 2.39 shape
  - helper.js WHATS_NEW entries for 2.40–2.49 collapsed into a single
    '2.40' block with one bullet per phase, marked unreleased
  - _getLatestWhatsNewVersion / _showOlderNotes filter out entries
    whose version is higher than the current build, so the 2.40 block
    won't fire the 'new' badge or appear in the What's New panel until
    we actually flip the build version
  - Picks up the artist-detail back-button fix from the previous turn
    (falls back to browser history when the user reached the inline
    detail from outside the Artists page)

When the unification project is done, a single commit that bumps
_SOULSYNC_BASE_VERSION to 2.40 will publish the whole folded entry.
2026-04-22 14:25:20 -07:00
Broque Thomas
19e9174866 Fix 404 on source-artist click — revert Phase 4a source migrations, bump to 2.48
Phase 4a (9361c29) mistakenly routed every artist click to
navigateToArtistDetail, which fetches /api/artist-detail/<id>. That
endpoint only knows how to look up local DB primary keys. For source
artists (Spotify/Deezer/iTunes/etc.) the id is a metadata-source id,
not a library PK — so clicks 404'd out.

Library artists (db_artists section in search results, library page
clicks, stats links, media player) continue to go to the standalone
/artist-detail page as before. Source artists now route back to the
Artists page's inline view via selectArtistForDetail, which calls
/api/artist/<id>/discography with a source param — the endpoint that
actually handles non-library IDs.

Reverted 7 migration points:
  - search.js: Enhanced Search source-artists onClick
  - downloads.js: global widget _gsClickArtist non-library branch
  - downloads.js: _navigateToArtistFromModal fallback
  - discover.js: viewRecommendedArtistDiscography
  - discover.js: viewDiscoverHeroDiscography
  - discover.js: 'Your Artists' card name-click inline HTML
  - discover.js: 'Your Artists' info-modal 'View All' button
  - discover.js: artist-map context menu
  - discover.js: genre-deep-dive artist click
  - api-monitor.js: watchlist artist discography view

Phase 4a's goal of "one artist page for everything" is deferred —
it needs backend work on /api/artist-detail to accept a source param
and fall back to metadata-source lookup when the local DB lookup
fails. Keeping the signature extension on navigateToArtistDetail
(source parameter) in place for when that lands.
2026-04-22 14:17:06 -07:00
Broque Thomas
d037643908 Clean up interactive help annotations for unified Search, bump to 2.47
Phase 4c of the Search/Artists unification — docs-only cleanup.

The click-for-help system and the 'Your First Download' guided tour
referenced elements that no longer exist (the Basic/Enhanced toggle,
the embedded download-manager toggle, the active/finished queue
panels). Updated annotations + tour steps to match the current UI.

  - New annotation for .search-source-picker-container (the dropdown)
  - Removed 6 annotations for deleted elements
  - 'first-download' tour now walks users through the source picker
    and uses page: 'search' (PAGE_TOUR_MAP accepts both 'search' and
    the legacy 'downloads' id so older bookmarks still match)
  - Retired the 'artists-browse' standalone tour — no sidebar entry
  - Dropped the dead #finished-queue detection in the setup milestone
    check (the dashboard stat card is the single source of truth)
2026-04-22 13:59:42 -07:00
Broque Thomas
09f15ce7d2 Retire Artists sidebar entry, redirect entry points to Search, bump to 2.46
Phase 4b of the Search/Artists unification. Cin flagged that 'Artists'
in the sidebar read like a library section but was actually a
dedicated artist-search page, duplicating what unified Search already
does. Removed the sidebar entry so users funnel through Search.

  - Sidebar Artists button gone
  - 'Browse Artists' on empty Watchlist now opens Search
  - 'View artist from Wishlist' opens Search pre-filled with the name
  - Profile Home Page + Page Access drop the Artists option

artists.js stays on disk: it defines ~30 shared helpers used across
the app (escapeHtml, openDownloadMissingModalForArtistAlbum, service
status, download bubbles, image helpers) that library/discover/etc.
depend on. Wholesale deletion would orphan too much. The inline
Artists page and its selectArtistForDetail flow are still there —
just unreachable from the sidebar — so /artists deep links keep
working for bookmarks.
2026-04-22 13:40:22 -07:00
Broque Thomas
9361c29965 Route all artist-detail callers to the standalone page, bump to 2.45
Phase 4a of the Search/Artists unification. The app had two artist-
detail implementations: the standalone page Library navigates to via
navigateToArtistDetail (its own route, deep-link support, highlights
Library in the sidebar), and an inline state inside the Artists page
reached via selectArtistForDetail. They rendered similar content but
were separate code paths and kept drifting apart (PR #356 just had
to fix source propagation in both).

Every external caller of selectArtistForDetail (9 sites across
api-monitor.js, discover.js, downloads.js, search.js) now calls
navigateToArtistDetail(id, name, source) directly. Removed ~63 lines
of the navigate-then-setTimeout-then-select dance. Source context
(Spotify/iTunes/Deezer/etc.) carries cleanly through via the new
third argument.

Artists sidebar entry, its inline search, and selectArtistForDetail
all still work — they just have no external callers. Phase 4b will
retire the sidebar entry and artists.js.
2026-04-22 13:36:36 -07:00
Broque Thomas
f203b3e46d Remove embedded Download Manager from Search page, bump to 2.44
Phase 3c of the Search/Artists unification. The Search page carried
a second copy of the Download Manager (active + finished queues,
clear/cancel-all buttons) that was hidden by default and duplicated
the dedicated Downloads page. That duplicate is now gone.

Removed:
  - Side-panel HTML block and the toggle button that showed/hid it
  - ~290 lines of polling + render infra in downloads.js: loadDownloads-
    Data, startDownloadPolling/stopDownloadPolling, updateDownload-
    Queues, renderQueue, updateTabCounts/updateDownloadStats,
    initializeDownloadTabs/switchDownloadTab, cancelDownloadItem,
    clearFinishedDownloads, cancelAllDownloads, and the
    activeDownloads/finishedDownloads globals
  - initializeDownloadManagerToggle and its call from init.js
  - Stopped hitting /api/downloads/status every second on the Search
    page (the dedicated Downloads page already polls its own view)

CSS grid for the Search page collapsed from '1fr 370px' to '1fr' now
that the right panel is gone. Unused .controls-panel__* / .download-
manager__* / .downloads-side-panel CSS rules kept in place — harmless,
can be pruned later.
2026-04-22 13:31:42 -07:00
Broque Thomas
6992e2e5b5 Rename Search page id from 'downloads' to 'search', bump to 2.43
Phase 3b of the Search/Artists unification. The Search page's
internal id was 'downloads', which clashed with the actual Downloads
page (id 'active-downloads') and confused anyone reading the code.
Renamed to 'search' across HTML, navigation, DOM selectors, and the
deep-link route list.

Backwards compat: navigateToPage('downloads') aliases to 'search'
at the top of the function; /downloads URL still serves index.html
and the client router resolves the page correctly; profile ACL
checks accept both 'search' and 'downloads' so existing profiles
with 'downloads' in allowed_pages keep working without migration.

Sidebar label unchanged. Zero visual change — pure internal tidy.
2026-04-22 13:22:49 -07:00
Broque Thomas
68d46c5aba Replace Enhanced/Basic toggle with source picker, bump to 2.42
Phase 3 of the Search/Artists unification. The Search page's two-mode
toggle is replaced by a single 'Search from' dropdown: All sources
(Auto), Spotify, Apple Music, Deezer, Discogs, Hydrabase, MusicBrainz,
or Soulseek (raw files). Auto keeps today's fan-out behavior for
backwards compatibility; picking a specific source hits only that
provider. 'Soulseek' routes to the raw-file basic section, so one
picker covers both old modes. Loading text and the enhanced fetch
now respect the selected source. Zero API changes — uses the source
param added in 2.40 and the shared fetch helper from 2.41.
2026-04-22 13:06:13 -07:00
Broque Thomas
377343326f Dedupe enhanced-search fetch in widget and page, bump to 2.41
Phase 2 of the Search/Artists unification: the Search page dropdown
and the global spotlight widget both POST to /api/enhanced-search
with identical boilerplate. Extracted into enhancedSearchFetch() in
search.js (loaded before downloads.js). Both callers migrated. Zero
UX change — purely sets up Phase 3 to wire a source picker in one
place instead of two.
2026-04-22 12:59:05 -07:00
Broque Thomas
952c35de39 Add source param to /api/enhanced-search, bump to 2.40
Phase 1 of the Search/Artists unification project: the endpoint now
accepts an optional `source` (spotify, itunes, deezer, discogs,
hydrabase, musicbrainz) so callers can target a single metadata source
instead of always fanning out. Omitted or `auto` preserves current
multi-source behavior — no existing callers break. Cache keys include
the source tag so per-source and fan-out results don't collide.
2026-04-22 12:49:49 -07:00
Antti Kettunen
5420c0de24
Fix album track source propagation
- Pass source through artist, library, wishlist, and rehydration album-track fetches
- Preserve the resolved metadata source on cached discography and artist detail state
- Prevent 404s when opening artist-page album modals from non-Spotify sources
2026-04-22 21:21:32 +03:00
BoulderBadgeDad
00f116ebee
Merge pull request #352 from JohnBaumb/refactor/split-script-js
Split monolithic script.js into 17 domain modules
2026-04-22 10:49:17 -07:00
Broque Thomas
8f85b0c251 Fix silent wrong-artist track downloads (Maduk/Tom Walker bug)
User reported searching "Maduk - Leave A Light On" on Tidal silently
downloaded Tom Walker's completely different song of the same name, then
embedded Maduk's metadata into Tom Walker's audio. Three layers of
defense all failed permissively. Two of them are fixed here; the third
(score formula weights) was left alone since these two together cover it.

Layer 1 fix — candidate artist gate (web_server.py:27782)
  Old: `if _best_artist < 0.4 and confidence < 0.85: continue`
  New: `if _best_artist < 0.5 and confidence < 0.85: continue`

  SequenceMatcher returns exactly 0.400 for "maduk" vs "tom walker"
  (5-char vs 10-char strings with coincidental char matches), which
  slipped past the strict `< 0.4` check. The word-boundary containment
  check earlier in the function already short-circuits legitimate
  formatting variations to sim=1.0, so falling to SequenceMatcher means
  strings are genuinely different. 0.5 closes the fencepost AND gives
  a small safety buffer.

Layer 3 fix — AcoustID verification (acoustid_verification.py:316)
  When title matches but artist doesn't AND expected artist isn't found
  anywhere in AcoustID's returned recordings:
    Old: always SKIP (let file through, assume cover/collab)
    New: FAIL if artist_sim < 0.3 (clear mismatch)
         SKIP if artist_sim >= 0.3 (ambiguous — cover/collab/formatting)

  The 0.3 cutoff catches hard mismatches like Maduk/Tom Walker (sim ~0.2)
  while preserving benefit-of-the-doubt for borderline artist formatting
  differences. Legitimate covers and collabs where the expected artist
  appears anywhere in AcoustID's recordings still PASS via the existing
  secondary-match loop above.

Both fixes are defense-in-depth — either alone would have caught this
bug. Together they close the pre-download AND post-download gaps.

All 292 tests pass. Version bumped to 2.39 with changelog entries.
2026-04-22 10:32:55 -07:00
JohnBaumb
a66c4d06e1 Split monolithic script.js (78K lines) into 17 domain modules
Extracts the single 77,957-line script.js into focused modules:

  core.js            (874)   - Global state, confirm dialog, websocket, constants
  init.js            (2358)  - Initialization, personal settings, navigation
  media-player.js    (2398)  - Media player, audio, visualizer, radio
  settings.js        (3657)  - Settings page, quality profiles, API keys, auth
  search.js          (1542)  - Search functionality, page data loading
  sync-spotify.js    (2538)  - Spotify sync, YouTube backend, hero section
  downloads.js       (6398)  - Wing It, batched polling, cancel, notifications
  wishlist-tools.js  (7234)  - Wishlist, matched downloads, tools, retag
  sync-services.js   (9076)  - Tidal, Deezer, Beatport, YouTube, ListenBrainz sync
  artists.js         (4610)  - Artists page, artist downloads
  api-monitor.js     (3798)  - API rate monitor gauges
  library.js         (6652)  - Library, artist detail, enhanced management
  beatport-ui.js     (3902)  - Beatport sliders, genre browser
  discover.js        (8920)  - Discover page and all sub-sections
  enrichment.js      (3551)  - All enrichment workers, library repair
  stats-automations.js (7575) - Stats, automations, issues, import
  pages-extra.js     (2874)  - Playlist explorer, server playlists, active downloads

Load order: core.js first (globals), init.js last (DOMContentLoaded).
All other modules define functions and load in any order.
No functional changes - pure extraction along existing section boundaries.
2026-04-21 23:52:30 -07:00
BoulderBadgeDad
47ced912b9
Merge pull request #349 from kettui/fix/lazy-load-beatport-only-when-needed
Load Beatport content only when needed
2026-04-21 22:59:44 -07:00
Broque Thomas
78fa83c8ac Add $cdnum template variable for multi-disc filenames
New smart template variable that emits "CD01" / "CD02" etc. in filenames
on multi-disc albums, and expands to empty string on single-disc albums
so mixed libraries don't end up with "CD01" on every single.

Template behaviour:
- total_discs > 1 -> "CD{disc:02d}" (zero-padded, CD prefix)
- total_discs <= 1 -> empty string
- Both $cdnum and ${cdnum} bracket form supported
- Empty value collapses cleanly via existing double-dash regex plus new
  leading-dash cleanup pass

Wiring:
- _apply_path_template in web_server.py (download pipeline)
- _apply_path_template in core/repair_jobs/library_reorganize.py
  (Reorganize repair job)
- total_discs added to every album-mode template context:
  * download pipeline album branch (uses resolved total_discs even for
    single-track downloads from search)
  * per-album Reorganize preview + apply endpoints (pre-scan all track
    tags once, take max disc_number)
  * Library Reorganize repair job (already had album_total_discs map,
    just added to context dict)

Leading-dash cleanup added to _get_file_path_from_template (web_server)
and _build_path_from_template (library_reorganize) so templates like
"$cdnum - $track - $title" don't leave "- 05 - Title" on single-disc
albums.

UI:
- Template hint in Settings -> File Organization documents $cdnum
- Template validation variable list includes $cdnum
- Reorganize modal variable reference shows $cdnum with example "CD01"

Verified:
- Multi-disc disc 1 -> "CD01 - 05 - Track"
- Multi-disc disc 2 -> "CD02 - 05 - Track"
- Single-disc      -> "05 - Track" (no leading dash)
- Templates without $cdnum behave unchanged
- 276/276 tests pass
2026-04-21 22:55:37 -07:00
Broque Thomas
b9a7ed97be Add per-row cancel + Cancel All to downloads page, fix streaming cancel
Three closely-related changes bundled together. The UI work exposed the
backend bug when I tried to cancel a Deezer download and saw it marked
cancelled in the DB but continuing in the background.

Backend — cancel_task_v2 orchestrator dispatch fix:
  The slskd-specific cancel block was written back when soulseek_client
  was a raw SoulseekClient. It was later swapped to DownloadOrchestrator
  (which doesn't expose .base_url / ._make_request), so the first
  diagnostic log line crashed with AttributeError. The outer try/except
  swallowed it, leaving streaming downloads (YouTube / Tidal / Qobuz /
  HiFi / Deezer / Lidarr) running in the background after the user
  clicked cancel.

  Replaced the ~80-line block with a single
  soulseek_client.cancel_download(download_id, username, remove=True)
  call — the orchestrator's dispatch picks the right client by username,
  same path /api/downloads/cancel already uses successfully.

Per-row cancel button (fancy):
  Circular X button on .adl-row for rows in active or queued state.
  Hidden by default (opacity 0, translateX + scale), fades in + settles
  on .adl-row:hover with a cubic-bezier overshoot. Own :hover gives a
  1.12x scale pop and brighter red glow. Touch devices (@media
  (hover: none)) keep it visible.

  Backend: surfaced playlist_id in /api/downloads/all items so the
  frontend can hit cancel_task_v2 without a second lookup. Frontend:
  adlCancelRow(btnEl, playlistId, trackIndex) with double-click guard
  via data-cancelling + adl-row-cancel-pending class.

Cancel All header button:
  Red-themed button next to "Clear Completed". Only visible when any
  task is in downloading / searching / post_processing / queued state —
  auto-hides the moment the last one finishes. Confirm dialog shows
  "Cancel N tasks across M batches?". Iterates _adlBatches, calls
  /api/playlists/<batch_id>/cancel_batch sequentially (same endpoint
  each modal's "Cancel All" and the per-batch-card cancel use). Disables
  during the loop, mixed/success/error toast based on result.

All 276 tests pass.
2026-04-21 22:35:30 -07:00
Antti Kettunen
cc8875f30b
Pause Beatport work on tab exit
- Abort Beatport content loading when leaving the Sync/Beatport tab
- Stop Beatport slider autoplay plus discovery and sync subscriptions on exit
- Make Beatport bubble hydration cancellable so hidden tabs do not keep fetching
2026-04-22 08:12:35 +03:00
Antti Kettunen
8dbc819765
Lazy-load Beatport tab data
- Defer Beatport scraper fetches until Beatport tab opens
- Skip Beatport bubble hydration during app bootstrap
- Keep rebuild sliders and top lists behind first visit
2026-04-22 07:57:05 +03:00
Broque Thomas
03b7230ac2 Preserve cover art in discovery fix-modal cache matched_data
Companion fix to the provider-hardcode bug (6ceedc8). The cache
matched_data built by the 5 update_match / fix endpoints was dropping
image_url and album.images when album came back as a bare string —
common for Deezer and iTunes search results. Cache hits on re-discovery
then produced downloads with no artwork.

Each save site now carries image info through:
- album_obj gets image_url + images:[{url}] populated from spotify_track.image_url
- matched_data adds top-level image_url for pipeline consumers that check there
- Works for both dict-shaped album (Spotify) and string-shaped album (Deezer/iTunes)

Mirrors the handling already present in _build_fix_modal_spotify_data for
the in-memory result['spotify_data'] — explains why the UI showed art fine
during the fix modal but the cached entry lost it after restart.

save_discovery_cache_match uses INSERT OR REPLACE, so existing bad cache
entries refresh when the user re-fixes the track. No manual cache clearing
needed.

Added to 2.38 changelog (same round of discovery-fix work).
2026-04-21 21:26:36 -07:00
Broque Thomas
6ceedc8fd4 Fix manual discovery fixes lost after restart for non-Spotify users
Five update_match endpoints hardcoded the provider as 'spotify' when
saving manual fixes to the discovery cache, but the re-discovery worker
queries the cache with _get_active_discovery_source() — the user's
actual primary. If the primary was Deezer/iTunes/Discogs/Hydrabase, the
provider column never matched, so every manual fix looked like it
vanished on restart.

Replaced 'spotify' with _get_active_discovery_source() at all 5 sites:
- Tidal update_match (web_server.py:34569)
- Deezer update_match (web_server.py:36235)
- Spotify Public update_match (web_server.py:37084)
- YouTube update_match (web_server.py:38037)
- Discovery Pool fix (web_server.py:49787)

Now symmetric with how the auto-discovery workers already save. Spotify-
primary users see no change (the hardcoded value matched their source).

Version bumped to 2.38 with changelog + version-info entries.
2026-04-21 21:12:22 -07:00
Broque Thomas
d0ee9c73b3 Reveal download cancel button on hover instead of always showing
Cancel button on active download items was always visible, cluttering
the card. Now hidden by default and fades in when you hover the card
(or focus anything inside it, for keyboard a11y).

- opacity + pointer-events approach so layout doesn't jump on reveal
- 4px slide-in on reveal for a subtle entrance
- Touch devices (hover: none) keep the button always visible — no hover
  means no way to discover it otherwise
2026-04-21 21:12:06 -07:00
Broque Thomas
0af98cdded Merge main into dev (brings in PR #342 Plex PIN OAuth) 2026-04-21 20:22:08 -07:00
Broque Thomas
e5d4d61c0e Fix watchlist content filters: live false positives + auto-scan bypass
Two bugs reported in issue #320:

1. Auto-watchlist scan bypassed Global Override settings.
   scan_watchlist_profile applied _apply_global_watchlist_overrides, but
   the scheduled auto-scan called scan_watchlist_artists directly —
   bypassing the override. Users who unchecked "Albums" or "Live" under
   Watchlist → Global Override still saw full albums and live tracks
   added during nightly scans (per-artist defaults, which include
   everything, won).

   Moved override application into scan_watchlist_artists itself so
   every entry point respects it. scan_watchlist_profile now forwards
   the apply_global_overrides flag through to avoid double-application.

2. is_live_version (watchlist + discography backfill) and
   live_commentary_cleaner's content patterns used bare \blive\b, which
   matched verb uses like "What We Live For" by American Authors,
   "Live Forever" by Oasis, "Live and Let Die" by Wings.

   Tightened the live patterns to require clear recording context:
   (Live) / [Live Version] / - Live / Live at|from|in|on|version|
   session|recording|performance|album|show|tour|concert|edit|cut|take
   / In Concert / On Stage / Unplugged / Concert.

   Locked in 11 regression tests covering the reported false positives
   (What We Live For, Live Forever, Living on a Prayer, Live and Let Die)
   and the reported true positives (Dimension - Live at Big Day Out,
   MTV Unplugged, etc.).

Version bumped to 2.37 with changelog entries.
2026-04-21 19:16:25 -07:00
Broque Thomas
178719e443 Fix metadata cache health bar duplicating on findings dashboard
_loadCacheHealthStats ran async from loadRepairFindingsDashboard and
appended a new .repair-cache-health div each time. If the dashboard
refreshed while earlier fetches were still in flight, each resolving
fetch appended its own section — producing 2–6 stacked copies.

Now each fetch removes any existing .repair-cache-health inside the
dashboard before appending, so at most one bar is ever visible.
2026-04-21 18:47:15 -07:00
Broque Thomas
457763cbab Rebuild Discography Backfill: auto-wishlist, Fix All, section UI
Root-cause fix for "scanning 50 artists" then silence: when the master
repair worker was paused, force-run still kicked off _run_job but the
job's first wait_if_paused() blocked forever because is_paused was tied
to the master-enabled state. Force-run now bypasses master-pause —
scheduled runs still respect it.

Also fixes Fix All on discography findings doing nothing: the backend
bulk_fix_findings query had a fixable_types allowlist that excluded
missing_discography_track (and acoustid_mismatch). Added both.

Backfill job rebuild:
- auto_add_to_wishlist opt-in setting — creates findings AND pushes to
  wishlist during the scan
- 3-option fix dialog (Add to Wishlist / Just Clear / Cancel) on single
  Fix, Bulk Fix selection, and Fix All (page-level)
- Fix All "Just Clear" path uses the clear endpoint with job_id filter
  instead of the generic "may delete files" bulk-fix warning
- Batched in-memory matching using get_candidate_albums_for_artist +
  get_candidate_tracks_for_albums (same fast path the Library pages use)
- Rich album context per finding (id, name, album_type, release_date,
  images, artists, total_tracks) — flows through the wishlist pipeline
  so auto-processor classifies each track into the right cycle
  (albums vs singles) and post-processing gets correct folder/tags/art
- Per-artist progress logs [N/50] Scanning ArtistName
- Default interval 24h (was 168h); all release types default on; settings
  reordered with _section_* group headers (Core / Release Types /
  Content Filters)

Repair settings UI:
- Generic _section_<name> key convention renders as an uppercase group
  divider in the settings panel — any job can opt in
- .repair-setting-row gets a dashed bottom border so label↔toggle pairing
  is visually clear
- _prettifyRepairSettingKey fixes acronym capitalization (EPs, not Eps)

Version bumped to 2.36 with changelog entries.
2026-04-21 18:44:43 -07:00
Broque Thomas
75d0dc3d4f Fix manual discovery match producing download cards with no cover art
User reported: after clicking Fix on a Not-Found discovery track and
picking a replacement in the fix modal, the resulting download card had
no cover image, and the track seemed to still behave like a Wing-It
stub. Both suspicions were correct. Three compounding bugs:

1. /api/spotify/search_tracks returned only id/name/artists/album/
   duration_ms — no image_url — unlike the sibling /api/itunes/ and
   /api/deezer/ endpoints which include image_url. The fix modal had
   no image data to work with when users searched via Spotify.

2. Frontend selectDiscoveryFixTrack discarded any image info it did get
   and posted the same minimal shape to the backend.

3. All 7 backend discovery/update_match endpoints built
   result['spotify_data'] with 'album' as a bare string (track.album
   which is just the album name). The download pipeline expects
   spotify_album to be a dict with image_url or images[].url — a string
   yields blank cover art. Normal discovery workers already build album
   as a rich dict; the fix-modal path was the anomaly.

4. Bonus: result['wing_it_fallback'] was never cleared on manual match.
   Tracks fixed after the auto Wing-It fallback kept the flag set, so
   downstream code checking it treated them as wing-it even though the
   user picked real metadata.

Changes:

- New helper _build_fix_modal_spotify_data(spotify_track) in web_server.py
  near _build_discovery_wing_it_stub. Handles both string and dict album
  inputs, normalises to a dict with image_url and images populated when
  the payload carries one. Matches the shape produced by normal discovery
  so downstream code is happy on both paths.

- /api/spotify/search_tracks now returns image_url (parity with iTunes
  and Deezer endpoints).

- All 7 discovery/update_match endpoints (youtube, tidal, deezer,
  spotify-public, listenbrainz, beatport — 6 via the identical pattern
  plus the listenbrainz variant with its None branch) now:
    * use the helper to build spotify_data (album as dict + top-level
      image_url)
    * explicitly set result['wing_it_fallback'] = False

- selectDiscoveryFixTrack forwards track.image_url in the POST body and
  mirrors the helper's output in the local state update so the UI
  reflects the same shape immediately.

Audit: every downstream reader of spotify_data['album'] is already
dict-or-string tolerant (isinstance checks at lines 2073, 2158, 25844,
28938, 29011, etc.) so promoting album from string to dict is safe.
Normal discovery already sets it as a dict, so we're moving the fix-
modal path to match the existing majority case.

Full suite stays at 263 passed. Ruff clean.
2026-04-21 17:12:28 -07:00
Broque Thomas
fadbb286b7 Fix [object Object] in discovery live-update paths (bulletproof pass)
The previous commit only fixed the INITIAL-render transform for
Spotify/Tidal/Deezer discovery rows. User confirmed [object Object]
still appeared after discovery completed — because there are two
additional update paths that do their own row-transform:

- WebSocket live-update handler (populates rows as discovery progresses)
- Poll-based fallback (same shape, runs when socket is disconnected)

Both had the same naive `.artists.join(', ')` on potentially-object
arrays. The poll and socket handlers exist for each of Spotify Public,
Tidal, and Deezer — six occurrences total across three platforms, all
with the same bug class. Now all use the object-aware map-and-join
pattern consistent with the initial-render fix.

Also fixes two more spots in openDiscoveryFixModal that the earlier
sweep missed:
- Missing spotify_public branch in the apply-selected-match handler:
  after user picks a replacement track, state lookup failed, the local
  row wouldn't refresh even though the backend had succeeded.
- Same artist-join bug in the same handler (track.artists from the
  fix-modal search results could be array-of-objects).

Full suite stays at 263 passed. Ruff clean.
2026-04-21 16:14:36 -07:00
Broque Thomas
891b18540b Fix Spotify Playlist Discovery: Fix button, header readability, [object Object]
Three separate issues reported on the Spotify Playlist Discovery modal:

1. Fix button fails with "Track data not found"
   openDiscoveryFixModal() branched on platform name to locate the discovery
   state but had no case for 'spotify_public'. Row rendering passes that
   platform value when the source is a Spotify public playlist, so state
   lookup failed and the toast fired. Added the spotify_public branch —
   state lives in youtubePlaylistStates alongside the other reused platforms.

2. Table header too transparent to read
   .youtube-discovery-modal .discovery-table th used rgba(255,255,255,0.1)
   as background (10% white) with white text, which lost contrast when the
   orange progress bar or varied row content scrolled underneath. Switched
   to near-solid dark rgba(17,17,20,0.96) with a brighter border-bottom
   and z-index:5 so the sticky header stacks cleanly above table content.

3. "[object Object]" in the matched-artist column for Wing-It tracks
   The Spotify Public Playlist row-transform joined result.spotify_data.artists
   directly with .join(', '). Wing-It stub metadata (built server-side by
   _build_discovery_wing_it_stub) returns artists as [{name: "..."}] —
   array of objects. .join() stringified each object to "[object Object]".
   Same pattern existed in three places in script.js; all now map objects
   to .name and filter empties before joining. Graceful fallback to "-"
   if the result is empty.

No existing tests touched. Full suite stays at 263 passed. Ruff clean.
2026-04-21 15:38:50 -07:00
elmerohueso
f8dd846fea
Merge branch 'main' into plex-pin-auth 2026-04-21 16:05:40 -06:00
Broque Thomas
0b4647ddd4 Add per-service config status indicators to Settings Connections tab
Adds green/yellow header gradient on each service card showing whether the
user has filled in credentials, plus an expand-triggered verification layer
that surfaces working-or-not status inline.

Backend (web_server.py):
- SERVICE_CONFIG_REGISTRY mapping each of the 11 services in Connections to
  its config requirements. Supports required-keys, always-green, any-of,
  and custom-check semantics (Tidal uses token-file check, Qobuz accepts
  either email/password OR cached auth token).
- _is_service_configured(service) — cheap config presence check, no APIs hit.
- GET /api/settings/config-status — returns {service: {configured}} for all
  services in one call. Drives the page-load gradient.
- POST /api/settings/verify — takes {services: [...]}, runs
  run_service_test per service, caches results 5 min in-memory, parallelizes
  with ThreadPoolExecutor(max_workers=3) to avoid self-rate-limiting. Query
  param ?force=true busts cache.
- Added verify branches for iTunes, Deezer, Discogs, Qobuz, Hydrabase in
  run_service_test (previously missing — these services couldn't be tested).

HTML (webui/index.html):
- data-service="..." on all 11 .stg-service containers so JS can map card
  to backend service name.

CSS (webui/static/style.css):
- .status-configured gradient (subtle green, left-to-transparent fade)
- .status-missing gradient (yellow, same shape)
- Spinner badge in header for .status-checking state
- "Testing connection…" status line style inside panel body
- Red warning bar style for verify failures at top of expanded panel
- Brand dot now glows always (was only glowing when expanded); hover and
  expand states intensify the glow progressively.

JS (webui/static/script.js):
- applyServiceStatusGradients() fetches config-status and applies
  green/yellow class per card. Called on Connections tab activate + after
  any settings save.
- _stgVerifyServices(services, {force}) — batch verify POST, tracks
  in-flight state, renders spinners/status lines/warnings per service.
- toggleStgService() fires single-service verify when a card is expanded
  (not on collapse). Skipped if a verify is already in flight for that
  service.
- toggleAllServiceAccordions() fires one batched verify for all 11 services
  when "Expand All" is clicked; skipped on "Collapse All".
- _stgRefreshAfterSave() — after settings save, refreshes gradient (cheap)
  and re-verifies only the cards the user currently has expanded (so
  freshly-edited credentials show their new verify result immediately,
  without re-pinging every service).

Failure UI: top-of-panel red warning bar with the error message (e.g.
"Discogs token rejected (HTTP 401)", "Hydrabase not connected…"). Removed
automatically on next successful verify.

No existing tests changed. Full suite stays at 263 passed. Ruff clean.
2026-04-21 14:24:41 -07:00
Antti Kettunen
6f79214439
Route artist image lookup through metadata service
- Move /api/artist/<artist_id>/image resolution into core.metadata_service.
- Resolve artist artwork through source priority, with explicit source/plugin overrides preserved.
- Keep Spotify call tracking inside the client layer to avoid double counting.
- Update similar-artist lazy loading to pass source context and add service coverage.
2026-04-21 21:14:35 +03:00
Broque Thomas
cf143f70af Batch discography completion matching against pre-fetched candidates
Artist detail pages ran check_album_exists_with_editions and check_track_exists
per discography item, each firing 5+ title variations times 3 artist variations
of fuzzy LIKE searches plus fallback broad-artist queries. For a 30-album artist
that was ~450 SQL round-trips just to answer "which of these do I own."

Hoist the artist's library albums and tracks into memory once per request via
two new helpers — get_candidate_albums_for_artist and get_candidate_tracks_for_albums —
and thread them through as optional candidate_albums / candidate_tracks params on
check_album_exists_with_editions, check_album_exists_with_completeness,
check_track_exists, check_album_completion, and check_single_completion.

Batched path scores the same _calculate_album_confidence / _calculate_track_confidence
against the in-memory list, preserving Smart Edition Matching and accuracy.
Title-only cross-artist fallback still fires for collaborative-album edge cases.
None on either param preserves legacy per-item SQL behavior for unaffected callers.

Applied to both /api/library/completion-stream (library artist detail page) and
iter_artist_discography_completion_events (Artists search page). Timing logs
added to confirm the pre-fetch cost and loop elapsed time.

On a Kendrick page load, per-album resolution drops from ~8 seconds to under
the 50ms streaming sleep floor. Observed ~100x SQL reduction on the happy path.
2026-04-21 10:47:47 -07:00
Broque Thomas
739d2f67c1 Fix Reorganize All sending every album to Albums folder
The reorganize endpoints built a template context without albumtype,
so ${albumtype} silently fell through to the engine's hardcoded "Album"
default — EPs, singles, and compilations all landed in Albums/.

Wires albumtype through from the albums table's record_type column
(populated by Spotify/Deezer/iTunes enrichment workers) with track-count
fallback when record_type is missing. New helper mirrors the download
pipeline's classification logic so reorganize produces the same folders
as initial placement. Also handles Deezer's raw 'compile' value which
the Deezer worker writes directly without mapping.
2026-04-21 08:59:16 -07:00
Broque Thomas
14359a1f98 Add confirmation dialog to Reorganize All and fix button style
Shows standard SoulSync confirm dialog before bulk reorganize with
album count and template preview. Button now uses enhanced-sync-btn
class to match other artist header buttons.
2026-04-20 23:51:17 -07:00
Broque Thomas
95cf1eeeea Add Reorganize All Albums button, version bump to 2.35, changelogs
New "Reorganize All" button in enhanced library artist header processes
all albums sequentially using the configured path template.

Version bumped to 2.35. Updated What's New modal with major features
(Discography Backfill, Multi-Artist Tagging, Enriched Downloads,
Template Delimiters, Reorganize All). Updated helper.js changelog
with all April 20 fixes and features.
2026-04-20 23:46:37 -07:00
Broque Thomas
e39a3f2af7 Add multi-artist tagging options: separator, multi-value tags, feat-in-title
Three new settings in Paths & Organization:
- Artist Tag Separator: choose comma, semicolon, or slash between artists
- Write multi-value ARTISTS tag: each artist as separate tag value for
  Navidrome/Jellyfin multi-artist linking (FLAC ARTISTS key, ID3 TPE1
  multi-value, MP4 multi-entry)
- Move featured artists to title: keep only primary artist in ARTIST
  tag, append others as (feat. ...) in track title

All opt-in with defaults matching current behavior. Raw artist list
stored on metadata dict for tag writers to access without re-parsing.
2026-04-20 23:18:10 -07:00
Broque Thomas
9c15d00128 Enrich downloads page cards with artwork, artist, album, and source badges
The downloads page previously showed only title and source per download.
Now shows album artwork thumbnail, artist name, album name, source badge,
and quality badge (after post-processing). All metadata comes from the
existing matched_downloads_context — no extra API calls needed.

Falls back gracefully to title-only display when context metadata is
not available (e.g. orphaned Soulseek transfers with no task mapping).
2026-04-20 22:18:20 -07:00
Broque Thomas
ed97fecc31 Add Discography Backfill maintenance job
New repair job that scans each artist in the library, fetches their
full discography from metadata sources, and creates findings for any
tracks not already owned. Users review findings and click "Add to
Wishlist" to queue missing tracks for download.

Respects content filters (live/remix/acoustic/instrumental/compilation)
and release type filters (album/EP/single). Opt-in, disabled by default,
runs weekly, processes up to 50 artists per run with rate limiting.
2026-04-20 22:04:38 -07:00
Broque Thomas
dad915dc2c Add ${var} hint text to single, playlist, and video template hints 2026-04-20 21:07:16 -07:00
elmerohueso
e3dd5727d8 Cancel button when manually configuring plex 2026-04-20 22:00:18 -06:00
elmerohueso
3e90de7a47 clean up buttons for manually configuring plex 2026-04-20 21:57:21 -06:00
elmerohueso
d36f18f5d7 pin auth should disable fields and show library 2026-04-20 21:34:29 -06:00
elmerohueso
6a27e7930c plex oauth via pin/code 2026-04-20 21:22:44 -06:00
elmerohueso
6344f250fc buttons to configure plex or view the current configuration 2026-04-20 20:54:10 -06:00
Broque Thomas
93e036848b Add ${var} delimiter syntax for path templates
Users can now append literal text to template variables using curly
braces: ${albumtype}s produces "Albums", "Singles", "EPs". Without
braces, $albumtypes was rejected as an unknown variable by validation.

Both syntaxes work: $albumtype (plain) and ${albumtype} (delimited).
Bracket vars are resolved first to prevent partial matching conflicts.
Validation updated for album, single, and playlist templates.
2026-04-20 18:03:11 -07:00
Broque Thomas
e1819186e4 Add fix action prompt for AcoustID mismatch findings
AcoustID findings had no Fix button and bulk "Fix Selected" silently
defaulted to retag with no user choice. Now shows a 3-option prompt
(Retag / Re-download / Delete) for both individual and bulk fix,
matching the pattern used by orphan files and dead files.
2026-04-20 16:22:26 -07:00
Antti Kettunen
24abae6908
Refactor album track lookup to use source priority
- Move album-track resolution into metadata_service
- Use the configured provider order instead of Spotify-first branching
- Switch the frontend to the unified /api/album/<id>/tracks endpoint
- Add tests for source-priority lookup, DB resolution, and formatting
2026-04-20 21:29:46 +03:00
Broque Thomas
71bff55c6a Fix iTunes album tracks failing on region-restricted releases
iTunes API can return collection metadata without song tracks for
region-restricted albums. The _lookup fallback only checked if results
was empty, so a collection-only response was accepted and cached as
{'items': []}. All future lookups returned the cached empty result.

Three fixes:
- get_album_tracks now checks for actual song items and tries fallback
  storefronts when only collection metadata is returned
- Skip cached results with empty items array (prevents stale cache hits)
- Backend returns descriptive 404 error, frontend surfaces it in toast
2026-04-20 11:10:54 -07:00
Broque Thomas
f5441c7992 Fix sync buttons showing on undiscovered LB/Last.fm playlists
The standalone mode handler ran every 10s on WebSocket status push and
set display='' on all sync buttons matching [id$="-sync-btn"], which
removed the display:none that kept undiscovered playlist sync buttons
hidden. Now tracks which buttons it hid via a data attribute and only
restores those, preserving the hidden state on undiscovered playlists.
2026-04-20 10:42:39 -07:00
Broque Thomas
21ae328955 Fix track ownership false positives, wing-it wishlist leak, debug counts
Track ownership: check-tracks endpoint now filters by album context
when provided, preventing false "Found" when a track exists in a
different album by the same artist (e.g. Thriller on HIStory).

Wing-it wishlist: manual "Add to Wishlist" button now skips wing-it
fallback tracks (wing_it_ ID prefix), matching the behavior of
failed download and failed sync paths.

Debug info: watchlist/wishlist/automation counts were always 0
because get_db() doesn't exist — fixed to get_database().
2026-04-20 10:17:47 -07:00
Broque Thomas
2f2e5ddbd0 Fix album track lookup hardcoded to Spotify on Artists page
The /api/artist/{id}/album/{id}/tracks endpoint was hardcoded to use
spotify_client and returned 401 if Spotify wasn't authenticated,
even when the user's primary source was Deezer or iTunes. Now uses
the configured primary metadata source via _get_metadata_fallback_client
with Spotify as fallback. Also gives a clearer error message when
no metadata source is available at all.
2026-04-20 09:15:09 -07:00
Broque Thomas
908924d22c Fix wishlist splitting multi-artist albums by track artist
Adding a soundtrack or compilation album to wishlist was creating
separate wishlist entries per track artist (e.g. Persona 3 OST split
into ATLUS Sound Team/Lotus Juice/Azumi Takahashi). Now uses the
album-level artist when available so all tracks stay grouped as one
album. Per-track artist resolution only applies to playlists where
there's no album context.
2026-04-20 08:18:35 -07:00
Broque Thomas
c8fcb4626a Fix artist search case sensitivity with metadata APIs
Some metadata APIs return fewer or no results for all-lowercase
queries. Title-case the query when it's all lowercase before
sending to the API ("foreigner" → "Foreigner"). Mixed-case input
is left as-is. Confidence scoring still uses the original query.
2026-04-20 08:01:55 -07:00
Broque Thomas
6ca3f3b070 Enable Lidarr as production-ready download source
Fixed 5 critical gaps in the download orchestrator where lidarr was
missing from client loops: get_all_downloads, get_download_status,
cancel_download fallback, clear_all_completed_downloads, and
cancel_all_downloads. Without these, lidarr downloads were invisible
to the UI, couldn't be cancelled, and accumulated in memory.

Also: error messages now visible in download list (appended to
filename on error state), removed "(Development)" label from UI.
2026-04-20 07:24:04 -07:00
Antti Kettunen
9fc757ce49
Fix library artist details failing to fetch correct album information
- Stop passing in spotify_id as the id in the UI, use the actual db id instead
  - Fixes an issue where albums for another artist would end up being returned for the actual searched artist
- Remove the redundant artist_id filtering code
  - Fixes an issue where not-currently-owned albums would be filtered out from the results, even if they were successfully fetched from the configured metadata provider
2026-04-20 08:14:28 +03:00
Broque Thomas
f8e24b8432 Regenerate M3U with real library paths after post-processing
M3U files were generated when the download batch completed but
before post-processing finished tagging and moving files. Paths
pointed to download locations instead of final library paths,
making every track show as missing. Now regenerates the M3U from
the backend batch completion handler after all post-processing
is guaranteed done, resolving real file paths from the library DB.
Skips overwrite if zero tracks resolve to avoid replacing a
partially-good M3U with an all-missing one.
2026-04-19 20:00:19 -07:00
Broque Thomas
036f284ee3 Fix AcoustID retag action not writing tags to audio file
The retag fix for AcoustID mismatches was only updating the DB
record (title, artist_id) without writing corrected tags to the
actual audio file. Users would click Fix, the finding disappeared,
but the file on disk stayed unchanged. Now writes title and artist
tags to the file via Mutagen after the DB update.

Also fixed artist INSERT missing server_source when creating a new
artist during retag — now uses the active media server value.
2026-04-19 19:14:49 -07:00
Broque Thomas
ef8cff3c69 Version bump to 2.34 2026-04-19 18:40:58 -07:00
Broque Thomas
35d6fff916 Fix wishlist albums cycle forced to 1 concurrent worker
Auto-wishlist albums cycle was passing is_album=True to
_get_batch_max_concurrent which returns 1 for soulseek mode.
This restriction is for folder-based album grabs from a single
peer, not individual track downloads. Wishlist always does
single-track downloads regardless of cycle, so it should use
the user's configured concurrency setting.
2026-04-19 17:29:46 -07:00
Broque Thomas
5fb1972361 Fix downloads nav badge dropping to 300 after page open
_adlFetch() fetches /api/downloads/all?limit=300 then _adlUpdateBadge()
was counting active statuses from that truncated array, overwriting
the real server-side count maintained by WebSocket. Removed the
badge update from _adlFetch — the WebSocket status push already
keeps it accurate.
2026-04-19 17:28:08 -07:00
Broque Thomas
dd3b71bc9a Fix discovery search quality and server playlist track positioning
Fix modal results: sort standard album versions above live, remix,
cover, soundtrack, remaster, and deluxe variants so users see the
original studio track first instead of obscure versions.

Plex Find & Add: tracks were always appended to the end of the
playlist because addItems ignores position. Now moves the track
to the correct slot after adding via moveItem.
2026-04-19 17:26:29 -07:00
Broque Thomas
5b9c9bc6fa Sort Fix modal results to prioritize standard album versions
Discovery Fix modal search results now sort standard album versions
above live recordings, remixes, covers, soundtracks, remasters,
deluxe editions, and other variants. Fixes cases where searching
"Mother Danzig" returned a live version first, or "Even Flow Pearl
Jam" returned a soundtrack instead of the original from Ten.
2026-04-19 17:21:02 -07:00
Broque Thomas
a3c8b9ecdd Add unmatch button, video naming template, and fix slskd log spam
Unmatch: found tracks in playlist discovery now have a red X button
to remove bad matches. Clears match data, sets back to Not Found,
persists in DB for mirrored playlists, and respects user choice on
re-discovery runs (won't re-match automatically).

Video naming: new path template in Settings with $artist, $title,
$artistletter, $year variables. Default unchanged ($artist/$title-video)
so existing Plex setups aren't affected.

slskd logs: Clean Search History automation skips when Soulseek is
not the active download source, eliminating connection error spam.
2026-04-19 17:16:22 -07:00
Broque Thomas
122a6999b3 Add customizable music video naming and fix slskd log spam
Video naming: new path template in Settings → Paths & Organization
with $artist, $artistletter, $title, $year variables. Default
unchanged ($artist/$title-video → Artist/Title-video.mp4) so
existing Plex setups aren't affected. Users can remove the -video
suffix or reorganize however they like.

slskd logs: the Clean Search History automation now skips when
Soulseek is not the active download source, eliminating noisy
connection error logs for users who don't use Soulseek.
2026-04-19 16:57:36 -07:00
Broque Thomas
a23bce5966 Auto Wing It fallback for failed playlist discovery tracks
When playlist discovery fails to match a track on any metadata API,
instead of marking it "Not Found" and excluding it from downloads,
automatically build stub metadata from the raw source title/artist
and include it in the download queue. Soulseek searches with the
raw data, post-processing enhances whatever it can find.

All 7 discovery workers updated: YouTube, ListenBrainz, Tidal,
Deezer, Spotify Public, Beatport, and automated mirrored playlists.
Amber "Wing It" badge distinguishes stubs from real API matches.
Fix button still available so users can manually find a proper match.
Wing It stubs persist in DB for mirrored playlists and are
re-attempted on future discovery runs. Failed wing-it downloads
skip wishlist per-track (checked by wing_it_ ID prefix) so real
matched failures in the same batch still go to wishlist normally.
2026-04-19 16:05:50 -07:00
BoulderBadgeDad
e0f036df08
Merge pull request #328 from JohnBaumb/feature/deep-linking
Add URL-based deep linking for SPA navigation
2026-04-19 13:07:46 -07:00
BoulderBadgeDad
12087f2407
Merge pull request #327 from kettui/fix/artist-details-refactoring
Refactor artist-detail discography flow
2026-04-19 12:53:35 -07:00
Broque Thomas
f9de081bd5 Fix library page crash when All letter filter is used
soul_id.startsWith() threw TypeError for non-string values, crashing
the entire card rendering pipeline. Letter-specific filters worked
because the problematic artist wasn't in those filtered results.
Added String() wrapper on all 3 soul_id.startsWith calls and a
try-catch around individual card rendering so one bad card can't
take down the whole page.
2026-04-19 12:48:53 -07:00
JohnBaumb
7d311451cb feat: URL-based deep linking for SPA navigation
- Flask catch-all route serves index.html for client-side paths, excluding api/static/auth/callback/status prefixes.- navigateToPage pushes history state so URL reflects current page.- popstate listener handles browser back/forward without reloading.- Initial load reads window.location to restore the page after refresh or direct link.- artist-detail and playlist-explorer fall back to parent pages since they need runtime context.
2026-04-19 10:46:58 -07:00
Antti Kettunen
33b4ea6429
Refine artist-detail discography flow
- move artist-detail discography resolution onto the shared source-priority metadata service
- keep the variant dedup helper in the UI-facing adapter
- pass the chosen source through completion checks
- add coverage for the new adapter and dedup behavior
2026-04-19 20:38:49 +03:00
Antti Kettunen
17865fe712
Refactor artist discography completion metadata flow
Move completion checks into metadata_service and make them follow the configured metadata source priority.

Drop the old test-mode path, remove the web_server wrapper indirection, and keep artist inference on explicit release metadata instead of guessing from a track search.

Add coverage for the source-priority completion behavior and the safer artist-name handling.
2026-04-19 15:40:08 +03:00
Broque Thomas
0380e88bb0 Fix MusicBrainz tab missing from enhanced search results
MusicBrainz was returned by the backend as an alternate source but the
frontend's orderedSources list didn't include it, so it was never fetched.
2026-04-18 23:52:48 -07:00
Broque Thomas
2bd8d2ac7a Version bump to 2.33 2026-04-18 23:39:26 -07:00
Broque Thomas
63230ae39c Add bottom padding to library pagination for scroll breathing room 2026-04-18 23:35:28 -07:00
Broque Thomas
533091f605 Update What's New — live log viewer now mentions search and smart filtering 2026-04-18 23:26:04 -07:00
Broque Thomas
3404812a1e Improve live log viewer — fix level filters, faster updates, add search
- Fix level filter showing nothing: now uses heuristic classification
  for print() output (error/traceback/failed→ERROR, warn→WARNING, etc.)
  in addition to exact logger format matching
- Speed up WebSocket updates from 2s to 0.5s polling
- Add search box with 300ms debounce — filters both initial load and live
- Use DocumentFragment for batch DOM appends (performance)
- Increase line cap from 1000 to 2000
- Backend search parameter support in /api/logs/tail
2026-04-18 23:13:51 -07:00
Broque Thomas
8b0e619fa1 Add live log viewer on Settings → Logs tab
Terminal-style real-time log viewer with:
- Log file selector (app, post-processing, acoustid, source reuse)
- Color-coded log levels (DEBUG gray, INFO blue, WARNING yellow, ERROR red)
- Level filter buttons (All/Debug/Info/Warn/Error)
- Auto-scroll with toggle, copy and clear buttons
- Live updates via WebSocket (2s polling, pushes new lines)
- Initial load fetches last 200 lines via REST API
- 1000-line display cap with oldest lines trimmed

Also fixes Advanced tab settings (Discovery Pool, Security, etc.) being
hidden inside collapsed Library Preferences section body — misplaced
closing div caused them to be invisible.
2026-04-18 22:57:15 -07:00
Broque Thomas
c0c38268f5 Fix tool help modal not closable from Automations page
The close button and backdrop click handlers were only attached when
the Tools page was visited (initializeToolHelpButtons). Automation
builder '?' buttons open the same modal but the close handlers were
never set up. Added inline onclick handlers to the modal HTML and a
global Escape key listener so closing works from any page.
2026-04-18 21:49:30 -07:00
Broque Thomas
07d67e8517 Fix Your Albums section opening playlist modal instead of album modal
Your Albums cards on the Discover page were using the YouTube/playlist
modal (openDownloadMissingModalForYouTube) instead of the album modal
(openDownloadMissingModalForArtistAlbum). Now displays with proper
album hero section and uses album download context for file organization.
2026-04-18 21:44:18 -07:00
Broque Thomas
aa8f97e3d5 Add optional ReplayGain analysis to post-processing pipeline
New toggle in Settings → Library → Post-Processing: "Apply ReplayGain
tags after download". When enabled, analyzes loudness via ffmpeg's
ebur128 filter and writes track-level ReplayGain gain/peak tags.
Runs after metadata tagging but before lossy copy so both files get
the tags. Off by default — adds a few seconds per track.

Applied to both album and playlist/single download paths.
2026-04-18 21:04:01 -07:00
Broque Thomas
288994e081 Update What's New with all today's features and fixes
Added: Genre Whitelist, Standalone Full Refresh, Folder Terminology
Rebrand, and 12 bug fixes (duplicate detector, single track downloads,
liked songs source label, metadata crash, scan button stuck, deep scan
logging, settings tab flash, standalone verify speed, and more).
2026-04-18 20:41:59 -07:00
Broque Thomas
288776a7f3 Add genre whitelist for filtering junk tags during enrichment
New core/genre_filter.py with ~180 curated default genres. When strict
mode is enabled in Settings → Library Preferences → Genre Whitelist,
only whitelisted genres pass through during enrichment. Junk tags from
Last.fm (artist names, radio shows, playlist names) are silently dropped.

Applied at all 10 genre write points: Spotify, Last.fm, AudioDB, Deezer,
Discogs, iTunes, Qobuz enrichment workers + post-processing genre merge
+ initial download artist/album creation.

Strict mode is OFF by default — zero behavior change for existing users.
First enable auto-populates the whitelist with defaults. Users can add,
remove, search, and reset genres via the Settings UI.
2026-04-18 20:23:53 -07:00
Broque Thomas
c6de707f94 Fix single track search results downloading with album context
When clicking a track in enhanced or global search, the download modal
correctly showed SINGLE but the download used is_album_download=true,
causing the file to be organized under the album path template instead
of the singles template. Now enhanced_search_track_ and gsearch_track_
prefixes pass album metadata for tagging but set is_album_download=false.
2026-04-18 19:46:20 -07:00
Broque Thomas
1564d7bd53 Fix Liked Songs playlist misidentified as YouTube source
The source detection chain for playlist hero sections didn't handle
the 'spotify:liked-songs' playlist ID prefix, falling through to the
default 'YouTube' label. Added 'spotify:' prefix check.
2026-04-18 19:21:25 -07:00
Broque Thomas
6036e02011 Fix library scan button stuck on 'Stop' and stale count shown as 'failed'
Dashboard scan polling checked for 'completed' but backend sets 'finished'.
Added 'finished' to the completion check so polling stops, button resets,
stats refresh, and toast fires correctly. Also fixed deep scan reporting
stale record removals as 'failed' instead of 'successful'.
2026-04-18 18:19:49 -07:00
Broque Thomas
f8e4adde41 Remove redundant output path mirror from standalone config section
The disabled path field on the Connections tab was showing stale data
(always ./Transfer) because it read from the DOM before settings loaded.
Removed it entirely — the output path is configured on the Downloads tab.
Standalone section now just shows description + verify button.
2026-04-18 17:53:07 -07:00
Broque Thomas
db7714c4db Fix settings page showing all tabs on first load
Non-active tab groups were visible during async data loading because
switchSettingsTab ran after the awaits. Moved it before async calls and
added CSS defaults to hide non-connections groups, preventing any flash.
2026-04-18 17:50:05 -07:00
Broque Thomas
efe8280e23 Rebrand folder terminology: Download→Input, Transfer→Output, Staging→Import
All user-facing labels, docs, help text, tooltips, error messages, and debug
info output updated. Backend config keys, variable names, actual path values,
and Docker volume mounts are completely unchanged — zero functional impact.
2026-04-18 17:35:20 -07:00
Broque Thomas
cef1162c5e Update What's New with today's features and fixes 2026-04-18 16:07:33 -07:00
Broque Thomas
b17a6e2dd7 Add per-artist metadata source override for watchlist scans
Users can now override which metadata provider (Spotify, Deezer, Apple Music,
Discogs) is used when scanning a specific watchlist artist for new releases.
The selector appears in the artist config modal and only shows sources the
artist has enrichment IDs for. Default behavior is unchanged — all artists
use the global metadata source unless explicitly overridden.
2026-04-18 16:05:05 -07:00
Broque Thomas
381e37ecf7 Enhance logging, debug info, and add Troubleshooting docs section
- Move Log Level dropdown from Downloads tab to Advanced tab (Settings)
- Fix staging path config key (import.staging_folder → import.staging_path)
- Fix library stats showing 0 (use get_database_info_for_server like dashboard)
- Add Troubleshooting & Support docs section (log files, debug info, common issues, reporting)
- Beef up Copy Debug Info: ffmpeg version, runner type, Discogs status, wishlist count,
  music library paths, music videos dir, log level, metadata source, hybrid priority,
  lossy copy config, auto import, duplicate tracks, replace quality, log file listing
- Add GitHub issue link footer to debug output
- Add discogs to enrichment worker list in debug endpoint
2026-04-18 15:05:19 -07:00
BoulderBadgeDad
f636014b9a
Merge pull request #316 from kettui/fix/reduce-ui-stalls
Reduce UI stalling during enhanced search, add caching & small concurrency optimizations
2026-04-18 12:32:55 -07:00
Broque Thomas
020ce6d765 Hide discovery modal sync buttons in standalone mode
"Sync This Playlist" buttons in YouTube/Tidal/Deezer/Spotify/Beatport/
ListenBrainz discovery modals were not gated by _isSoulsyncStandalone.
Added check to the hasSpotifyMatches condition that generates them.
2026-04-18 09:15:19 -07:00
Broque Thomas
33911a7001 Show Sync page in standalone mode, only hide sync buttons
The Sync page was hidden entirely for standalone users, blocking
access to playlist browsing, discovery, and downloads. Now the page
is accessible — only the sync-to-server buttons are hidden since
there's no server to push playlists to.
2026-04-18 09:00:55 -07:00
Broque Thomas
ad9d5817cf Fix 'Loading playlist...' stuck forever on error
openDownloadMissingModal showed loading overlay but didn't hide it
on error paths (playlist not found, fetch failure). The overlay
persisted across page navigation, blocking the entire UI.
2026-04-18 08:37:31 -07:00
Broque Thomas
f4aaab8a66 Reorganize Settings Library tab with collapsible sections
Three collapsible categories, collapsed by default:
- Paths & Organization (file templates + music library paths)
- Post-Processing (metadata, tags, conversion, lyrics)
- Library Preferences (import, content filter, stats, playlists, M3U)

Section headers have data-stg=library so they only appear on the
Library tab. Bolder headers with accent-colored arrows and subtle
border. Collapse state preserved when switching settings tabs.
2026-04-18 08:28:12 -07:00
Antti Kettunen
688e84ce4d Fix MusicBrainz label, primarily show tabs with fetched results only 2026-04-18 14:57:12 +03:00
Antti Kettunen
02f190efc6 Reduce enhanced search stalls
Delay alternate-source fan-out until the primary enhanced-search response arrives, and stagger those follow-up requests so they do not all compete at once. Also parallelize artist, album, and track lookups inside each metadata source request to shorten the time the UI thread spends waiting on remote APIs. This keeps the single-worker web UI more responsive under the app's chatty search flow.
2026-04-18 14:46:50 +03:00
Broque Thomas
841ad42fdd Fix MusicBrainz tab not appearing in enhanced search
Source fetch list was hardcoded without musicbrainz — tabs only showed
for sources that were pre-fetched in the parallel search loop.
2026-04-18 02:10:12 -07:00
Broque Thomas
4f5025d526 Add MusicBrainz search tab, wider global search, bump to v2.32
New MusicBrainz tab in Enhanced and Global search — finds tracks and
albums on MusicBrainz's community database with Cover Art Archive
images. Covers obscure tracks that Spotify/Deezer/iTunes miss.

- core/musicbrainz_search.py: search adapter with Track/Artist/Album
  dataclasses, Cover Art Archive integration, smart query parsing
- Albums deduplicated (keeps best version with date and art)
- No artist results shown (MusicBrainz has no artist images)
- Album detail with full tracklist for download modal
- Smart word-boundary splitting for queries without separators
- Global search results container widened from 620px to 920px
- UI version bumped to 2.32
2026-04-18 02:06:27 -07:00
Broque Thomas
70005968b6 Restructure version modal and What's New for v2.31
SoulSync Standalone Library is now the first section in both the
version modal and What's New popup. Auto-Import section updated with
all improvements (recursive scan, singles, tag preference, AcoustID).
New Downloads & Soulseek section groups download-related improvements.
Recent Fixes cleaned up — feature items moved to proper sections.
2026-04-18 01:28:19 -07:00
Broque Thomas
b4475152a9 Hide all sync buttons in standalone mode, bump UI to v2.31
All sync-related buttons hidden when active server is SoulSync
Standalone. Covers static buttons (querySelectorAll on status update)
and dynamic modal buttons (_isSoulsyncStandalone flag).
UI version bumped to 2.31 (Docker stays at 2.3).
2026-04-18 01:14:54 -07:00
Broque Thomas
12e9c0034b Hide Sync page in sidebar for SoulSync standalone mode
No media server to sync playlists to — sync page is irrelevant.
M3U generation is still available via settings toggle and download
modal buttons for standalone users who want playlist files.
2026-04-18 00:27:38 -07:00
Broque Thomas
d688e7fa15 Update What's New and version modal with standalone library and import fixes 2026-04-17 23:29:04 -07:00
Broque Thomas
c009acdbb6 Add SoulSync Standalone toggle to Settings page
Fourth server option on the Connections tab with SoulSync logo and
'Standalone' label. Config panel shows Transfer folder path and
Verify Folder button. Test connection counts audio files in the
Transfer folder. Settings save/load properly detects soulsync toggle.
2026-04-17 20:56:05 -07:00
Broque Thomas
bbf5af1ce1 Fix auto-import rescan race condition, coverage penalty, and UI
Race condition: scanner re-scanned folders while post-processing was
still moving files, causing partial matches and ghost failures. Now
tracks in-progress paths and skips them on subsequent scans.

Coverage penalty fix: individual tracks that match at 80%+ confidence
now auto-import even when overall album coverage is low (e.g. 2 of 18
tracks present). Previously low coverage killed the entire import.

Import page: stats bar, filter pills, Scan Now, Approve All, Clear
History (clears imported + failed), live scan progress.
2026-04-17 19:37:42 -07:00
Broque Thomas
a2e3ce8000 Fix auto-import track numbers, dates, cover art, and track name display
- Track numbers defaulted to 1 instead of using metadata source values
- Release dates not captured, causing missing year in path templates
- Cover art missing for Deezer (direct image_url not checked)
- Track names in expanded view showed Unknown (wrong JSON field name)
- Read year/date from embedded file tags as fallback
- Add Deezer get_album_metadata/get_album_tracks fallbacks
- Handle Deezer tracks.data response format
2026-04-17 19:05:52 -07:00
Broque Thomas
d2c6979ce4 Recursive staging scan, singles support, and improved import UI
Auto-import now scans the staging folder recursively — any folder
structure depth works (Artist/Album/tracks, Album/tracks, etc.).
Loose audio files are treated as singles with tag/filename/AcoustID
identification.

Import results UI redesigned:
- Click cards to expand per-track match details with confidence scores
- Shows identification method badge (Tags, Folder Name, AcoustID)
- Per-track grid: track name, matched filename, confidence percentage
- Time ago labels, folder path, better status badges
- Approve/Dismiss buttons use event.stopPropagation for clean UX
2026-04-17 17:48:00 -07:00
Broque Thomas
c20529dbb5 Fix auto-import toggle visual and import page refresh
Toggle appeared off when running because CSS :checked rules were
scoped to .repair-master-toggle. Added auto-import-toggle-label
selectors. Refresh now re-renders whichever tab is active.
2026-04-17 17:07:19 -07:00
Broque Thomas
1446c7b63a Fix auto-import toggle visual flicker
Toggle state was set by browser click, then immediately overwritten by
the status reload callback. Now optimistically sets the toggle and
status text before the API call, reverting only on failure.
2026-04-17 16:33:05 -07:00
Broque Thomas
eb2218ec8d Add file deletion option to album delete on enhanced library page
Album delete now shows a smart delete dialog with two options:
- Remove from Library (DB only, files untouched)
- Delete Files Too (removes DB records AND deletes audio files from
  disk, cleans up empty album folder)

Backend /api/library/album/<id> DELETE now accepts ?delete_files=true
parameter, resolves each track's file path, and removes files before
deleting DB records. Reports files_deleted and files_failed counts.
2026-04-17 15:26:25 -07:00
Broque Thomas
619b7ab4be Update What's New and version modal with recent fixes 2026-04-17 15:05:57 -07:00
Broque Thomas
aede7dd089 Fix download modal freezing by moving M3U save to completion only
autoSavePlaylistM3U was called on every 2-second poll cycle once any
track completed, flooding the server with heavyweight M3U generation
requests (fuzzy matching all tracks against the DB). This exhausted
Flask's thread pool, causing the batch status endpoint to hang and
killing the poller — making the modal freeze mid-download.

Now fires once when the batch completes instead of on every poll.
2026-04-17 15:01:45 -07:00
Broque Thomas
7415485b9a Harden download modal polling against premature completion
- Include completed batches in poll cycle so late task updates still
  render (prevents modal freezing when batch completes before all rows
  update)
- Require server to report no active tasks before client-side
  completion fires (prevents phase=complete from prematurely ending UI)
- Apply same fix to backoff poller and WebSocket resubscription

Note: modal stalling issue persists — setInterval stops firing after
~12 cycles for unknown reasons. Needs deeper browser-level debugging.
2026-04-17 14:52:18 -07:00
Broque Thomas
57259b6e3a Always run HTTP polling for download modal updates
The global download poller was disabled when WebSocket was connected,
but WebSocket connections can silently stop delivering messages without
triggering a disconnect event (room subscription lost, server emit
error, proxy timeout). This left modals frozen while downloads
continued server-side. Removing the socketConnected gate ensures the
2-second HTTP poll always runs as a fallback alongside WebSocket.
2026-04-17 14:08:48 -07:00
Broque Thomas
1384b966e8 Fix Unknown Artist when adding playlist tracks to wishlist
When adding tracks to wishlist from a playlist download modal,
process.artist was undefined (only set for album downloads) and
defaulted to {name: 'Unknown Artist'}. This got stored in
source_info.artist_name and was prioritized over the track's own
artist data during wishlist download post-processing.

Now resolves the artist per-track from the track's own artists array,
falling back to the process-level artist only for album downloads
where it's actually set correctly.
2026-04-17 14:01:56 -07:00
BoulderBadgeDad
6b70d7331c
Merge pull request #295 from pxjx22/fix/socketio-polling-fallback
Fix Socket.IO client fallback order
2026-04-17 13:12:22 -07:00
Broque Thomas
168b4c21dd Fix batch panel collapse button not clickable when collapsed
Title text was pushing the toggle button out of the 44px collapsed
panel. Now hides the title and centers the button when collapsed.
2026-04-17 12:58:29 -07:00
Broque Thomas
8f7a3b4861 Use SoulSync confirm dialog for batch cancel instead of browser alert 2026-04-17 12:29:50 -07:00
Broque Thomas
04b8c02ea9 Reject junk artist Soulseek results and cancel downloads on wishlist clear
Soulseek results from "Various Artists", "VA", "Unknown Artist", and
"Unknown Album" folders are now rejected before scoring. These
compilation folders rarely contain properly tagged files for the target
artist.

Clearing the wishlist now also cancels any active wishlist download
batch and resets the auto-processing flag, so downloads don't keep
running after the source tracks are removed.
2026-04-17 12:24:50 -07:00
Broque Thomas
9898bd1190 Add batch context panel to Downloads page
Split Downloads page into main list (left) and batch panel (right).
Each active batch gets a color-coded card with artwork thumbnail,
progress bar, per-track status with download percentages, and
expandable track list. Download rows get matching color indicators.

- Click batch name to open its download/wishlist modal
- Filter icon narrows main list to one batch with clear banner
- Collapsible panel toggle for full-width list view
- Completed batches fade out after 15 seconds
- 7-day batch history with source type color dots
- Artwork fallback shows colored initial when no art available
- Per-track progress: download %, spinner for searching, proc label
- source_page column on sync_history for UI origin tracking
- /api/downloads/all includes batch summaries and per-track progress
- /api/downloads/batch-history endpoint for history queries
- Responsive layout, overflow-x hidden to prevent scroll flicker
2026-04-17 12:18:00 -07:00