Commit graph

991 commits

Author SHA1 Message Date
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
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
e8094c2218 Fix genre whitelist settings not saving — add to allowed config keys
The genre_whitelist key was missing from the settings POST handler's
service whitelist, so the config was silently dropped on save.
2026-04-18 20:26:37 -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
62251a66bf Improve deep scan logging — show existing track counts, not just new
Per-artist log lines now show the full details string from the worker
(e.g. "5 albums, 0 new tracks (150 existing updated)") instead of
just "5 albums, 0 tracks". Finished message shows "library up to date"
when no new content is found instead of "0 successful, 0 failed".
2026-04-18 19:10:35 -07:00
Broque Thomas
a02266596a Add Full Refresh support for SoulSync standalone mode
Full Refresh now clears all soulsync library records and rebuilds from
file tags in the output folder. Reads tags via Mutagen, groups by
artist/album, creates DB records with stable IDs. Files stay in place.
Previously Full Refresh did nothing for standalone — just returned.
2026-04-18 18:29:02 -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
2c15d50bff Fix metadata enhancement crash when album_info is None
Playlist and single track downloads pass None as album_info to
_enhance_file_metadata. The downstream _extract_spotify_metadata
called .get() on it without a null guard, crashing with AttributeError.
2026-04-18 18:12:39 -07:00
Broque Thomas
f5a2d51d4e Speed up standalone verify button — stop counting after 10 files
Was walking the entire directory tree counting up to 100 audio files,
taking 60+ seconds on large libraries. Now breaks after 10 files.
2026-04-18 17:54:37 -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
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
c473bf777c
Merge pull request #318 from kettui/feat/artist-discography-refactoring
Centralize metadata lookup for artist discography
2026-04-18 13:15:49 -07:00
Broque Thomas
1527fea5a1 Merge branch 'main' of https://github.com/Nezreka/SoulSync 2026-04-18 12:33:14 -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
b516f6e8ce Restore python web_server.py support alongside gunicorn
The gunicorn PR blocked direct Python execution with SystemExit.
Replaced with _DIRECT_RUN flag at top and startup block at bottom
so both paths work:
- python web_server.py (Werkzeug dev server, Windows compatible)
- gunicorn -c gunicorn.conf.py wsgi:application (production)
2026-04-18 12:03:12 -07:00
BoulderBadgeDad
64d87389d6
Merge pull request #315 from kettui/feat/gunicorn
Run SoulSync under Gunicorn
2026-04-18 11:48:48 -07:00
Antti Kettunen
3191f1fe3b Auto-reload static assets in development 2026-04-18 19:22:03 +03:00
Antti Kettunen
832bb07787 Prevent running web_server.py directly, enforce gunicorn
Should not support completely different ways of running the server, as that can lead to inconsistencies between environments / runtimes
2026-04-18 19:22:03 +03:00
Antti Kettunen
cb9a4b23b6 Clean up legacy env vars, print a warning log if running web_server directly 2026-04-18 19:22:03 +03:00
Antti Kettunen
8ff89c63a3 Add logging for slow requests 2026-04-18 19:22:03 +03:00
Antti Kettunen
14bc9b6fad Introduce Gunicorn production runner
Switch the web UI from Werkzeug's built-in server to Gunicorn for a more stable production deployment path.

Keep a separate dev config so local runs still reload quickly, while the production path uses a dedicated WSGI entrypoint and cleaner startup behavior.

The main motivation is to reduce the websocket teardown noise and make the server behavior more predictable under the app's mostly background-driven workload.
2026-04-18 19:21:53 +03:00
Broque Thomas
8df5cf3be0 Fix lossy copy conversion failing on FLACs with embedded cover art
Added -vn flag to all codec ffmpeg commands (MP3, Opus, AAC) to strip
video/image streams during conversion. Embedded cover art in FLAC
files caused ffmpeg to fail when the output muxer couldn't handle
the image stream, producing 0KB output files. Cover art is
re-embedded afterwards by Mutagen.
2026-04-18 08:57:21 -07:00
Broque Thomas
9369924968 Fix discography dedup dropping studio albums for compilations
The dedup key (normalized_title, year) caused different albums from
the same year to collide when title normalization stripped too much.
The "prefer more tracks" logic then kept compilations over studio
albums.

Two fixes:
- Title similarity check: if normalized titles are <85% similar,
  they're different albums, not variants — keep both
- Compilation deprioritization: studio albums win over compilations
  and "best of" collections when they do collide
2026-04-18 08:53:46 -07:00
Antti Kettunen
cca396e7bd Centralize Hydrabase enablement
Move Hydrabase availability checks into metadata_service so source resolution owns the policy. Keep web_server delegating to the centralized helper and add tests for the enabled/disabled cases.
2026-04-18 18:47:54 +03:00
Antti Kettunen
2b575a59ae Refactor artist discography lookup
Move artist discography resolution into core metadata_service, introduce MetadataLookupOptions, and keep web_server focused on request handling. Add focused tests for the new service boundary and preserve current fallback behavior for now.
2026-04-18 18:41:31 +03:00
Antti Kettunen
1905678c7b Refactor get_artist_discography to respect metadata provider priority 2026-04-18 16:24:59 +03:00
Antti Kettunen
e687486561 Add caching for enhanced search, don't attempt searches with 2 characters or less 2026-04-18 15:06:00 +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
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
b78616431a Add SoulSync standalone deep scan and skip incremental scan
Deep scan for standalone mode:
- Scans Transfer folder for all audio files
- Compares against soulsync DB records by file_path
- Moves untracked files to Staging for auto-import processing
- Removes stale DB records where files no longer exist
- Cleans orphaned albums and artists with no tracks

