Commit graph

1948 commits

Author SHA1 Message Date
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
dev
e29cc641cb chore(ui): drop redundant per-source "quality is global" notes
Each streaming source (Tidal, Qobuz, HiFi, Deezer, Amazon) carried a
"X Download Quality: Quality is set globally in Quality Profile…" note. The
ranked-target profile already drives every source's tier via
quality_tier_for_source, so these were pure noise. Removed all five; the auth /
status / token fields in each container are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:13:17 +02: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
4d287c9699 fix(ui): shorten ranked-target group labels to "All lossless" / "All lossy"
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:27:32 +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