Commit graph

1631 commits

Author SHA1 Message Date
Broque Thomas
d75893bc30 Add Album Tag Consistency repair job: detect and fix inconsistent tags across album tracks
New maintenance job scans albums for tracks with mismatched album names, album
artist names, or MusicBrainz release IDs. These inconsistencies cause Navidrome
and other media servers to split one album into multiple entries. The fix
normalizes outlier tracks to the majority value by rewriting file tags.
2026-03-23 10:44:36 -07:00
Broque Thomas
68f06d663b Pause enrichment workers during database scans to reduce lock contention
All 11 background workers (9 enrichment + repair + SoulID) pause when a DB
scan starts and resume when it finishes. Tracks which workers were running
so manually-paused workers stay paused after scan. Covers incremental,
full refresh, deep scan, automation-triggered, and startup scans.
2026-03-23 10:33:10 -07:00
Broque Thomas
efe8164501 Fix missing album art on wishlist items from mirrored playlists
Embed scraper was pre-marking spotify_public tracks as discovered with
playlist-level images instead of per-track album art. Discover step then
skipped them entirely. Now stores spotify_hint (track ID) without marking
discovered, so Discover runs proper API lookups for real album art.
2026-03-23 09:22:49 -07:00
Broque Thomas
a48018f4ca Fix config save 'database is locked': add 30s timeout, WAL mode, and retry
Settings DB connections had no timeout (default 5s), causing lock failures when
enrichment workers hold concurrent write locks. Added 30s timeout, WAL journal
mode for better concurrency, retry-once before falling back to config.json.
2026-03-23 08:54:53 -07:00
Broque Thomas
df390feece Fix automation signal chain: forward event data (playlist_id) to action handlers
Event-triggered automations now receive playlist_id from the triggering event
when the action config doesn't have one set. Fixes silent 'No playlist specified'
failures in Discover/Sync chains. Added debug logging to trace event matching.
2026-03-23 08:51:52 -07:00
Broque Thomas
655e1e251d Add rate limit check to search_tracks and search_albums in Spotify client
Both methods were calling Spotify API without checking the global rate limit,
causing 429 errors to spam the logs even when the ban was active.
2026-03-23 07:53:46 -07:00
Broque Thomas
429306c7f3 Fix enrichment retry loops, cover art finding dupes, and Spotify rate limit during art scan
- All 9 enrichment workers: stop auto-retrying 'error' status items (was infinite loop)
  Only 'not_found' items retry after configured days; errors require manual full refresh
- Cover art dedup: check both 'pending' AND 'resolved' findings to prevent recreation
- Cover art scanner: top-level Spotify rate limit check skips Spotify entirely when
  banned, falls back to iTunes/Deezer only, logs once instead of spamming 429s
2026-03-22 23:24:42 -07:00
Broque Thomas
d8217d66ba Speed up metadata cache browser: add composite indexes, remove full-scan LIKE filters 2026-03-22 23:13:34 -07:00
Broque Thomas
203f317236 Redesign download missing and wishlist modals with unified glass style 2026-03-22 22:16:25 -07:00
Broque Thomas
8d3d623f10 Redesign sync page playlist cards: unified glass style across all sources and mirrored playlists 2026-03-22 22:10:42 -07:00
Broque Thomas
1ac16ce0d4 Redesign dashboard cards: tool cards, service status, and system stats with unified glass style 2026-03-22 21:52:47 -07:00
Broque Thomas
17e06aaae5 Update What's New modal with cache discovery, genre deep dive, DB storage, performance, mobile, and album split fix 2026-03-22 21:38:24 -07:00
Broque Thomas
e3d70da55a Add DB storage visualization + cache-powered discovery sections + Genre Deep Dive
- Stats page: database storage donut chart with per-table breakdown and total size
- Discover page: 5 new sections mined from metadata cache (zero API calls):
  Undiscovered Albums, New In Your Genres, From Your Labels, Deep Cuts, Genre Explorer
- Genre Deep Dive modal: artists (clickable → artist page), popular tracks,
  albums with download flow, related genre pills, in-library badges
