Commit graph

1822 commits

Author SHA1 Message Date
Broque Thomas
e7fe083099 Add per-profile Spotify credentials and server library selection
Complete end-to-end per-profile Spotify support:
- DB migration: 11 columns on profiles table (Spotify creds, Tidal
  tokens, server library IDs) with encryption
- API endpoints: save/load/delete Spotify creds + server library
- Per-profile Spotify client cache with separate OAuth token storage
- Profile-aware OAuth flow (auth initiation + callback via state param)
- Playlist listing endpoint uses per-profile client
- Frontend: Spotify + Server Library + ListenBrainz sections in
  personal settings modal (non-admin only)
- Admin users see zero change — fully backwards compatible
- Tidal per-profile deferred (needs device auth flow)
2026-03-20 11:49:30 -07:00
Broque Thomas
06caf43f2f Add per-profile Spotify client with cache and playlist endpoint wiring
get_spotify_client_for_profile() returns the right SpotifyClient for
the active profile. Admin and profiles without custom creds get the
global client (zero behavioral change). Profiles with custom creds
get a dedicated cached instance with separate OAuth token storage.

Playlist listing endpoint now uses per-profile client so non-admin
users with their own Spotify credentials see their own playlists.
2026-03-20 10:47:58 -07:00
Broque Thomas
efe4561bcb Add per-profile service credentials foundation (DB + API)
DB migration adds 11 columns to profiles table for per-profile Spotify
credentials, Tidal tokens, and media server library selection. All
encrypted, all default NULL (fall back to global config).

API endpoints follow existing ListenBrainz per-profile pattern:
- GET/POST/DELETE /api/profiles/me/spotify
- GET/POST /api/profiles/me/server-library

Foundation only — no frontend UI or client initialization changes yet.
2026-03-20 10:30:56 -07:00
Broque Thomas
be77397132 Fix enrichment workers never showing idle/complete status
Pending count queries included NULL-ID rows that _get_next_item filters
out, so pending stayed > 0 even when no processable items remained.
Workers reported running instead of idle, UI never turned green. Added
AND id IS NOT NULL to _count_pending_items across all 9 workers to
match the _get_next_item filter.
2026-03-20 10:07:27 -07:00
Broque Thomas
e0533215da Fix enrichment workers looping on tracks with NULL IDs
Workers would endlessly match the same track because UPDATE WHERE id =
NULL matches 0 rows in SQL. Added AND id IS NOT NULL to all enrichment
queries (individual, batch EXISTS, and batch fetch) across all 9
workers. Also added process-level guard for belt-and-suspenders safety.

Fix Deezer get_track → get_track_details method name mismatch.
2026-03-20 09:36:25 -07:00
Broque Thomas
81852caa59 Fix wishlist download UI showing status on wrong track rows
Stamp _original_index on tracks before submitting to download processor
so task indices match frontend modal row order. Sanitization and
filtering could reorder tracks, causing the backend to assign indices
that didn't match the displayed rows. Visual-only fix — actual
downloads were always correct.
2026-03-20 08:42:37 -07:00
Broque Thomas
81da57f306 Add min completion percentage filter to Album Completeness job
New setting: Min Completion % (default 0 = disabled). Skips albums
where the user has fewer than N% of tracks — filters out playlist
imports where a single track exists but the full album was never
intended to be downloaded. Catches real failed downloads (6/12)
while ignoring incidental singles (1/12).
2026-03-20 08:19:56 -07:00
Broque Thomas
67db796958 Fix Tidal token refresh hammering when client credentials removed
Skip refresh attempt if client_id/secret not configured — clears stale
tokens to stop retry loop permanently. Add 5-minute backoff after
failed refresh to prevent hammering Tidal API every 2 seconds.
2026-03-20 07:28:32 -07:00
Broque Thomas
e97dc8f86a Add 4 new automation pipelines and fix deploy list refresh
New pipelines: Startup Recovery (3 automations), Import Pipeline (3),
Weekly Deep Clean (5), Beatport Fresh (1). Fix deploy calling
nonexistent loadAutomationsPage — now calls loadAutomations so the
list updates immediately after deployment.
2026-03-20 07:23:57 -07:00
Broque Thomas
e8c26fb015 Revamp Automation Hub with one-click pipeline deployment
Add Pipelines tab with 7 pre-built automation groups that deploy
multiple linked automations in one click. Visual pipeline cards with
accent-colored gradients, connected flow nodes, and deploy buttons.

- Release Radar Sync, Discovery Weekly Sync, Playlist Auto-Sync:
  4-stage pipelines (refresh → discover → sync → download)