Incremental scan skips for standalone — library updates at download
time, no periodic scanning needed. Both changes are purely additive
and only activate when server_type is 'soulsync'.
2026-04-18 01:17:32 -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
d688e7fa15 Update What's New and version modal with standalone library and import fixes 2026-04-17 23:29:04 -07:00
Broque Thomas
0b13a9d886 Fix single track completion check not filtering by active server
Single track ownership check was calling check_track_exists without
server_source, matching against all servers instead of the active one.
Album and EP checks already passed server_source correctly — this was
the only missing spot. Affects all server types.
2026-04-17 22:50:39 -07:00
Broque Thomas
d916b01fd6 Fix soulsync library entries creating own artist/album records
Previously reused existing plex/jellyfin artist IDs, causing soulsync
tracks to be invisible on the library page (filtered by server_source).
Now always creates soulsync-specific artist and album records with
server_source='soulsync', avoiding PK collisions with hash suffixes.
2026-04-17 22:14:54 -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
43dedeb2ee Add SoulSync standalone library — no media server required
New 'soulsync' media server option manages the library directly from
the filesystem, bypassing Plex/Jellyfin/Navidrome entirely.

Two paths populate the library:
1. Downloads/imports write artist/album/track to DB immediately at
   post-processing completion, with pre-populated enrichment IDs
   (Spotify, Deezer, MusicBrainz) so workers skip re-discovery
2. soulsync_client.py scans Transfer folder for incremental/deep scan
   via DatabaseUpdateWorker (same interface as server clients)

New files:
- core/soulsync_client.py: filesystem scanner implementing the same
  interface as Plex/Jellyfin/Navidrome clients. Recursive folder scan,
  Mutagen tag reading, artist/album/track grouping, hash-based stable
  IDs, incremental scan by modification time.

Modified:
- web_server.py: _record_soulsync_library_entry() at post-processing
  completion, client init, scan endpoint integration, status endpoint,
  web_scan_manager media_clients dict, test-connection cache updates
- config/settings.py: accept 'soulsync' in set_active_media_server,
  get_active_media_server_config, is_configured, validate_config
- core/web_scan_manager.py: add soulsync to server_client_map