- All cache queries filtered by active metadata source (Spotify/iTunes/Deezer)
- Stale cache entries (404) gracefully fall back to name+artist resolution
- Album cards show "In Library" badge, artist avatars scaled by prominence
2026-03-22 21:35:18 -07:00
Broque Thomas
c937045192 Mobile responsive overhaul: stats, artist detail, enhanced library, automations, hydrabase, docs
- Stats page: full mobile layout with compact cards, charts, ranked lists
- Artist hero: stacked layout, compact image/name/badges, top tracks below
- Enhanced library: meta header/expanded header stack vertically, track table
  collapses action columns into bottom sheet popover on mobile
- Automations: builder sidebar collapses, inputs go full width
- Hydrabase/Issues/Help: responsive stacking and compact layouts
- Fix grid blowout: add min-width:0 to stats grid children and overflow:hidden
2026-03-22 19:32:58 -07:00
Broque Thomas
40a9858f28 Fix album split in Navidrome: normalize MusicBrainz release cache keys across edition variants 2026-03-22 16:51:03 -07:00
Broque Thomas
01d96866e4 Increase stats page top artists/albums/tracks from 10 to 25 2026-03-22 16:39:53 -07:00
Broque Thomas
93005298ee Cap Opus bitrate at 256kbps, fix lossy copy flavor text, redesign artist action buttons 2026-03-22 16:34:36 -07:00
Broque Thomas
21d7e65986 Speed up library page: split DB query, innerHTML rendering, staggered card animation 2026-03-22 16:14:28 -07:00
Broque Thomas
8c84189121 Add per-artist enrichment coverage rings to artist hero section 2026-03-22 15:56:12 -07:00
Broque Thomas
f6225ec9a8 Fix enrichment coverage: correct Spotify column name and add all 9 services 2026-03-22 15:36:25 -07:00
Broque Thomas
b59a0eaf95 Add play buttons to stats page with cover art support 2026-03-22 15:24:38 -07:00
Broque Thomas
9e75731f6c Add scrobbling to Last.fm/ListenBrainz + update What's New
Scrobbling:
- Last.fm: API signing, web auth flow, batch scrobble (50/request)
- ListenBrainz: submit_listens (batch 1000, listen_type=import)
- Worker hook: scrobbles unscrobbled events after each poll
- DB: scrobbled_lastfm/scrobbled_listenbrainz tracking columns
- Settings: API secret input, authorize button, scrobble toggles
- Config: lastfm.api_secret, lastfm.session_key, *.scrobble_enabled

What's New: added all features from this session (scrobbling,
personalized discovery, stats page, SoulID, lossy codecs, import,
hero redesign, Hydrabase, orphan fixes, year collection).
2026-03-22 14:38:53 -07:00
Broque Thomas
232481fd13 Personalize discovery playlists using listening stats
Integrates play history data into the discovery algorithm:

- Listening profile: _get_listening_profile() builds user's top artists,
  genres, play counts, and listening velocity from the last 30 days
- Artist genre cache: pre-built from local DB for O(1) genre lookups
- Release Radar: +10 genre affinity, +15 artist familiarity, -10 overplay
  penalty. Weights rebalanced to 45% recency + 25% popularity + bonuses
- Discovery Weekly: serendipity scoring within tiers — boosts unheard
  artists in preferred genres, penalizes overplayed artists
- Recent Albums: adaptive time window (21-60 days) based on listening
  velocity — heavy listeners get fresher content, casual listeners more
- New "Because You Listen To" sections: personalized carousels based on
  user's top 3 played artists via similar artists + genre fallback
- New endpoint: /api/discover/because-you-listen-to with artist images
- Frontend: BYLT sections with artist photo headers on discover page
- All changes gracefully fall back when no listening data exists
2026-03-22 13:54:37 -07:00
Broque Thomas
cfb0e85564 Add Listening Stats page with media server play data integration
Full stats dashboard that polls Plex/Jellyfin/Navidrome for play
history and presents it with Chart.js visualizations:

Backend:
- ListeningStatsWorker polls active server every 30 min
- listening_history DB table with dedup, play_count/last_played on tracks
- get_play_history() and get_track_play_counts() for all 3 servers
- Pre-computed cache for all time ranges (7d/30d/12m/all) rebuilt each sync
- Single cached endpoint serves all stats data instantly
- Stats query methods: top artists/albums/tracks, timeline, genres, health