- New Music Pipeline: watchlist scan → download → cleanup → notify
- Nightly Operations: staggered time-based (1AM-4AM)
- Download Monitor: 3 notification automations for failures/quarantine/completion
- Library Guardian: quality scan → notify chain

Pipeline detail modal shows full automation breakdown with WHEN/DO/THEN
tags. Deploy prompts for notification config when pipeline includes
alert steps. All signal chains use unique prefixes to avoid collisions.
2026-03-20 00:17:30 -07:00
Broque Thomas
46f82027cb Allow re-sync from download_complete state and add Rediscover button
All 6 playlist sync endpoints now accept download_complete phase so
users can re-sync after downloading. Added Rediscover button to
discovered and download_complete states (YouTube + Beatport). Added
full button set (Sync, Download Missing, Rediscover) for
download_complete which previously showed no buttons at all.
2026-03-19 22:17:59 -07:00
Broque Thomas
6ee20973a8 Fix Album Completeness wishlist adding missing cover art and track data
Wishlist entries from auto-fill were missing album images, album ID,
track number, disc number, and total tracks. Downloads would have no
cover art and wrong file organization. Now includes full album context
matching the standard wishlist data format.
2026-03-19 22:05:06 -07:00
Broque Thomas
dadc489b08 Add tag-based fallback to orphan detector to prevent false positives
When suffix path matching fails, reads file tags (title+artist) and
checks against DB tracks. Prevents false orphan detection from path
mismatches in Docker where DB paths differ from filesystem paths.
Only runs for files that fail suffix matching — zero overhead in
normal cases.
2026-03-19 22:01:27 -07:00
Broque Thomas
c53583716a Fix batch_complete firing with 0 downloads and album ID int cast error
- Only emit batch_complete automation event when successful_downloads > 0
- Remove int() cast on album_id in _fix_incomplete_album — Plex/Jellyfin
  use hash/text IDs that SQLite stores as-is in INTEGER columns
- Use string comparison for album_id equality check
2026-03-19 19:48:06 -07:00
Broque Thomas
1b95c05041 Fix Track Number Repair findings returning 400 on fix
Finding was created with entity_id=None (file-based) but fix handler
required entity_id for DB update. Rewrote handler to work with file_path
as primary — writes corrected track number to file tags, renames file
if needed, updates DB path. Also stores total_tracks in finding details
for correct tag writing.
2026-03-19 19:21:24 -07:00
Broque Thomas
fc4e16337a Redesign hybrid mode with N-source priority ordering
Replace fixed primary/secondary hybrid dropdowns with an ordered list
of all 5 download sources. Users enable/disable each source and reorder
with up/down arrows to set download priority. Sources are tried in
order until one returns results.

- New hybrid_order config field (backward compat with legacy primary/secondary)
- Download orchestrator loops ordered list with per-source error handling
- Sortable source list UI with icons, toggle switches, priority numbers
- Source-specific settings shown for all enabled hybrid sources
- Seamless migration from legacy 2-source to N-source format
2026-03-19 18:51:52 -07:00
Broque Thomas
3293a476fc Check staging folder for existing files before downloading
Before searching Soulseek/YouTube, checks the staging folder for a
matching file using tag-based or filename-based fuzzy matching. On match,
copies the file to the transfer folder and runs normal post-processing
(tagging, AcoustID, path organization). Staging scan is cached per batch
to avoid re-walking for every track.
2026-03-19 16:51:10 -07:00
Broque Thomas
42b457f9d6 Redesign settings page with modern tabbed single-column layout
Replace 3-column glassmorphic card wall with centered single-column
tabbed interface. Horizontal pill tab bar (Connections, Downloads,
Library, Appearance, Advanced) with category switching.

- Kill glassmorphic cards, accent gradient bars, and box shadows
- Clean section headers with subtle dividers
- Horizontal setting rows (label left, control right)
- Custom styled select dropdowns with SVG arrow
- Quality Profile moved into Downloads tab (conditionally visible)
- Help text wraps to new line below controls
- Path inputs and template inputs properly styled
- Mobile responsive (rows stack, tab bar scrolls)
- Zero functional changes — all element IDs and JS logic preserved
2026-03-19 15:59:55 -07:00
Broque Thomas
e715ceef3e Add multi-source search tabs for enhanced search (Spotify/iTunes/Deezer)
Search results now show switchable tabs for alternate metadata sources.
Primary source renders immediately, alternate sources load in parallel
and tabs appear progressively as each completes.

- New /api/enhanced-search/source/<name> endpoint for per-source queries
- Source-aware routing via ?source= param on discography, album tracks,
  album detail, and artist image endpoints (prevents numeric ID
  misrouting between iTunes and Deezer)