Dedup: checks existing artist/album by name across ALL server sources
before inserting to avoid duplicates. Enrichment IDs only written when
the column is empty (won't overwrite existing data).
2026-04-17 20:34:55 -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
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
f0ccd197e5 Fix lossy copy error logging hiding actual ffmpeg error
The ffmpeg failure log was truncated to 200 chars which only showed the
version banner, hiding the actual error message. Now strips the banner
preamble and shows up to 500 chars of the real error with exit code.
Also cleans up 0KB output files left behind when ffmpeg fails.
2026-04-17 13:17:16 -07:00
BoulderBadgeDad
0cf5cbe9cd
Merge pull request #311 from kettui/feat/remove-desktop-app
Remove desktop app, clean up test files, remove unused dependencies
2026-04-17 12:52:10 -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
Broque Thomas
6989701d65 Include album name in Soulseek search queries
Priority 0 query (artist + album + title) was gated behind a download
mode check that excluded Soulseek, the source that benefits most from
it. Soulseek searches match against file paths where users organize as
Artist/Album/Track — without the album name, ambiguous artist names
could match wrong-artist results (e.g. "Bleakness" as an album folder
instead of an artist). Removed the mode gate so all sources get the
most specific query first.
2026-04-17 11:12:07 -07:00
Antti Kettunen
64d9db57ea More dead code removal after desktop app removal 2026-04-17 20:06:24 +03:00
Broque Thomas
a4415db339 Skip slskd polling when Soulseek is not active or disconnected
Dashboard stats (every 10s) and download status endpoint were
unconditionally calling slskd transfers/downloads API, causing
connection timeout spam for users with a slskd URL configured but
using YouTube/Tidal/etc as their download source. Now checks both
download source mode and status cache before making the API call.
2026-04-17 09:35:18 -07:00
Broque Thomas
2429d87dbe Update What's New and version modal with recent features and fixes
Adds April 17 entries for Auto-Import, Wishlist Nebula, automation group
management, bidirectional artist sync, provider-agnostic discovery, live
sidebar badges, and critical source ID embedding fix. Version modal
reorganized to lead with current features and summarize earlier v2.2 work.
2026-04-17 07:46:07 -07:00
Broque Thomas
308773ea7c Add Auto-Import — background staging folder watcher with smart matching
Full auto-import pipeline: background worker watches the staging folder,
identifies music using embedded tags → folder name parsing → AcoustID
fingerprinting, matches files to metadata source tracklists, and
processes high-confidence matches through the existing post-processing
pipeline automatically.

Worker: AutoImportWorker with start/stop/pause/resume, configurable
scan interval (default 60s), confidence threshold (default 90%), and
auto-process toggle. Processes one folder per cycle, alphabetical
order. Disc folder detection, stability checking, content hash dedup.

Confidence gate: 90%+ auto-processes silently, 70-90% queued as
pending review with approve/dismiss actions, <70% flagged for manual
identification. Track matching uses weighted algorithm (title 45%,
artist 15%, track number 30%, album tag 10%).

Database: auto_import_history table tracks every scan result with
folder hash, match data JSON, confidence, status, timestamps.

API: 7 endpoints — status, toggle, settings (GET/POST), results
(filtered/paginated), approve, reject.

UI: Auto tab on Import page with enable toggle, confidence slider,
scan interval selector. Live result cards with album art, confidence
bar (green/yellow/red), status badges, match stats. 5-second polling.
2026-04-17 06:51:08 -07:00
Broque Thomas
a867bba18f Bidirectional artist sync, repair jobs grid, deezer column fix
Artist Sync button on enhanced library page now does true bidirectional
sync: Phase 1 pulls new albums/tracks from the media server using the
DatabaseUpdateWorker in deep scan mode (preserves enrichment), Phase 2
removes stale DB entries for files no longer on disk. Works for Plex,
Jellyfin, and Navidrome. Toast shows +albums, +tracks, -stale counts.

Repair jobs tab redesigned: 2-column grid layout with glass gradient
cards, accent top line on hover, hover lift effect, job description
text below name, running state with pulsing accent bar. Responsive
to single column under 900px.

Fixed deezer_artist_id → deezer_id column name on artists table lookup.
2026-04-16 19:13:03 -07:00
Broque Thomas
876c5665ad Fix 'no such column: deezer_artist_id' on enhanced library sync
The artists table uses 'deezer_id' but the enhanced library artist
lookup was querying 'deezer_artist_id' (the watchlist_artists column
name). Fixed to use the correct column name.
2026-04-16 18:14:40 -07:00
Broque Thomas
09d358ef69 Fix watchlist scan false failures, Spotify backfill, and wishlist remove
Watchlist scanner: empty discography (no new releases in lookback) was
treated as API failure, causing "Failed to get artist discography" for
artists like Kendrick Lamar who simply had no recent releases. Now
distinguishes None (API failure → try next source) from [] (success,
no new tracks). Spotify backfill now uses the authenticated client
instance instead of creating a fresh unauthenticated one.

Wishlist nebula: album remove now sends album_name (API updated to
accept album_name as fallback alongside album_id). Track remove
re-renders the nebula after deletion. Toned down processing pulse
animation.

Updated test to verify fallback triggers on API failure (None), not
on empty results.
2026-04-16 18:06:45 -07:00
Broque Thomas
85b470809e Add automation group management — rename, delete, bulk toggle, drag-drop
Full automation page upgrade with group management and drag-and-drop:

Backend: batch_update_group() and bulk_set_enabled() DB methods, new
PUT /api/automations/group and POST /api/automations/bulk-toggle endpoints.

Group headers: rename (inline edit), delete (choice dialog — keep
automations or delete all), bulk toggle (enable/disable all in group).
Actions appear on hover, styled as small icon buttons.

Drag and drop: non-system cards are draggable between group sections.
Drop zones show dashed accent border feedback. Collapsed sections
auto-expand on 500ms drag-hover. System/Hub sections dimmed during drag.
dragenter counter pattern handles child element bubbling.

Delete group dialog: glass card modal with three options — keep
automations (move to My Automations), delete everything, or cancel.
2026-04-16 14:19:13 -07:00
Broque Thomas
78db3fda2b Fix source ID embedding broken by missing context parameter
The MusicBrainz consistency change referenced 'context' inside
_embed_source_ids(), but that variable was never passed to the function.
Every download since that commit silently skipped ALL source ID tags
(Spotify, MusicBrainz, Deezer, AudioDB, Tidal, Qobuz, Last.fm, Genius)
with the error 'name context is not defined' caught as non-fatal.

Fix: pass context from _enhance_file_metadata to _embed_source_ids,
with None default for backward compatibility.
2026-04-16 12:58:57 -07:00
Broque Thomas
d9b4e5b853 Add smart Library Status card to Dashboard with deep scan support
Adaptive card on the Dashboard showing library state with four modes:
- No server: gold accent, directs to Settings
- Disconnected: gold warning with troubleshooting guidance
- Empty library: blue accent with prominent Scan Now button
- Healthy: green accent with stats grid (artists/albums/tracks/DB size),
  Refresh button (incremental) and Deep Scan button (full re-check)

Stats displayed as mini cards with individual icons. Animated glow orb,
gradient accent top line, shimmer progress bar during scans. Deep scan
added to /api/database/update endpoint (deep_scan flag) — re-checks
every track, adds new ones, removes stale, preserves enrichment data.
Confirmation dialog explains what deep scan does before starting.
2026-04-16 11:45:29 -07:00
Broque Thomas
0b60986f44 Wipe source tags when metadata enhancement is skipped or fails
Soulseek source files often carry the uploader's MusicBrainz IDs from
different releases. When post-processing skipped tag clearing (missing
spotify_album context in wishlist batches, or enhancement exceptions),
these conflicting IDs persisted and caused Navidrome to split one album
into multiple entries.

Added _wipe_source_tags() — a lightweight emergency tag wipe that clears
all tags without writing new ones. Called in every failure/skip path:
stream processor exception, playlist mode exception, verification worker
missing context, and verification worker exception. Idempotent and
wrapped in try/except so it never interferes with the existing flow.
2026-04-16 09:45:15 -07:00
Broque Thomas
1c8a25cff9 Fix 'Delete File Too' silently failing when file path cannot be resolved
When the DB stored a path the resolver couldn't map to a local file
(common with Navidrome virtual paths or Docker path mismatches), file
deletion was silently skipped — the DB record was removed but the file
stayed on disk with no indication to the user.

Now logs the resolution failure with the stored path, returns a
file_error in the API response, and the frontend shows a warning toast
explaining the file wasn't deleted plus a second toast with the specific
reason (e.g. Navidrome 'Report Real Path' instructions).
2026-04-16 08:43:16 -07:00
BoulderBadgeDad
4dab3de2d6
Merge pull request #303 from kettui/fix/watchlist-scanner-fixes
Consolidate watchlist scanning code, respect primary metadata provider
2026-04-16 08:29:22 -07:00
Antti Kettunen
40f39604ad Restore Last.fm radio after watchlist scans
Keep the weekly Last.fm radio generation step in the web watchlist scan post-processing chain so the higher-level scan behavior stays intact after moving the scan loop into the shared scanner core.
2026-04-16 08:35:27 +03:00
Antti Kettunen
9d73b8b561 Restore placeholder filtering and shared image backfill
Bring placeholder tracklist skipping back into the shared watchlist scan path, and centralize the DB-only artist image backfill helper so both web scan entrypoints reuse the same logic.
2026-04-16 08:31:04 +03:00
Antti Kettunen
40fa139804 Remove dead watchlist scan paths
Drop the legacy watchlist scan entrypoints that are no longer used by the web scan flow, and keep the live refresh path pointed at the shared scanner helper.
2026-04-16 08:27:41 +03:00
Antti Kettunen
657d86cace Consolidate web watchlist scanning
Move the shared watchlist scan loop into core/watchlist_scanner.py so web_server.py only handles triggers, locks, progress, and post-scan orchestration.

Manual and scheduled watchlist scans now share the same scanner-side core, while the web entrypoints keep profile selection and automation progress updates.
2026-04-16 08:20:48 +03:00
Broque Thomas
316d4cb466 Picard-style MusicBrainz album consistency for tag embedding
Recording MBIDs are now pulled from the matched release tracklist instead
of independent match_recording() searches, guaranteeing the recording ID
is consistent with the selected release. Batch-level artist name is used
for release cache keys so all tracks hit the same preflight-cached entry
even when Soulseek metadata spells the artist differently. A post-batch
consistency pass (run_album_consistency) rewrites album-level tags on all
files after the batch completes — the safety net that prevents Navidrome
album splits even when per-track lookups drift.
2026-04-15 21:34:24 -07:00
Broque Thomas
8866c4654b Add inbound music request API and webhook automation trigger
New POST /api/v1/request endpoint accepts a search query from external
sources (Discord bots, Home Assistant, curl) and triggers the
search-match-download pipeline asynchronously. Returns a request_id
for status polling via GET /api/v1/request/<id>. Optional notify_url
for callback on completion.

Also adds webhook_received trigger type and search_and_download action
type to the automation engine, so users can build custom flows like
"when webhook received → search & download → notify Discord".

Includes info panel in Settings showing endpoint URL and curl example.
2026-04-15 20:35:39 -07:00
Broque Thomas
41b5cd1f34 Fix allow_duplicate_tracks setting not saving and wishlist dropping cross-album tracks
Two bugs: (1) 'wishlist' was missing from the settings save whitelist,
so the toggle silently reset to ON on every page reload. (2) The
wishlist cleanup function unconditionally removed tracks sharing the
same name+artist regardless of album, ignoring the allow_duplicates
setting. Now when allow_duplicates is on, the dedup key includes the
album name so same song from different albums can coexist.
2026-04-15 19:39:34 -07:00
Broque Thomas
6677807b2a Add SOULSYNC_* env var dump at startup for Docker diagnostics
Prints all SOULSYNC_* environment variables before OAuth servers start,
helping diagnose reported Unraid issue where callback port env var is
present in the container but not seen by the Python process.
2026-04-15 18:57:15 -07:00
Broque Thomas
b89ff796bf Fix OAuth callback port hardcoding and add diagnostic logging
Auth instruction pages and log messages now use the actual configured
callback port instead of hardcoding 8888. Added startup logging that
prints whether SOULSYNC_SPOTIFY/TIDAL_CALLBACK_PORT env vars were
detected, helping diagnose Unraid/Docker env var issues. Also fixes
uses_main_port detection for custom callback ports and moves the
wishlist button handler to global init so it works on all pages.
2026-04-15 18:38:13 -07:00
Broque Thomas
aac75d6a3b Fix Explore tab checkmark badge not persisting after refresh
Explored status was stored only in frontend memory; on reload the badge
disappeared because the API never returned it. Added explored_at column
to mirrored_playlists (auto-migrated), written when build-tree completes,
and read back via SELECT * so the badge survives page refreshes.
2026-04-15 12:09:41 -07:00
Broque Thomas
71a56bf9b7 Fix playlist cover art not syncing to Plex/Jellyfin from Deezer and other sources
- Pass playlist image_url to _run_sync_task from all source-specific sync
  start handlers (Deezer, Tidal, Spotify public, YouTube, automation mirror)
  — previously only the /api/sync/start endpoint passed it
- Fix plex_client.set_playlist_image: use uploadPoster(url=) instead of
  uploadPoster(data=) which is not a valid PlexAPI argument
- deezer_client: use picture_xl > picture_big > picture_medium fallback
  for better cover art resolution
- tidal_client: extract image_url in get_playlist() from JSON:API
  relationships (was only extracted in metadata-only listing)
- parse_youtube_playlist: capture playlist thumbnail from yt-dlp result
- Add visible logging for image upload attempts and outcomes
2026-04-15 10:25:38 -07:00
Broque Thomas
fe399636b2 Fix Spotify API calls leaking when Deezer/iTunes is primary source
Spotify was being called for album/artist data fetching across multiple
background workers and the Artists page search even when the user had
Deezer or iTunes set as their primary metadata source. Being authenticated
for playlist sync was treated as permission to use Spotify for everything.

- watchlist_scanner: add _spotify_is_primary_source() that checks both
  auth and primary source config; use it for all album/artist data fetching
  (discovery pool, recent album caching, playlist curation, similar artist
  ID matching, proactive ID backfill). _spotify_available_for_run() is kept
  for sync_spotify_library_cache which must run regardless of primary source
- repair_jobs/metadata_gap_filler: gate Spotify ISRC lookup on primary
  source being 'spotify'; MusicBrainz lookup unaffected
- repair_jobs/unknown_artist_fixer: replace hardcoded spotify_client with
  source-aware client selection — primary source ID tried first, each ID
  matched to its correct client (fixes latent bug passing Deezer IDs to
  Spotify)
- web_server.py /api/match/search: Artists page search was hardcoded to
  spotify_client.search_artists(); now uses _get_metadata_fallback_client()
  so results come from the configured primary source
2026-04-15 09:47:43 -07:00
Broque Thomas
251c27e006 Add Last.fm Track Radio to Discover page
Adds a new Last.fm Radio section to the Discover page that lets users
search a track on Last.fm, generate a similar-tracks playlist, and run
it through the existing discovery/download/sync pipeline. Also generates
playlists automatically from top listening history during watchlist scans
(max once per week).

- core/lastfm_client.py: Add get_similar_tracks() using track.getsimilar
- core/listenbrainz_manager.py: Add save_lastfm_radio_playlist() with
  deterministic MBID (MD5 seed), cleanup limit of 5 for lastfm_radio type
- web_server.py: Add /api/lastfm/configured, /api/lastfm/search/tracks,
  /api/lastfm/radio/generate, /api/discover/listenbrainz/lastfm-radio;
  fix playlist['name'] KeyError in discovery worker that was resetting
  phase back to 'fresh' after completion
- core/watchlist_scanner.py: Add _generate_lastfm_radio_playlists() with
  weekly throttle, called at end of scan_all_watchlist_artists()
- webui/index.html: Add #lastfm-radio-section above ListenBrainz section,
  hidden unless Last.fm API key is configured
- webui/static/script.js: Search/generation/card-load functions; fix
  discovery modal labels (Last.fm Radio vs ListenBrainz), description
  update on completion, belt-and-suspenders completion handling inside
  updateYouTubeDiscoveryModal; fix album/duration display for tracks
  without metadata; music note SVG placeholder for missing art
- webui/static/style.css: Styles for search bar, dropdown, result rows
2026-04-14 23:41:36 -07:00
Broque Thomas
ce129010e1 Add ReplayGain analysis and tagging support
New core/replaygain.py module uses FFmpeg's ebur128 filter (already a
project dependency) to analyze integrated loudness and true peak, then
writes ReplayGain 2.0 tags (-18 LUFS reference) to MP3 (TXXX frames),
FLAC/OGG/Opus (Vorbis comments), and M4A/MP4 (freeform atoms).