Frontend:
- New Stats nav page with glassmorphic container matching dashboard style
- Overview cards (plays, time, artists, albums, tracks) with accent hover
- Listening timeline bar chart (Chart.js)
- Genre breakdown doughnut chart with legend
- Top artists visual bubbles with profile pictures + ranked list
- Top albums and tracks ranked lists with album art
- Library health: format breakdown bar, unplayed count, enrichment coverage
- Recently played timeline with relative timestamps
- Time range pills with instant switching via cache
- Sync Now button with spinner, last synced timestamp
- Clickable artist names navigate to library artist detail
- Last.fm global listeners shown alongside personal play counts
- SoulID badges on matched artists
- Empty state when no data synced yet
- Mobile responsive layout

DB migrations: listening_history table, play_count/last_played columns,
all with idempotent CREATE IF NOT EXISTS / PRAGMA checks.
2026-03-22 13:18:14 -07:00
Broque Thomas
aca8a8e996 Fix Opus cover art and quality: map audio only, embed art via Mutagen
Opus ffmpeg command now uses -map 0:a to extract only the audio stream,
matching the user's working command and preventing picture stream
interference with the encoder. After conversion, cover art is embedded
from the source FLAC using Mutagen: Opus gets METADATA_BLOCK_PICTURE
(base64-encoded per OGG spec), AAC gets MP4Cover. Applied to both
the post-download lossy copy and the repair job fix handler.
2026-03-22 08:32:04 -07:00
Broque Thomas
b0b1c22f80 Read codec/bitrate from current settings at fix time, not scan time
The lossy converter fix handler now reads codec and bitrate fresh
from the Settings page when converting, not from what was stored
in the finding details at scan time. Users can change their codec
preference after scanning and Fix All will use the new setting.
2026-03-22 08:24:35 -07:00
Broque Thomas
bd27bbe1b2 Add independent Blasphemy Mode setting for lossy converter job
The lossy converter fix handler now reads delete_original from its
own job settings (repair.jobs.lossy_converter.settings.delete_original)
instead of the global lossy_copy.delete_original. Defaults to false.
Separate from the per-download Blasphemy Mode toggle in Settings.
2026-03-22 08:08:47 -07:00
Broque Thomas
adefe1c892 Register lossy_converter in repair jobs module list
Job was not appearing in Library Maintenance because the module
was not in the _JOB_MODULES import list.
2026-03-22 07:58:47 -07:00
Broque Thomas
598b63ba25 Add missing_lossy_copy to finding type labels and fixable types
Adds 'Convert' fix button and 'No Lossy Copy' type label for the
lossy converter repair job findings in the Library Maintenance UI.
2026-03-22 07:56:25 -07:00
Broque Thomas
bc5dd75c8e Add lossy converter repair job for retroactive FLAC conversion
New library maintenance job that scans for FLAC files missing a lossy
copy (MP3/Opus/AAC) and creates findings. Fix action converts via
ffmpeg using the configured codec/bitrate from Settings. Supports
Blasphemy Mode (delete original + update DB path). Finding details
store codec/bitrate from scan time for consistency. Disabled by
default, manual-run only, no auto-schedule.
2026-03-22 07:43:19 -07:00
Broque Thomas
491b89a1d2 Redesign library artist hero with Last.fm integration
- Add get_artist_top_tracks to Last.fm client (up to 100 tracks)
- Include lastfm_listeners, lastfm_playcount, lastfm_tags, lastfm_bio,
  and soul_id in artist detail API response
- New endpoint: /api/artist/<id>/lastfm-top-tracks for lazy loading
- Hero layout: image (160px) | center (name, badges, genres, bio,
  listener/play stats, progress bars) | right card (scrollable top
  100 tracks from Last.fm)
- Badges 36px with hover lift, bio in subtle card with Read More
  toggle, Last.fm tags merged with existing genres