- Source override stored on artistsPageState for consistent navigation
- Tabs styled with source brand colors, show result counts
- All additive — users who ignore tabs see zero behavioral change
2026-03-19 13:23:43 -07:00
Broque Thomas
b9c83a50fa Add Soulseek peer queue filtering and configurable download timeout
- Add max_peer_queue setting to skip peers with long queues (soft filter
  with fallback to unfiltered if all results removed)
- Add download_timeout setting replacing hardcoded 10-minute limit
- Include quality_score (peer health: upload speed, free slots, queue
  length) in result ranking — was calculated but never used in sort key
- New UI controls in Soulseek settings section
2026-03-19 11:47:37 -07:00
Broque Thomas
f0270ce7a5 Expand Album Completeness to support iTunes and Deezer sources
Was Spotify-only — users without Spotify got zero results. Now queries
albums with any source ID (spotify_album_id, itunes_album_id, deezer_id)
and uses the matching API client for track count and missing track lookup.
Falls back gracefully across sources with client-type detection.
2026-03-19 11:27:08 -07:00
Broque Thomas
e2345c659d Add mass orphan safety guard to prevent accidental library deletion
When >50% of files are flagged as orphans (likely a DB path mismatch),
findings are marked as warnings with mass_orphan flag. Fixing these
requires typing "witness me" to confirm — prevents nuking an entire
library from a false-positive orphan scan.
2026-03-19 11:04:11 -07:00
Broque Thomas
ade189fa38 Fix Library Reorganize producing (_) in paths when year is empty
_sanitize_context_values passed empty strings through _sanitize_filename
which converts '' to '_' (empty-name fallback). Template $album ($year)
became Album (_) instead of Album () which the cleanup regex couldn't
match. Now preserves empty strings so the existing () cleanup works.
2026-03-19 10:36:21 -07:00
Broque Thomas
11b1222c90 Fix YouTube playlist parsing capped at ~100 tracks
Force full playlist resolution with lazy_playlist=False and materialize
entries list to prevent yt-dlp from stopping at the first page of results.
2026-03-19 09:47:12 -07:00
Broque Thomas
4eb028a7ce Add fix handler for Library Reorganize findings and expand year sources
- Add _fix_path_mismatch handler so fixing Library Reorganize dry-run
  findings actually moves files (was returning 400 with no handler)
- Add path_mismatch to fixable_types and _execute_fix handler map
- Add recent_releases and wishlist_tracks as year sources in
  _load_album_years to cover more playlist-synced tracks missing years
- Add sys and json imports needed by new code
2026-03-19 09:24:09 -07:00
Broque Thomas
0de8841b14 Fix bulk Fix All ignoring Single/Album Dedup findings and expand version keywords
- Add single_album_redundant to fixable_types in bulk_fix_findings so
  Fix All actually includes these findings (Fix Selected worked, Fix All
  silently returned 0)
- Expand version keyword regex from 9 to 25 terms (remastered, deluxe,
  unplugged, etc.) to reduce false positives in Single/Album Dedup
- Add word boundary anchors to prevent substring matches (e.g. "live"
  inside "Alive", "edit" inside "Meditate")
- Cast similarity thresholds to float for config type safety
2026-03-19 08:53:14 -07:00
Broque Thomas
8754e160b0 Fix Album Completeness job scanning no albums due to wrong HAVING filter
The SQL HAVING clause filtered on local track count instead of expected
track count, excluding albums with fewer than 3 local tracks from the
scan entirely. Now fetches all Spotify-matched albums and filters by
expected track count in the loop.
2026-03-19 08:36:15 -07:00
Broque Thomas
99481a0232 Fix Track Match search ignoring Track/Artist fields and low result limit
- Frontend was concatenating Track and Artist inputs into a single
  query string, causing Spotify to return mixed results matching
  either word in any field. Now sends track and artist as separate
  params; backend builds field-filtered query (track:X artist:Y).
- Result limit was silently capped at 10 in spotify_client.search_tracks
  via min(limit, 10). Raised to respect requested limit up to
  Spotify's API max of 50.
- iTunes fallback endpoint updated with same field-specific params.
- Legacy ?query= param still supported for backward compatibility.

Fixes #194
2026-03-19 08:23:19 -07:00
Broque Thomas
0a3cca4f1f Fix Spotify invalid_client and Tidal redirect URI mismatch on auth
- Duplicate `spotify` key in saveSettings() object literal caused
  second definition (embed_tags/tags) to silently overwrite the first
  (client_id/client_secret/redirect_uri), destroying credentials on
  every save. Merged into single key.
