Commit graph

1824 commits

Author SHA1 Message Date
BoulderBadgeDad
2b8f6f8611 Release 2.8.0: version bump + docker-publish tag + What's New / version modal + PR description
- web_server: _SOULSYNC_BASE_VERSION 2.7.9 -> 2.8.0
- docker-publish.yml: default version_tag -> 2.8.0
- pr_description.md: rewritten for 2.8.0 (preview-clip cleanup, unverified-queue self-heal #934,
  album-completeness split albums #936, clear-completed, youtube cookies, #937, discography
  speed, wishlist art, dashboard perf #935, bounded memory #802)
- helper.js: WHATS_NEW carries the 2.8.0 block + folded "Earlier versions" summary;
  VERSION_MODAL_SECTIONS leads with 2.8.0 highlights, rolls 2.7.9 into an aggregator
2026-06-27 23:58:22 -07:00
BoulderBadgeDad
d30273985f downloads: restore Clear Completed for persisted history (clears the whole completed list)
since 9a0e3b40 persisted completed downloads in the Downloads view, the Clear Completed button
was hidden for those rows and clear-completed only pruned live session tasks. after a restart
the page filled with persisted completed downloads with no way to clear them.

now Clear Completed clears BOTH:
  - live session completed/failed tasks (clear_completed_local, unchanged), AND
  - the persisted download-history tail: new clear_completed_download_history() deletes every
    library_history event_type='download' row, so the list actually empties and stays empty.

this includes unverified rows (the verification review queue) by design: on a library where
verification never confirmed the imports, ALL completed downloads are 'unverified', so preserving
them made the button a no-op. it only removes HISTORY rows — the actual files and their tracks
entries are untouched, so nothing in the library is lost, only the 'needs verification' flags.
the action confirms first (showConfirmDialog, destructive) and the button now shows whenever any
completed/failed row is present.

3 seam tests (clears all incl unverified; leaves non-download history; empty=0); reconcile +
orphan + JS integrity suites green.
2026-06-27 23:37:06 -07:00
BoulderBadgeDad
d84fba14ba
Merge pull request #938 from nick2000713/fix/unverified-acoustid-934
Fix/unverified acoustid 934
2026-06-27 22:14:01 -07:00
BoulderBadgeDad
88b68b8073 worker orbs: drive the render loop with setInterval on firefox (permanent 1fps fix)
the dashboard orb canvas kept falling back to ~1fps on firefox after the page settled. root
cause: firefox throttles requestAnimationFrame to ~1fps for a canvas it heuristically deems
occluded. the WAA keepalive only delayed the heuristic; it re-fired over time.

real fix: on firefox, drive tick() with a setInterval(~60fps) instead of self-scheduling rAF —
setInterval is not subject to the canvas-occlusion rAF throttle, so the orbs stay at full
framerate indefinitely. chrome is untouched (keeps vsync-aligned rAF). same render workload
(idle still drops to 20fps via the existing sleep skip); background tabs still parked by the
visibilitychange handler → stopLoop clears the interval. kept the keepalive (harmless).
2026-06-27 18:27:31 -07:00
BoulderBadgeDad
b683c7fb50 wishlist: render library-sourced art (album + artist) — fix blank wishlist images
root cause: the library stores album/artist art as media-server RELATIVE paths (Plex
/library/metadata/.., Jellyfin /Items/.., Navidrome /rest/..), which don't render in a browser
<img>. normal wishlist items carry Spotify CDN urls so they show fine, but LIBRARY-sourced
items — dead-file re-downloads and preview-clip re-fetches — carry the raw relative path, so
their album art came up blank. and the nebula only had artist photos for WATCHLISTED artists,
so non-watchlist orbs showed initials.

fix on READ in the wishlist tracks endpoint (so it also repairs items already in the wishlist,
no re-run needed), using the library data we already have:
  - normalize each track's album.images url that needs it — relative/internal only, via the
    canonical normalize_image_url; CDN urls are left untouched so already-rendering items can't
    regress.
  - build an artist-name -> normalized library-photo map and return it; the nebula seeds its
    artist-image map from it (every wishlist artist), with curated watchlist photos overriding.

8 tests (predicate: relative/internal fixed, CDN untouched; album normalize in-place; artist
map build/skip-empty/idempotent/graceful). 237 wishlist+repair+JS tests green, ruff clean.
2026-06-27 17:35:28 -07:00
dev
0be1952222 downloads(#934): opt-in "Clean orphaned" action for dead review-queue rows
The reconcile heals rows whose file is still in the library; it deliberately
leaves ORPHANS — history rows whose file is gone (deleted / replaced /
re-downloaded elsewhere). Those can never be healed (no file left to confirm)
and linger in the Unverified list forever. This adds an explicit, user-initiated
cleanup for them.

- core/downloads/orphan_history.py: pure, tested rule. A row is an orphan when
  its file resolves nowhere; flags `suspicious` when EVERY reviewed file is
  unreachable (the mount-down signature) so the caller refuses rather than
  mass-delete a healthy log during an outage.
- POST /api/verification/clean-orphans (admin-only): runs it against
  _resolve_history_audio_path (raw path -> prefix-swap resolver -> tracks-table
  title fallback), refuses on the suspicious signature, and deletes only history
  ROWS — never a file (the files are already gone).
- UI: "🧹 Clean orphaned" button in the Unverified bulk-actions row, with a
  confirm dialog spelling out that it removes log rows only and refuses if the
  library looks offline.

NEVER automatic / never at boot — a filesystem check during a mount outage would
otherwise wipe good history. 5 pure-rule tests + safety-gate coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWJk7EuM7YktQeNyqQwTZY
2026-06-28 02:04:08 +02:00
BoulderBadgeDad
034a19b24f preview-clip findings: add Play button + length comparison to verify before approving
each preview-clip finding now renders a dedicated detail card: File Length vs Real Length
(e.g. 28s vs 200s) and a Play button so the user can listen to the clip and confirm it's a
busted ~30s preview before approving the delete + re-download. reuses the existing
_renderPlayButton/playFindingTrack path that dead_file/orphan findings already use (the
finding carries file_path + title/artist/album, everything it needs).
2026-06-27 16:27:27 -07:00
BoulderBadgeDad
116edeb477 tools: add Preview Clip Cleanup repair job (detect ~30s previews, re-fetch full track)
HiFi (and occasionally other) downloads sometimes deliver a ~30s preview clip instead of the
full song; it lands in the library looking real. new repair job scans short tracks (duration
<= 30s, configurable), looks up the EXPECTED length from the track's metadata source
(spotify/itunes/mb get_track_details), and flags any whose real length is much longer than the
file (default: >= 30s longer) as a preview clip.

approving the finding (repair_worker._fix_short_preview_track) deletes the preview file (path
resolved via _resolve_file_path like the other delete tools), drops the DB row so the track
goes missing, and re-adds it to the wishlist with the full payload (mirrors _fix_dead_file)
so the real version downloads. scan ONLY creates findings — nothing destructive without user
approval, like every other tool.

conservative: genuine short tracks (source agrees they're short) and tracks whose length can't
be verified are skipped, never flagged. registered the job + finding-type label/fix-button in
the UI. 5 tests (scan flag/skip/scope + fix delete+remove+wishlist); 89 repair tests green.
2026-06-27 16:07:41 -07:00
BoulderBadgeDad
5a54ffe14a dashboard: show SoulSync's own RAM next to system memory %
the Memory Usage stat showed only global system memory (psutil.virtual_memory().percent).
add the process's own resident set size (RSS) — the real 'how much RAM SoulSync uses' number —
formatted MB under 1GB, GB above. headline stays the system %, subtitle now reads 'SoulSync ·
612 MB' instead of the generic 'Current usage'. graceful fallback if psutil errors / older
backend. useful context after the recent RAM-footprint discussions.
2026-06-27 13:42:14 -07:00
BoulderBadgeDad
6802399805 dashboard/shell: drop frosted-glass blur on Firefox only (#935)
Firefox re-rasterizes blur()/backdrop-filter every composite where Chrome caches it, so the
always-visible shell glass (sidebar header + aura orbs, hero/header buttons) was ~half of
Firefox's idle GPU. gate behind @supports(-moz-appearance:none) so it's Firefox-only: hide the
two blur(28px) sidebar orbs + the dash-card blobs, and drop backdrop-filter on the sidebar
header and hero/header buttons (each keeps its tint, just unfrosted). measured ~20-25% -> ~10-13%
on Firefox, every page (sidebar is always visible). chrome is untouched — the block doesn't
exist there, full frost intact.
2026-06-27 13:36:15 -07:00
BoulderBadgeDad
240dce0c1b worker-orbs: Firefox compositor keep-alive fixes post-hover 1fps slowdown (#935)
removing the always-on dash-card blob animation (for the Chrome GPU win) incidentally let
Firefox start throttling the worker-orb canvas's compositing to ~1fps after a header hover
re-layerizes the dashboard — Chrome never throttles it. re-add the 'keep the compositor warm'
effect cheaply: a 2px, ~invisible element running an infinite transform-only animation (zero
paint). gated behind CSS.supports('-moz-appearance') so it's Firefox-only; Chrome never gets
it. confirmed fix in Firefox/Zen.
2026-06-27 13:07:03 -07:00
BoulderBadgeDad
c6caaaa599 dashboard: hide cursor-glow blobs on Firefox only (#935)
the .dash-card cursor blobs are 16 large blur(48px)/blur(18px) layers. chrome caches them
once; firefox re-rasterizes blur on every composite, so they're a big chunk of idle dashboard
GPU on firefox. they're purely decorative and reduce-effects already hides them. gate behind
@supports(-moz-appearance:none) so it's firefox-only — chrome keeps the full cursor glow,
this block doesn't exist there.
2026-06-27 12:53:10 -07:00
BoulderBadgeDad
9faaf5c50c sidebar orbs: drop scale() from the drift so the blur stops re-rasterizing (#935)
same antipattern as the dash-card blobs: .sidebar::before/::after are blur(28px) and the
orb keyframes animated transform: scale() infinitely → the GPU re-blurred them every frame,
on every page (the sidebar is always visible). keep the translate drift + opacity (both
compositor-only, the blur layer just moves), remove the scale. same look, no per-frame reblur.
2026-06-27 11:34:37 -07:00
BoulderBadgeDad
bce6a91aa2 dashboard: stop the cursor-blob pseudo-elements re-blurring every frame (#935)
each .dash-card renders two accent-blob pseudo-elements — ::before is 1280x1280 blur(48px),
::after 540x540 blur(18px) + mix-blend-mode:screen — and both ran an INFINITE scale-pulse
animation. scaling a blurred element re-rasterizes the blur every frame; with 8 cards × 2
blobs that's 16 huge blurred layers re-blurring at 60fps whether or not the user touches
anything. that's the dashboard's whole-screen repaint / ~36% idle GPU.

remove the infinite pulse (the dashBlob*Pulse animations). the blob still follows the cursor
via --blob-x/y; it just no longer 'breathes' at idle, so when nothing's moving there's nothing
to repaint. trimmed will-change to the props that actually change (left/top).
2026-06-27 11:29:39 -07:00
BoulderBadgeDad
28a539a840 ui: default Background Particles OFF (#935)
the full-page particle canvas runs a continuous requestAnimationFrame loop behind every
page — real GPU cost, and multiple users hit GPU strain until they found the toggle. flip
the default to off; the eye candy is opt-in now.

- init.js: runtime flag defaults false unless localStorage is explicitly 'true'
- settings.js: config read is now '=== true' (default off) instead of '!== false'
- index.html: checkbox no longer 'checked' by default; hint reworded

existing users who explicitly enabled it (localStorage/config 'true') keep it on; the
existing '!== false' runtime guards still work since the flag is now always set explicitly.
2026-06-27 10:48:07 -07:00
dev
027cf74d47 ui(downloads): Unverified review rows get the Quarantine-style card design
The Quarantine sub-view was reworked into rich cards (artwork, source line,
row-click to expand an inline detail panel, consistent action cluster) but the
Unverified sub-view was left on the generic download-row layout that opened a
modal on click. Bring it to parity:

- dedicated _verifUnverifiedRowHtml / _verifUnverifiedRows renderer, used via a
  sibling branch in _adlRender (mirrors the quarantine sub-view branch).
- row click toggles an inline details panel (why flagged, download source,
  quality, file, downloaded-at), open-state keyed by stable id so it survives
  the 2 s poll re-render — same pattern as verifQuarInspect.
- reuses the existing verif-quar-* / verif-actions / adl-row CSS (no new styles)
  and the existing play/compare/audit/approve/delete handlers.
- NO grouping: each unverified import is its own track (grouping only makes
  sense for the quarantine alternates), per design intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWJk7EuM7YktQeNyqQwTZY
2026-06-27 19:27:28 +02:00
BoulderBadgeDad
30ff0bde49 Release 2.7.9: version bump + What's New / version modal + PR description + docker-publish default tag
- _SOULSYNC_BASE_VERSION -> 2.7.9; docker-publish default version_tag -> 2.7.9
- pr_description.md rewritten for 2.7.9 (best-quality downloads + quality profile #896, Discover listening recs + Listening Mix #913, Wing It Pool, Auto-Sync lane redesign, multi-disc #927, sync labels #925, post-processing race #928)
- WHATS_NEW: 2.7.8 block -> 2.7.9 (+ brief earlier-versions); VERSION_MODAL_SECTIONS: 2.7.9 highlights promoted, 2.7.8 rolled into an Earlier aggregator
- RELEASE_2.7.9_discord.md: mini Discord post
2026-06-25 16:11:51 -07:00
BoulderBadgeDad
3c33e31985
Merge pull request #928 from nick2000713/fix/post-processing-race-and-followups
Fix import-vs-quarantine race + opt-in rank-based download order + quality-settings UI cleanup
2026-06-25 14:52:53 -07:00
BoulderBadgeDad
dc813d67c1
Merge pull request #926 from ramonskie/fix/issue-925-playlist-sync-label
Fix playlist sync status labels
2026-06-25 14:39:42 -07:00
BoulderBadgeDad
9847d6f0a9 Wing It Pool: two-card landing (review + resolved), matching the Discovery Pool
Opens to the same category-card landing the Discovery Pool uses, with two cards: 'guesses to review' (unverified wing-it) and 'resolved manually' (ones you've Fixed) — click to drill in, Back to return. Previously it jumped straight to a single list.

To populate the resolved list, the /fix endpoint now stamps was_wing_it on the rewritten extra_data (the wing_it_fallback flag is otherwise lost on fix), and get_wing_it_pool gained a resolved flag + the stats return both counts. Fixing/re-matching from either card refreshes in place. Seam test updated for both states.
2026-06-25 14:15:42 -07:00
BoulderBadgeDad
602b035bad Wing It Pool: review + re-match tracks Wing It auto-matched
Wing It auto-matches tracks to the server library on a best-effort guess; those tracks are flagged wing_it_fallback in extra_data and count as 'discovered', so the Discovery Pool hides them — there was no way to see or audit the guesses. New 'Wing It Pool' button (next to Discovery Pool on the Mirrored Playlists tab) opens a modal listing them with a per-playlist filter + search; 'Fix Match' reuses the Discovery Pool's fix flow (/api/discovery-pool/fix), and a manual match drops the track from the pool on refresh.

No new table or provider hooks needed — the wing-it flag is already persisted, so this is a pure query (get_wing_it_pool / get_wing_it_pool_stats, cloning the failed-pool LIKE pattern) + a /api/wing-it-pool endpoint + a cloned modal. Found 81 wing-it tracks on a real library. Seam-tested (include unverified / exclude manual-matched / scope by playlist+profile).
2026-06-25 13:57:50 -07:00
BoulderBadgeDad
e3915b63e6 Library cards: in-card badges no longer trigger artist-detail navigation
The card is an <a> link and the shell's capture-phase link handler navigated to artist-detail before the grid's bubble-phase badge handler could preventDefault — so clicking the watchlist eye or a source badge opened the detail page (and the badge's own link too). The shell handler now bails when the click lands on an in-card control (.source-card-icon or [data-no-card-nav]), letting the badge do only its own thing.
2026-06-25 13:57:34 -07:00
BoulderBadgeDad
7a8b66fd2e Auto-Sync Manager: redesign hourly + weekly boards as horizontal lanes
Replace the side-scrolling column board with vertically-stacked interval lanes (hourly) and day lanes Mon-Sun (weekly). Empty intervals/days collapse to thin dashed strips, busy ones grow; scheduled playlists flow as cards within a lane. Kills the horizontal scroll + the wasted whitespace of the old kanban columns, and the two boards now share one cohesive design.

Polish: accent gradient wash + gradient interval numerals + count badge on filled lanes, drag-over glow/lift, card pop-in animation, hover states. Also preserves the board's scroll position across the full re-render so dropping/removing a playlist no longer snaps it back to the top. Same drag-and-drop handlers + scheduled-card content reused; old column CSS is now unused (harmless).
2026-06-25 13:06:07 -07:00
dev
2668980872 feat(ui): collapsible ⓘ help, rank-based toggle, tidier quality-profile settings
Quality-profile settings UI cleanup:

- Add the "Rank-based download order" toggle (priority mode). It's hidden when
  Best quality is active, since that mode always ranks by quality.
- Plain-language search-strategy options ("fast" / "thorough"); load + save the
  new rank_candidates_by_quality flag.
- Move the long help texts behind a dim ⓘ icon that sits on the (fixed) label
  row and toggles a collapsible body below — the trigger no longer moves on
  open. Applied to: search strategy, rank-based order, off-list fallback,
  AcoustID-verified, and the "How it works" ranked-targets explainer.
  toggleSettingHelp walks to the next .setting-help-body sibling so it works
  regardless of wrapper or an in-between control.
- Fix the "Search strategy" label: zero the flex-row margin so it aligns with
  the ⓘ, and bump it to 12px/brighter so it doesn't read as dim/undersized.
- Remove the duplicate "🎵 Quality Profile" heading inside the tile body.
- Replace the inline "Reset to defaults" link with a proper ↺ button.
- Restore the gap between the "Quality priority" label and the target list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:01:09 +02:00
BoulderBadgeDad
88ff47e115 SoulSync Discovery sync tab: list all kinds + Listening Mix generator
The tab reads the v2 personalized framework (personalized_playlists), but the Discover page generates through the legacy path and nothing seeded those v2 rows -> the tab was empty. Fixes:

- New 'listening_mix' v2 generator: hands the scan's stored 'listening_recs_tracks_full' tracks to the personalized manager so the Listening Mix can mirror + Auto-Sync like every other kind (no pool hydration; can't shrink on rotation). Registered + tested.
- Sync tab now lists every registered SINGLETON kind (Listening Mix, Fresh Tape, Archives, Hidden Gems, Discovery Shuffle, Popular Picks) as a card, not just already-generated rows. Clicking 'Refresh & Mirror' runs the generator + mirrors. Variant kinds (decade/genre/daily) need a picker, so they're not auto-listed; existing variant rows still show.

Additive: new generator + frontend merge, no backend endpoint changes. End-to-end verified (refresh -> generate -> persist -> syncable tracks).
2026-06-25 11:38:41 -07:00
BoulderBadgeDad
9c91ba29bf Discover: listening-driven recommendations + mix (#913), Fresh Tape fix
#913 was silently producing 0 recs: similar_artists.source_artist_id is a SOURCE id (Spotify/etc.), but the scan keyed id->name by internal artists.id (resolved nothing), and the consensus ranker was fed the name-collapsed get_top_similar_artists (consensus could never fire). Fixed + elevated:

- id->name keyed by source-id columns; raw per-seed edges (real consensus); similarity_rank threaded into the score; recency-weighted seeds (recent plays boost lifetime favs)
- new 'Based On Your Listening' artist row (/api/discover/listening-recommendations) with 'because you listen to X' explanations
- new 'Your Listening Mix' track row: each rec's top tracks via a guarded, name-resolved Spotify/Deezer fetch (falls back to the discovery pool), stored as full render dicts so the row can't shrink on pool rotation
- pure tested core: similarity_from_rank, build_recency_weighted_seeds, to_mix_track, names_match (+ rank-aware grouping)

Fresh Tape (5-10 tracks): future-dated albums sorted to the top of get_discovery_recent_albums and ate the 50-album budget before the is_future_release skip ran. Add exclude_future_years + fetch a generous budget; downstream caps unchanged. Regression tested.

Also drop the per-track block 'X' from the compact playlist rows (wrong spot). Plan/audit in DISCOVER_BEST_IN_CLASS_PLAN.md.
2026-06-25 10:15:20 -07:00
BoulderBadgeDad
71aa3397bf Music automations page: hide video-owned automations
Video-side automations (owned_by='video') live in the shared automation-engine DB and were rendering on the music automations page across branches. Filter them out client-side — the /api/automations endpoint is shared with the video page + auto-sync board, so it can't filter server-side. Pure no-op for anyone without the video side (they have no such rows); auto_sync rows untouched.
2026-06-25 10:15:00 -07:00
ramonskie
ad657f02a8 Fix playlist sync status labels 2026-06-25 13:43:24 +02:00
BoulderBadgeDad
ed0a2079cf
Merge pull request #896 from nick2000713/feature/best-quality-search-mode
Global quality system: real-audio verification, best-quality search & quality profiles (please try...not ready to merge)
2026-06-24 20:27:38 -07:00
BoulderBadgeDad
f010fbc487 Release 2.7.8: version bump + What's New / version modal + PR description + docker-publish default tag
- _SOULSYNC_BASE_VERSION -> 2.7.8; docker-publish default version_tag -> 2.7.8
- pr_description.md rewritten for 2.7.8 (align playlists + re-add-to-wishlist-from-sync
  features, the #922 Spotify-Free label fix, and the #918 iTunes-cache self-heal follow-up)
- WHATS_NEW: replaced the 2.7.7 block with 2.7.8 (current release + brief 'earlier versions')
- VERSION_MODAL_SECTIONS: promoted the 2.7.8 highlights, rolled 2.7.7 into an 'Earlier' aggregator
2026-06-24 19:02:44 -07:00
BoulderBadgeDad
79101e1847 Sync detail: label wing-it rows 'Unmatched', not '→ Wishlist'
A wing-it fallback track shows download_status='wishlist' (the sync stamps that on
every unmatched track) but was never actually added — the sync skips wing_it_* for
the wishlist. Showing '→ Wishlist' implied it was wishlisted. Now those rows read a
muted, non-actionable 'Unmatched' instead. Real wishlisted tracks keep the amber
'→ Wishlist' re-add button.
2026-06-24 18:30:49 -07:00
BoulderBadgeDad
5b7f99c30b Sync wishlist re-add: skip wing-it stubs (match the sync), no clickable button
'sami matar' was a wing-it FALLBACK stub — a placeholder the discovery pipeline
makes when it can't resolve a track to real metadata (no album, no cover). The
live sync explicitly skips wing_it_* ids for the wishlist (no metadata to act on),
but my re-add didn't — so it stored a coverless, single-classified placeholder.
That's why: sync didn't add it, no images, marked single.

Fix (parity): reconstruct refuses ids starting 'wing_it_'. Frontend renders the
'-> Wishlist' status as plain, non-clickable text for wing-it rows (with a tooltip)
since they were never actually wishlisted. Real tracks keep the working button +
the byte-identical-payload re-add from the prior fix.
2026-06-24 16:51:43 -07:00
BoulderBadgeDad
e148f859e7 Sync detail modal: click '→ Wishlist' to re-add a track with the original context
In the dashboard Recent Syncs detail modal, the '→ Wishlist' status on unmatched
tracks is now a button. Clicking it re-adds that exact track to the wishlist with
the SAME context the sync used (source_type='playlist' + the playlist's name/id +
failure_reason), so it's indistinguishable from the original auto-add.

- reconstruct_sync_track_data() (pure, tested): prefers the full cached track from
  tracks_json (by source_track_id, then index) so album art/full data carry over;
  falls back to the track_result fields; refuses non-'wishlist' rows and rows with
  no id (can't re-wishlist a matched/unidentifiable track).
- POST /api/sync/history/<id>/track/<i>/wishlist resolves the entry server-side and
  calls the wishlist service; idempotent (reports added vs already-on-wishlist).
- button shows a busy state then '✓ Re-added' / '✓ On wishlist'.

7 pure tests (full-track preference, id-vs-index match, fallback rebuild, non-
wishlist + out-of-range refusal). JS/PY/ruff clean.
2026-06-24 16:09:55 -07:00
dev
81a8b57dba feat(ui): show download source on quarantine rows (like Completed)
Quarantine rows now display the download service (HiFi / Soulseek / Tidal …)
on a third line, matching the Completed view's source line. Derived from the
entry's source_username (a streaming service name passes through; a Soulseek
uploader/peer collapses to "Soulseek") and rendered with the same
adl-row-batch styling + _adlSourceLabel mapping the Completed rows use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 00:04:16 +02:00
dev
df4ef99389 feat(ui): add a Custom… option for manual lossy bitrate entry
Keeps the reference presets (96/128/192/256/320) but adds "Custom…", which
reveals a number input so you can type any minimum bitrate. addRankedTarget
reads the manual value when the dropdown is on Custom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:30:27 +02:00
dev
551d12dba3 feat(ui): add "All lossless / All lossy" group entries to ranked targets
Convenience: pick a group + constraints (e.g. All lossless, ≥24-bit/≥96kHz) and
it expands into one concrete per-format target each (FLAC/ALAC/WAV, or the five
lossy formats) at that slot — so you don't add them one by one. Purely UI; the
backend still ranks concrete per-format targets. Re-adding a group skips formats
that already have an identical target, and the expanded entries can be
reordered/pruned individually afterwards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:24:28 +02:00
dev
124e8bb21c fix(ui): polish the ranked-target add controls
- dark-style the format <optgroup>/<option> so the dropdown no longer shows
  light "bars" over the dark theme (mirrors the existing
  .library-source-filter-select optgroup treatment)
- replace the fiddly tiny kbps number input with a dropdown of reference
  bitrates (Any / ≥96 / ≥128 / ≥192 / ≥256 / ≥320, default 320) — no typing,
  consistent with the lossless bit-depth/sample-rate selects
- bump control font 11→13px, larger padding + min-height, format select
  min-width so the row is comfortable to use

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:14:12 +02:00
dev
774d0f6c65 feat(quality): make every audio format controllable via ranked targets
The ranked-target list is now the single source of truth for which formats
download, in the user's exact priority order, for ALL sources — no hardcoded
format hierarchy decides anything. A candidate passes only if it matches a
ranked target; if nothing matches, the existing Use-Fallback toggle decides.

- source_map: new shared format_from_extension() + AUDIO_EXTENSIONS — one
  source of truth for extension→format used by every extension-based source, so
  adding a format lights it up everywhere. Soulseek now classifies through it
  (opus/wav/aiff were previously dropped as 'unknown').
- file_ops.probe_audio_quality (generic import-time guard, all sources): add
  WMA; detect ALAC from the real codec (an .m4a is AAC or ALAC).
- soulseek: drop the AAC-specific opt-in gate — AAC now follows the same
  universal rule as every format.
- model.tier_score: documented as ONLY a same-format tiebreak + fallback order,
  never cross-format priority (the list owns that); add opus/alac bases.
- UI: ranked-target editor offers all formats (FLAC/ALAC/WAV·AIFF lossless with
  bit-depth+sample-rate; MP3/AAC/OGG/Opus/WMA lossy with min-bitrate).
- tests: AAC retargeted to the universal model; new coverage for
  format_from_extension and matches_target across all formats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:01:20 +02:00
BoulderBadgeDad
49d3c77808 Align playlists: add Jellyfin support (in-place reorder via Move endpoint)
Completes align across all three servers. Jellyfin reorders in place: DELETE the
extra entries (Mirror) then POST /Playlists/{id}/Items/{entryId}/Move/{index} for
each desired track in ascending order — so the playlist's poster/name/Id survive
(no delete-recreate), same as Plex/Navidrome. Mirrors the existing reconcile path's
entry-id handling (PlaylistItemId via /Playlists/{id}/Items).

- jellyfin reorder_playlist() + get_playlist_track_ids(); reuses the shared, tested
  plan_align_rewrite planner (no new pure logic).
- /align endpoint + frontend gate now cover navidrome|plex|jellyfin.

UNTESTED LIVE: no Jellyfin instance to verify against (same status as the Navidrome
path). Plex is the only one confirmed working end-to-end so far.
2026-06-24 13:20:32 -07:00
BoulderBadgeDad
bac5da9177 Align playlists: add Plex support + cover art + modal redesign
The align buttons were gated to Navidrome, so Plex users (the actual tester) never
saw them. Plex reorders in place via plexapi moveItem/removeItems — preserves the
playlist's poster/summary/ratingKey (no delete-recreate), same spirit as Navidrome's
overwrite.

- plex_client.reorder_playlist(): moves each desired track into sequence, removes
  any current item not in the ordered list (Mirror drops extras; Keep includes them).
  get_playlist_track_ids() feeds the shared tested plan_align_rewrite.
- /align endpoint dispatches navidrome + plex; reuses the pure planner for both.
- frontend gate opened to navidrome|plex.
- modal redesigned: cover art per row, gradient header, pop/fade animation, hover
  rows, real polish (was a plain numbered list).

plexapi moveItem/removeItems signatures verified against the installed version.
2026-06-24 13:12:09 -07:00
BoulderBadgeDad
8afbfbfeab Align modal: pin footer so the Align buttons aren't clipped on long playlists
The server-order list wasn't flex-shrinking, so a long tracklist pushed the
align footer past the dialog's 80vh cap and overflow:hidden clipped it. Make the
list flex:1/min-height:0 (scrolls) and the footer flex:0 0 auto (always visible).
2026-06-24 13:03:10 -07:00
BoulderBadgeDad
606d1f951d Align playlists: reorder a server playlist to the source order (Navidrome)
Adds the 'Align playlists' action to the out-of-order modal — a dedicated,
order-only write path that does NOT touch the normal sync. Subsonic has no
per-track move, so it overwrites the song list in source order via createPlaylist
+ playlistId (same primitive replace-mode uses; identity/id preserved).

- plan_align_rewrite() (pure, tested): matched server ids in source order; every
  one must already be in the playlist (never injects a track); extras either
  dropped ('Mirror source') or parked at the end ('Keep extras'); returns None on
  stale data so a vanished track can't be written.
- navidrome rewrite_playlist_order() primitive (raw ordered ids).
- /api/server/playlist/<id>/align: validates ids are in the live playlist, then
  rewrites. Navidrome-only for now (Plex/Jellyfin reorder = follow-up).
- modal gets two explained options; missing tracks are NOT added (normal sync's
  job) and that's stated. Metadata-free by design — it only reshuffles existing
  server ids, so there's no sync-parity surface.

Open: confirm createPlaylist+playlistId preserves the playlist comment/image on a
live Navidrome (same risk as replace mode); add a re-apply step if it doesn't.
2026-06-24 12:58:01 -07:00
BoulderBadgeDad
ecd2500c39 Server playlist editor: surface 'accurate but out of order' + read-only server-order view
The editor renders the server column in SOURCE order (reconcile_playlist pairs
each server track to its source row), so a reordered-but-same-membership playlist
read as '5 matched / in sync' when Navidrome's real order actually differed — the
reorder never reaching the server was invisible.

- compute_order_status() (pure, tested): matched tracks' server positions must be
  strictly ascending in source order; uses RELATIVE order so missing/extra tracks
  never false-flag. reconcile entries now carry server_index (additive).
- endpoint returns order_status + server_order (the server's actual sequence).
- editor shows an amber 'out of order' badge on the server column when membership
  matches but sequence differs, opening a read-only modal of the real server order.
  One-way: source order stays the source of truth; no server-side editing.

Tests reproduce the reported 'Real Love Baby moved to #2' case + guard against
false-flagging on missing/extra. The actual 'sync order' WRITE is a separate
follow-up (membership/extra semantics + live identity-preservation test pending).
2026-06-24 12:15:47 -07:00
dev
ff12d8bbf2 fix(repair-jobs): boolean settings saved as string 'true'/'false' by UI dropdown
HTML <select> options can only store string values, so setting_options booleans
([True, False]) were serialised as 'true'/'false' strings and sent to the API.
Python's `x is True` check returned False for the string, making require_top_target
and deep_audio_verify permanently read as False regardless of what the user saved.

Fix JS: convert 'true'/'false' strings to real booleans before POSTing.
Fix Python: _to_bool() in quality_upgrade + inline coercion in scanner to handle
both existing string values in config and correct future booleans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:59:25 +02:00
dev
1f14fb4d5e fix(quarantine): align single rows with groups via fixed-width alt-slot
Move the "▾ N more" toggle out of .verif-actions into a dedicated
.verif-quar-alt-slot div (min-width: 68px) so every row reserves
the same horizontal space — action buttons now stay aligned whether
a row is a single track or the head of a group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:44:23 +02:00
dev
ef0b68a973 fix(quarantine+quality): persistent group toggle, better alt-row UI, quality upgrade default scope
Quarantine grouping:
- First candidate shown as normal row; others hidden under a "▾ N more"
  button inline in the actions bar — no separate header row
- Group open state tracked in _verifQuarOpenGroups (Set), survives
  periodic re-renders so the list no longer auto-collapses

Quality Upgrade Finder:
- Default scope changed from 'watchlist' to 'all' so it scans the whole
  library when no scope is explicitly configured
- _get_settings rewritten to read the full settings dict at once
  (same pattern as QualityUpgradeScannerJob) to fix silent read failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:26:41 +02:00
dev
5f5bf4b24e feat(quarantine): consolidate quarantine view into downloads page filter
Removes the duplicate Quarantine tab from the Library History modal and
brings the same-song grouping feature into the ⚠ Unverified/Quarantine
filter on the Downloads page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:14:54 +02:00
dev
842bc0ab34 fix(quarantine): auto-delete siblings + cancel retry on approve (#920)
Multiple quarantine entries accumulate per track (one per retry attempt)
and approving one left the others behind and kept the retry running.

- Approve now sends remove_siblings=true — backend sibling deletion was
  already implemented but the flag was never passed from the UI
- Toast message now reports how many duplicate candidates were removed
- After approve, the backend cancels any in-flight quarantine-retry task
  for the same track (matched by title) so the engine stops fetching new
  candidates once the user has already accepted one
- Approve All also sends remove_siblings=true

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:37:33 +02:00
dev
55f7176c34 fix(quarantine): auto-refresh panel during batch + quarantine retry badge
Quarantine list was only fetched once on page load — new entries created
during a running batch never appeared without a manual tab click.

- _adlFetch (2 s poller) now also calls _verifLoadQuarantine(true) every
  7th poll (~14 s) so quarantine entries appear shortly after they land.

- Retry badge now shows 🛡 and a clearer tooltip when retry_trigger is
  'acoustid' or 'acoustid_unverified', making it visible during the
  quarantine-retry cycle that a previous candidate was quarantined.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:27:05 +02:00
BoulderBadgeDad
d647fc8ad1 Release 2.7.7: version bump + What's New / version modal + PR description + docker-publish default tag
- _SOULSYNC_BASE_VERSION -> 2.7.7; docker-publish default version_tag -> 2.7.7
- pr_description.md rewritten for 2.7.7 (the #915 primary-source parity headline, #913 listening-recs
  foundation, jellyfin atomic-write, and the #905/#908/#909/#910/#911/#912/#914/#916/#917/#918 batch)
- WHATS_NEW: replaced the 2.7.6 block with 2.7.7 (current release + a brief 'earlier versions' summary)
- VERSION_MODAL_SECTIONS: promoted the 2.7.7 highlights, rolled 2.7.6 into the 'Earlier' aggregator
2026-06-23 23:24:39 -07:00