- Numbers formatted: 1234567 → 1.2M
- Graceful degradation: sections hidden when Last.fm data unavailable
2026-03-21 23:04:34 -07:00
Broque Thomas
f9fc95c9f5 Add Opus and AAC codec options to lossy copy (Blasphemy Mode)
Lossy copy now supports MP3, Opus, and AAC (M4A) codecs with a
configurable dropdown in settings. Each codec uses the appropriate
ffmpeg encoder (libmp3lame/libopus/aac) and Mutagen tag writer
(ID3/Vorbis/MP4). Quality tag, filename substitution, and Blasphemy
Mode file cleanup all work per-codec. Backward compatible — existing
configs default to MP3.
2026-03-21 20:19:36 -07:00
Broque Thomas
a3592d2a14 Fix playlist sync crash: 'Working outside of application context'
get_current_profile_id() uses Flask's g object which doesn't exist
in background threads. Now profile_id is captured at request time
and passed as a parameter to _run_sync_task. All 8 call sites
updated. Automation path defaults to profile_id=1.
2026-03-21 16:09:24 -07:00
Broque Thomas
a015e8653b Rename orphan file fix button from 'Delete File' to 'Resolve'
The handler now prompts for staging vs delete, so the button label
should not imply deletion is the only option.
2026-03-21 11:34:15 -07:00
Broque Thomas
4dba3757be Fix orphan detector false positives and add staging/delete choice
Orphan detector: add normalized tag matching that strips parentheticals
and brackets (feat. X, [FLAC 16bit], etc.) and tries first-artist-only
for comma-separated artists. Prevents false orphan flags for tracks
like "The Mountain (feat. Dennis Hopper...)" that exist in DB as
"The Mountain". All lookups remain O(1) set operations.

Orphan fix: replace auto-delete with user choice prompt. Single Fix
and Fix All both show modal asking "Move to Staging" or "Delete".
Move to Staging relocates file to import staging folder for proper
re-import with metadata matching. Fix action flows through API
endpoint → repair_worker.fix_finding → _fix_orphan_file handler.
Staging path uses docker_resolve_path for container compatibility.
2026-03-21 11:27:51 -07:00
Broque Thomas
f991d02f0c Fix MusicBrainz release matching preferring standard over deluxe
When searching for "Playing the Angel (Deluxe)", MusicBrainz would
match the standard release because its higher popularity score
outweighed the lower title similarity. Now match_release extracts
version qualifiers (Deluxe, Remastered, etc.) from both the query
and each result, applying a +10 bonus for exact qualifier match,
+5 for partial, and -5 penalty when the query has a qualifier but
the result doesn't. This ensures deluxe downloads get the deluxe
MBID, preventing Navidrome from splitting albums.
2026-03-21 10:48:35 -07:00
Broque Thomas
85044261a4 Create trans2.png 2026-03-21 10:23:36 -07:00
Broque Thomas
2d511d0a16 Add SoulID worker with API-based debut year disambiguation
SoulID worker generates deterministic soul IDs for all library entities:
  - Artists: hash(name + debut_year) — searches iTunes + Deezer APIs,
    verifies correct artist by matching discography against local DB
    albums via MusicMatchingEngine, pools years from both sources and
    picks the earliest. Falls back to hash(name) if no match found.
  - Albums: hash(artist + album)
  - Tracks: song ID hash(artist + track) + album ID hash(artist + album + track)

Dashboard button with trans2.png logo, rainbow spinner, hover tooltip.
Worker orb with rainbow effect. SoulSync badge on library artist cards.
DB migration adds soul_id columns with indexes to artists/albums/tracks.
Migration version flag auto-resets artist soul IDs when algorithm changes.
2026-03-21 10:21:06 -07:00
Broque Thomas
cc96af2cb1 Fix auto-groups state cleanup on search reset and manual search
Clear _autoGroupFilePaths and re-show groups section on album search
reset. Hide auto-groups section during manual search.
2026-03-20 22:52:49 -07:00
Broque Thomas
a7bef972e0 Smarter staging import: tag-first matching and auto-grouping
1. Fix filename parser pattern order — "01 - Title" now matched
   before "Artist - Title", preventing track numbers being treated
   as artist names (e.g., "08" no longer becomes the artist)

2. Tag priority over filename parsing — shared _read_staging_file_metadata()
   helper reads title, artist, albumartist, album, track_number, disc_number
   from Mutagen tags. Only falls back to filename parsing when BOTH title
   AND artist tags are empty. Applied to all 3 staging scan sites.

3. Improved match scoring — rebalanced from title(0.5)+tracknum(0.5) to
   title(0.45)+artist(0.15)+tracknum(0.30)+album_bonus(0.10). Files
   whose album tag matches the selected album get boosted.

4. Auto-group detection — new /api/import/staging/groups endpoint groups
   staging files by album+artist tags. Frontend shows "Auto-Detected
   Albums" section with one-click search. Match endpoint accepts
   optional file_paths filter to scope matching to a specific group.
2026-03-20 22:34:20 -07:00
Broque Thomas
ee3500242e Fix Hydrabase search types, ID routing, and plugin passthrough
- Use correct server request types: 'tracks', 'albums', 'artists',
  'artist.albums', 'album.tracks' (were singular, caused timeouts)