Three analysis modes in the enhanced library view:
- Per-track RG button: synchronous single-track analysis (~1-3 s)
- Album "ReplayGain" button: background job writing both track gain
  and album gain (mean LUFS across all album tracks) to every file
- Bulk bar "ReplayGain" button: batch track-gain for selected tracks

read_file_tags() in tag_writer.py extended with four new optional keys
(replaygain_track_gain/_peak, replaygain_album_gain/_peak) so existing
RG values surface in the tag-preview diff view. Purely additive — no
existing endpoints or DB schema changed.
2026-04-14 19:09:25 -07:00
Broque Thomas
3db00ca7ef Allow flat single path templates with no subfolder
Singles could not be saved as a flat file (e.g. "$artist - $title")
because the frontend blocked any template without a "/" and the
backend path builder treated an empty folder_path as falsy, falling
through to the hardcoded nested-folder structure.

Frontend: removed the must-include-slash validation for single
templates only (album templates still require it).
Backend: changed condition from `if folder_path and filename_base`
to `if filename_base` so an empty folder_path is handled correctly
as a flat drop into the transfer root.
2026-04-14 17:42:54 -07:00
Broque Thomas
1071b2ebe5 Make OAuth callback ports configurable via environment variables
Hardcoded ports 8888/8889 conflict when SoulSync runs behind Gluetun or
other containers that claim those ports. Introduce SOULSYNC_SPOTIFY_CALLBACK_PORT
and SOULSYNC_TIDAL_CALLBACK_PORT env vars (defaulting to 8888/8889) so
users can remap without rebuilding the image.