- authenticateSpotify() and authenticateTidal() now await saveSettings()
  before opening auth window, ensuring credentials are persisted.
- Tidal auth now dynamically sets redirect_uri from request host for
  LAN/Docker users and stores it in tidal_oauth_state so the callback
  token exchange uses the same URI.

Fixes #191
2026-03-19 08:02:12 -07:00
Broque Thomas
9a6ee0e229 Replace Inspiration templates with tabbed Automation Hub
Swap the flat 8-card Inspiration section for a rich 4-tab Automation Hub
(Recipes, Quick Start, Tips, Reference) with 20 categorized recipe cards,
5 step-by-step guides, 8 power-user tips, and full trigger/action/then
reference tables. Includes category + difficulty filters, chain flow
visuals, accordion expand/collapse, and responsive grid layouts.
2026-03-18 19:03:41 -07:00
Broque Thomas
6b5e37aded Add preflight track-hash comparison and fix duplicate sync history entries
Preflight: hash track IDs before syncing and compare against last sync.
Skip only if exact same tracks were already synced and all matched.
Replaces the old count-based smart-skip which could miss track swaps.

Sync history: update existing entry for same playlist_id instead of
creating duplicates. Re-syncing the same playlist now refreshes the
existing row with new timestamps and stats.
2026-03-18 18:17:50 -07:00
Broque Thomas
5489af2647 Add defensive album data repair in wishlist add_to_wishlist
If album info is missing, not a dict, or named "Unknown Album",
it gets repaired using the track name as fallback instead of
storing junk display data.
2026-03-18 17:57:33 -07:00
Broque Thomas
0dd0d50837 Fix Single/Album Dedup flagging EP tracks as redundant singles
EPs are no longer classified as singles — removing EP tracks would
break the release and conflict with album completeness checks.
Only actual singles (album_type='single' or unknown type with <=2
tracks) are flagged as redundant when the same song exists on an album.
2026-03-18 17:48:01 -07:00
Broque Thomas
e1b203405f Add ignore cross-album duplicates setting to Duplicate Detector
Tracks on different albums (e.g., same song on a studio album and a
compilation) are no longer flagged as duplicates by default, keeping
albums complete. Setting is enabled by default and configurable via
the job settings UI.
2026-03-18 17:42:49 -07:00
Broque Thomas
0428a4565d Deduplicate variant album releases in artist discography view
Metadata sources (Deezer/iTunes/Spotify) return separate entries for
Explicit/Clean/Remastered variants of the same album. Normalize titles
by stripping variant suffixes and group by name+year, keeping the
version with the most tracks or preferring explicit.
2026-03-18 17:22:39 -07:00
Broque Thomas
e4579b4f2a Fix wishlist items from playlist sync missing album name and cover art
Playlist refresh was storing matched_data.album as a bare string with
no images dict. When unmatched tracks were added to the wishlist, they
showed as 'Unknown Album' with no cover art. Now all three playlist
import paths (authenticated API, public scraper, spotify_public) store
album as a proper dict with name and images array, matching the format
used by the discovery worker.
2026-03-18 17:11:26 -07:00
Broque Thomas
7b3bfd8db4 Fix artist discography miscategorizing albums as singles/EPs
Track count heuristics were overriding the album_type field from
Spotify/iTunes/Deezer, forcing any release with <=3 tracks into
singles and 4-6 tracks into EPs regardless of actual album type.
Now trusts the source's album_type directly.
2026-03-18 16:52:27 -07:00
Broque Thomas
b9732156cd Add per-tag granular toggle settings for all 47 embedded tags
Replace category-based tag settings (10 toggles) with per-tag controls
grouped by source service in an accordion UI. Each of the 11 service
groups (Spotify, iTunes, MusicBrainz, Deezer, AudioDB, Tidal, Qobuz,
Last.fm, Genius, General) has a master toggle that disables all child
tags, with individual toggles for fine-grained control. ISRC and
copyright fallback chains are now per-source toggleable. Genre merge
contributions from each source are independently controllable. All
tags default to enabled for backward compatibility.
2026-03-18 16:17:20 -07:00
Broque Thomas
d3bff90fd6 Add Picard-compatible MusicBrainz tags and per-category tag settings
Post-processing now writes all 18 MusicBrainz tags that Picard writes:
Release Group ID, Album Artist ID, Release Track ID, Release Type,
Status, Country, Original Date, Media, Barcode, Catalog #, ASIN,
Script, Total Discs (plus the 5 already supported). One cached API
call per album via get_release with recordings include.