- Normalize artists to strings (server may send dicts)
- Use native plugin IDs (iTunes/Spotify) instead of soul_id for
  album/artist/track IDs so downstream endpoints can resolve them
- Carry soul_id and plugin_id in external_urls for routing
- Pass plugin param from frontend to server for correct client routing
  (iTunes vs Deezer vs Spotify) with isdigit() fallback
- Route source=hydrabase to iTunes client for artist images
- Include external_urls in enhanced search API response
- Reduce WebSocket timeout from 15s to 8s
2026-03-20 21:04:56 -07:00
Broque Thomas
a4f0745547 Fix Hydrabase not appearing as enhanced search source tab
- Remove stale hydrabase.enabled check, use is_connected() directly
- Add hydrabase to frontend alternate source fetch list
- Normalize Hydrabase artists to strings (server may send dicts),
  fixing silent crashes that prevented albums/tracks from appearing
2026-03-20 18:24:20 -07:00
Broque Thomas
a8c5a6ccaa Add full Spotify-compatible interface to Hydrabase client
Implements get_track_details, get_album, get_artist, get_artist_albums,
get_track_features, is_authenticated, and reload_config to match the
iTunes/Deezer client interface. Each method tries a specific request
type first then falls back to search if the server doesn't support it.
Preserves existing get_album_tracks (List[Track]) for backward compat.
2026-03-20 18:14:31 -07:00
Broque Thomas
c69040886e Collect release year from all metadata sources during post-processing
Post-processing now extracts release year from MusicBrainz, Deezer,
Tidal, Qobuz, and Spotify context (first source wins). Writes
ORIGINALDATE and DATE tags to file, and backfills the album year
in the DB if currently missing. Fixes Library Reorganize showing
blank years for Tidal-only downloads.

Also raises Library Reorganize API year lookup cap from 50 to 200.
2026-03-20 17:31:00 -07:00
Broque Thomas
172c4e96ce Make repair worker orb cycle through rainbow colors matching its button
Adds rainbow color interpolation synced to the same ~3s cycle as the
CSS rainbow-spinner animation. Applies to orb core, glow, pulse rings,
connection lines, and spark particles.
2026-03-20 16:53:21 -07:00
Broque Thomas
2709f0fb37 Add hover animations and interaction polish to settings page
Tabs, cards, form rows, buttons, toggles, and inputs all get
tactile hover/focus/active states with lifts, glows, and accent
highlights to clarify which section the user is editing.
2026-03-20 16:31:55 -07:00
Broque Thomas
ce89154952 Fix hybrid source toggle/reorder not saving and skip unconfigured sources
- Add debouncedAutoSaveSettings() to moveHybridSource and toggleHybridSource
- Skip unconfigured sources at search time with is_configured() check
- Add get_source_status() to orchestrator, include in settings API response
- Auto-disable unconfigured sources in UI on settings load
2026-03-20 16:22:48 -07:00
Broque Thomas
3f70fac48c Allow manual match selection on failed tracks (not just not_found)
Failed tracks had candidates from the initial search but no way to
retry with a different source. Now clickable like not_found tracks
to open the manual match modal.
2026-03-20 16:09:07 -07:00
Broque Thomas
485a2d2792 Add fallback queries to album pre-flight Soulseek search
Album pre-flight now tries 3 query variations instead of one:
full artist+album, cleaned artist+album (no feat./parentheticals),
and album-only. Stops at first success. Fixes cases where a banned
keyword in the artist name caused zero results, falling back to
slower track-by-track search unnecessarily.
2026-03-20 16:01:50 -07:00
Broque Thomas
d7b9b3ba26 Refactor post-processing metadata lookups + fix Hydrabase as fallback source
Post-processing: Extract 7 inline source lookup blocks into standalone
_pp_lookup_* functions called in configurable order via
metadata_enhancement.post_process_order config. Default order matches
original hardcoded sequence — zero behavioral change.

Hydrabase: Fix connected Hydrabase incorrectly becoming primary source.
_is_hydrabase_active() now only returns True in dev_mode (legacy).
Auto-connect no longer forces dev_mode. Hydrabase as fallback works
through normal _get_metadata_fallback_client path like iTunes/Deezer.
Settings button min-width fix.
2026-03-20 15:48:48 -07:00