docker-compose.yml exposes the vars with comments explaining how to keep
the port mappings in sync with the redirect URI in Settings → Connections.
2026-04-14 14:08:31 -07:00
Broque Thomas
6e405143a7 Improve Tidal download failure diagnostics and error messaging
Track per-quality-tier failure reasons across all failure paths (stream
error, empty manifest, download exception, stub file, MP4 extraction
failure) and include them in the exhausted-tiers log message so failures
are diagnosable from logs.

When HiRes is configured with no fallback and all tiers are exhausted,
log an actionable hint directing the user to enable Quality Fallback.

Surface Tidal-specific error messages in the UI task on retry
exhaustion: distinguishes HiRes-unavailable (with actionable guidance)
from general Tidal auth/quality failures, rather than showing the
generic Soulseek error string.
2026-04-14 14:07:50 -07:00
Broque Thomas
c1ef32acd2 Fix source-info popover showing no data due to path format mismatch
track_downloads stores local Windows paths but tracks table stores
server-side paths (Plex/Jellyfin). Both the track_id lookup (NULL
due to failed auto-link at insert time) and exact file_path fallback
were failing.

Added filename-suffix LIKE matching as a final fallback in
get_track_source_info, plus a back-link so the track_id gets written
back for fast future lookups. Also improved the auto-link in
record_track_download to use the same suffix matching when exact path
fails.
2026-04-14 12:30:39 -07:00
Broque Thomas
751024ec64 Fix M3U playlist export to use real library file paths
M3U entries now resolve actual file paths from the DB instead of
synthesising a fake 'Artist - Title.mp3' string that no media server
could use. Adds optional M3U Entry Base Path setting (Downloads tab)
so servers requiring absolute paths (e.g. /mnt/music) can be supported.