New "Tags to Embed" settings section with 10 category toggles (all
enabled by default): MusicBrainz IDs, Release Info, Source IDs, ISRC,
BPM, Mood & Style, Copyright & Label, Genre Merging, URLs, Quality.
Each shows inline description of what it includes.
2026-03-18 15:49:36 -07:00
Broque Thomas
5adfdad9a3 Speed up dashboard polling intervals for more responsive UI
Service status 10s→5s, activity feed 5s→2s, watchlist/wishlist/db stats 30s→10s.
2026-03-18 15:02:00 -07:00
Broque Thomas
ebc0713348 Add API year lookup fallback for Library Reorganize job
Playlist-synced tracks often have no year in file tags or the DB albums
table. Now checks discovery_pool release_date as a second DB source,
then batch-lookups remaining missing years from the user's active
metadata source (Spotify/iTunes/Deezer) capped at 50 API calls.
2026-03-18 14:49:35 -07:00
Broque Thomas
2c2c006f83 Fix wishlisted tracks showing 'Unknown Album' with no cover art
Multiple code paths built wishlist album data as a bare string instead
of a dict with images/album_type/total_tracks, causing wishlist entries
to lose album name and artwork. Fixed in: Beatport fallback conversion,
sync normalization, _ensure_spotify_track_format, and wishlist service
track reconstruction. Now uses track name as album fallback and always
includes required album dict keys.
2026-03-18 14:23:53 -07:00
Broque Thomas
4a7f297b2c Add minimum peer upload speed setting and fix quality scoring tiers
New dropdown in Soulseek settings lets users filter out slow peers at
search time (Any/1/2/3/4/5/10 Mbps). Passes minimumPeerUploadSpeed
to slskd API in bytes/sec.

Also fixes quality scoring tiers which were using wrong units — old
thresholds (5000, 1000, 500) treated bytes/sec values as kbps,
making speed scoring effectively meaningless. Now uses correct
bytes/sec thresholds based on real peer data.
2026-03-18 14:07:39 -07:00
Broque Thomas
b4da777bdc Fix automation group dropdown clipped at bottom of page
Dropdown always opened downward, getting cut off for the last row.
Now checks available space and flips upward when near viewport bottom.
2026-03-18 13:08:37 -07:00
Broque Thomas
e8e8a2e934 Fix deep scan skipping file path updates for existing tracks
Deep scan passed skip_existing_tracks=True which skipped calling
insert_or_update_media_track entirely for known tracks, so stale
file paths were never refreshed from the media server. Now always
calls insert_or_update (which safely uses UPDATE for existing tracks,
preserving enrichment data) so file paths stay in sync.
2026-03-18 13:03:01 -07:00
Broque Thomas
8abcf386d5 Fix duplicate detector ignoring similarity settings for remixes
Normalization was stripping parenthetical content before comparison,
so 'title' and 'title (xxx remix)' both became 'title' and always
matched at 1.0 regardless of the user's threshold setting.
2026-03-18 12:47:34 -07:00
Broque Thomas
2564e6bf4f Fix deep scan wiping track enrichment data and not updating file paths
INSERT OR REPLACE on existing tracks was deleting the entire row and
reinserting only 9 columns, nuking spotify_track_id, deezer_id, isrc,
bpm, musicbrainz IDs, and ~15 other enrichment columns every scan.

Now uses UPDATE for existing tracks (preserves all enrichment) and
INSERT only for new tracks. Also ensures file_path gets updated from
the media server on each scan, fixing stale paths for users whose
files were moved/reorganized.
2026-03-18 10:38:06 -07:00
Broque Thomas
f66eecebf5 Fix seasonal playlists not appearing when Deezer is active source
_get_source() hardcoded 'itunes' as fallback, so seasonal content was
stored with source='itunes' but API endpoints queried source='deezer' —
resulting in empty discover page seasons. Now reads the configured
fallback source from metadata_service.
2026-03-18 10:28:54 -07:00
Broque Thomas
a42927f369 Add re-download capability to dead file findings (#189)
Dead file Fix button now adds the track to wishlist for re-download
instead of just removing the DB entry. Builds full wishlist-compatible
track data from DB (artist, album, artwork, IDs) so the download
pipeline can process it like any other wishlist item.
2026-03-18 09:49:38 -07:00
Broque Thomas
2888a5dda1 Fix Soulseek test crash, dedup query, and reorganize missing year
- Add null guard for soulseek_client in test-connection endpoint (#190)
- Fix single_album_dedup query: use al.record_type and al.track_count
  (al.album_type and al.total_tracks don't exist on main albums table)
- Fix library reorganize missing year: pre-load album years from DB as
  fallback when file tags lack the date field (common with playlist syncs)
2026-03-18 09:09:45 -07:00