- New POST /api/generate-playlist-m3u endpoint: per-artist batch DB
  lookups with fuzzy title matching, prefixes entry_base_path when set
- autoSavePlaylistM3U and exportPlaylistAsM3U now call the new endpoint
- M3U Entry Base Path input added below Music Videos Dir in settings,
  follows path-input-group pattern with Unlock button and autosave
2026-04-14 12:07:35 -07:00
Broque Thomas
86621704fe Fix Discover synced playlists not appearing under Server Playlists
Server Playlists was filtered to only show playlists matching mirrored_playlists entries,
but Discover syncs are stored in sync_history (not mirrored_playlists), so they were
excluded. Adds GET /api/sync/history/names returning distinct synced playlist names,
and includes those in the filter alongside mirrored playlists.
2026-04-14 08:24:44 -07:00
Broque Thomas
61c6b6f3f9 Fix staging files bypassing path template
_try_staging_match() built a minimal context missing spotify_artist,
spotify_album, is_album_download, and has_clean_spotify_data. Post-
processing returned early at the missing-spotify_artist guard and the
copied file was left at the transfer root with its original filename.

Now mirrors the sync modal worker's context-building: uses
_explicit_album_context/_explicit_artist_context when available
(artist-page album downloads), falls back to track.album/track.artists
for playlists and sync modal. track_number and disc_number are also
forwarded so multi-disc albums land in the correct Disc N/ subfolder.
2026-04-14 08:19:10 -07:00
Broque Thomas
ab9064d3f6 Fix import pipeline not placing multi-disc albums into Disc N/ subfolders
import_album_process never computed total_discs, so spotify_album.total_discs
always defaulted to 1 in _build_final_path_for_track. Now pre-computes
total_discs from the matched tracks before the per-track loop.
2026-04-14 08:04:15 -07:00
Broque Thomas
3b8b369492 Add Your Albums — multi-source liked albums pool (Spotify, Tidal, Deezer)
Builds a new Your Albums section on the Discover page that aggregates
saved/liked albums from all connected services, mirroring the Your Artists
pattern. Deezer works via both OAuth and ARL.

- tidal_client: add get_favorite_albums() with V2/V1 API fallback
- deezer_client: add get_user_favorite_albums() via OAuth (user/me/albums)
- deezer_download_client: add get_user_favorite_albums() via ARL session
- music_database: add liked_albums_pool table (deduped by artist::album
  normalized key), upsert_liked_album, get_liked_albums,
  get_liked_albums_last_fetch, clear_liked_albums
- web_server: GET /api/discover/your-albums (ownership-checked, paginated),
  GET /api/discover/your-albums/sources, POST /api/discover/your-albums/refresh,
  _fetch_liked_albums background worker (Spotify + Tidal + Deezer OAuth/ARL)
- frontend: Your Albums section with source selector cog, album grid reusing
  spotify-library-card styles, search/filter/sort/pagination, download missing
  button, auto-refresh poll on first load

Also fix: Deezer greyed out in Your Artists sources when using ARL — connection
check now accepts ARL auth (deezer_dl.is_authenticated()) in addition to OAuth,
and _fetch_and_match_liked_artists falls back to ARL client for artist fetching.
2026-04-13 23:02:37 -07:00
Broque Thomas
453eb90f19 Add Deezer ARL favorite artists support
Add get_user_favorite_artists(limit=200) to DeezerDownloadClient to fetch a user's favorite artists via the public API using an ARL-authenticated session (paginated, error-handled, returns deezer_id, name, image_url).

Update web_server to treat Deezer as connected if either OAuth or ARL is authenticated, and to fetch favorite artists from OAuth client when available or from soulseek_client.deezer_dl (ARL) otherwise. Fetched artists are upserted into the database and appropriate log/console messages and counters are updated.
2026-04-13 19:46:39 -07:00
Broque Thomas
1fbc699879 Fix server playlist Find & Add not persisting to Plex
Three issues fixed:

1. Plex add-track used delete+recreate (Playlist.create) which was
   unreliable — switched to addItems() which atomically appends the
   track without touching existing playlist items.

2. After a successful add, the UI only did an optimistic local update.
   On reopen the automatic matcher ran fresh and couldn't connect the
   manually selected track to the source slot, making it look unfixed.
   Now both add and replace re-fetch the compare view from the server
   so the matcher sees the actual updated Plex state.

3. Matching algorithm was too strict for common title variants. Added
   _norm_title() which strips feat./ft., remaster/remastered, and
   edition qualifiers before comparison — so "Boy 1904" matches
   "Boy 1904 (2019 Remaster)" and "Float Away" matches "Float Away
   (feat. Flamingosis & Eric Benny Bloom)". Display titles unchanged.
2026-04-13 19:09:25 -07:00
Broque Thomas
20c8bff85c Upgrade artwork to highest available resolution for Spotify and iTunes
Spotify album art: replace the 4-char size segment after '0000ab67616d'
with '82c1' to request the original uploaded master (up to 2000px+).
Applied via _upgrade_spotify_image_url() in Track, Artist, and Album
dataclass constructors and as a catch-all in _download_cover_art.
Scoped to the ab67616d album art prefix only — artist images use a
different prefix (ab676161) where the trick does not apply.

iTunes/Apple Music: replace '100x100bb' with '3000x3000bb' in all
artworkUrl100 replacements across Track, Artist, Album, and the
get_album images arrays. Also applied as a catch-all in _download_cover_art.

Deezer already uses cover_xl at its maximum — no changes needed there.
2026-04-13 15:04:37 -07:00
Broque Thomas
4cf18ad2c9 Fix service status not showing Spotify as active metadata source
The status cache's source field was being set by _get_metadata_fallback_source(),
which internally calls is_spotify_authenticated() a second time via module import.
Any inconsistency between that path and the direct auth check already performed
would return 'deezer', poisoning the cache — and since the WebSocket takes over
from the HTTP poll, it never corrected itself.

Now reads config directly and reuses the single auth probe result.
2026-04-13 14:50:49 -07:00
Broque Thomas
ff5684bded Add configurable sources for Your Artists section on Discover page
Gear button next to View All opens a sources modal letting users pick
which connected services (Spotify, Tidal, Last.fm, Deezer) contribute
artists to the Your Artists carousel. Setting saved via standard
/api/settings endpoint under discover.your_artists_sources.

- GET /api/discover/your-artists/sources returns enabled config + which
  services are currently connected
- _fetch_and_match_liked_artists skips sources not in the enabled list
- Disconnected services shown dimmed and non-interactive in modal
- Saving with nothing selected blocked with error toast
- Remove z-index from .sidebar-header (fixes artist map overlap)
- Add padding-bottom to #automations-list-view (search bar overlap fix)
2026-04-13 13:39:36 -07:00
Broque Thomas
352ad5ff68 Fix AcoustID test connection falsely failing for valid API keys
The fallback test (used when no audio files exist in the library) sends
a dummy fingerprint to the AcoustID API. The API correctly rejects the
dummy fingerprint but this is not error code 4 (invalid key), so the
test was returning False instead of True. Any non-code-4 error from the
fallback means the API key was accepted — only code 4 means a bad key.
2026-04-13 09:37:50 -07:00
Broque Thomas
e1c8620928 Record Spotify API calls via api_call_tracker
Instrument web_server.py to record Spotify API usage by calling core.api_call_tracker.api_call_tracker.record_call before various Spotify SDK calls. Added local imports and record_call invocations around artist, playlist, playlist_tracks_page, current_user_saved_tracks, artists_batch, track and related endpoints across functions such as get_artist_image, get_playlist_tracks, watchlist_artist_config, enrich_similar_artists, get_your_artist_info, get_artist_map_explore, import_album_process and import_singles_process. This enables per-endpoint monitoring and helps with rate-limit tracking while keeping the import local to avoid top-level dependency issues.
2026-04-13 08:29:20 -07:00
Broque Thomas
0edd8f5c81 Raise artist discography limit from 50 to 200 with Deezer pagination
Deezer and iTunes defaulted to 50 albums max, silently truncating large
discographies. Deezer now paginates (100 per page) up to 200. iTunes
raised to 200 (single call). All callers in web_server.py updated to
use the new defaults instead of hardcoding limit=50.

Also adds diagnostic logging for allow_duplicates album comparison
to help debug inconsistent singles behavior.
2026-04-12 21:46:04 -07:00
Broque Thomas
26b2ca60fc Bump to v2.3, sticky sidebar header, compact idle player, Plex fixes
Version bump to 2.3 with rewritten What's New modal covering all
changes since v2.2. Docker publish workflow default updated.

Sidebar improvements:
- Header stays pinned at top while nav and player scroll beneath it
- Media player collapses to compact single-line when no track is
  playing, expands to full size when playback starts

Fixes:
- Server playlists endpoint Plex Tag object crash (getattr fix)
- Server playlists tab auto-refreshes after download completion
- Fixed dead code syntax error in archived version notes
2026-04-12 14:43:18 -07:00
BoulderBadgeDad
4f25cf4661
Merge pull request #286 from kettui/fix/improve-graceful-shutdown
Improve shutdown procedures so that the application can close gracefully
2026-04-12 12:42:00 -07:00
BoulderBadgeDad
187a925037
Merge pull request #283 from kettui/fix/logging-init
Setup logging early to prevent initial logs from being swallowed, fix db init operations
2026-04-12 12:27:29 -07:00
Broque Thomas
3d26c2b140 Fix [object Object] in M3U files and database reference error
M3U generator was calling .join() on an array of artist objects instead
of extracting .name first, producing "[object Object] - Track Name".
Now handles all artist formats: array of objects, array of strings,
single string, single object.

Also fix "name 'database' is not defined" error when updating album
year in post-processing — was using bare 'database' instead of
get_database() helper.
2026-04-12 11:10:11 -07:00
Broque Thomas
1c1be6190a Fix Plex playlists crash on Tag objects and add Unknown Artist guard
Plex API can return Tag objects mixed with playlists — these lack the
playlistType attribute, causing AttributeError. Use getattr with safe
default instead of direct attribute access.

Add 3-tier Unknown Artist guard in post-processing: checks track_info
artists, original search result, then re-fetches from metadata API
before building folder paths or embedding tags. Prevents files from
landing in Unknown Artist folders when the download context has
incomplete artist data.
2026-04-12 10:43:51 -07:00
Antti Kettunen
7f5384a89e Add some more shutdown checks for websocket handlers 2026-04-12 18:30:11 +03:00
Antti Kettunen
29d964e8b0 Tighten up teardown logic
- Stop active DB update work before tearing down executor pools.
- Short-circuit scan completion callbacks during shutdown so in-flight timer ticks don’t queue follow-up work.
- Prevent the download monitor from draining deferred/completed tasks after shutdown starts.
- Make listening stats startup stop-aware so it exits cleanly if teardown begins during warmup.
- If a metadata update is already running, it can now observe should_stop and exit cleanly instead of continuing after SIGTERM.
2026-04-12 18:13:27 +03:00
Antti Kettunen
aec3047216 Improve graceful shutdown and rollback safety
- Add interruptible stop events to background workers so shutdown
  wakes out of long sleeps instead of waiting on fixed delays.
- Stop scan managers, repair worker, executors, and cleanup helpers
  deterministically so process exit does not leave background threads
  alive.
- Add startup warnings for stale SQLite WAL/SHM sidecars so unclean
  shutdowns are easier to spot before init/migration errors cascade.
- Prevent forced kills from leaving SQLite sidecars behind, which
  made rollbacks to older branches fail with malformed database
  errors.
2026-04-12 15:17:18 +03:00
Antti Kettunen
7a8cc854db Setup logging early to prevent initial logs from being swallowed
There's a lot side-effects happening at import-time (eg. client initialization etc.), some of which include meaningful logging as well (eg. migration-related logging in MusicDatabase client).

If we delay the logging initialization to the __main__ block, we'll lose out on these early logs
2026-04-12 11:45:55 +03:00