Commit graph

1966 commits

Author SHA1 Message Date
Broque Thomas
71e4df65e3 Remove emojis from all Python log and print statements
Stripped 4,200+ emoji characters from print(), logger calls across
39 Python files. Logs are now clean text — easier to grep, more
professional, no encoding issues on terminals without Unicode support.

Seasonal config icons preserved for UI display.
2026-04-11 21:11:02 -07:00
Broque Thomas
faba4d5847 Add missing pages to profile page access and home page options
Profile creation was missing Listening Stats, Playlist Explorer, and
Issues from the page access checkboxes. Home page dropdown was missing
Stats, Playlist Explorer, and Help & Docs. Both admin and self-edit
pageLabels dicts updated to match.
2026-04-11 19:41:03 -07:00
Broque Thomas
a3ab5adcba Backfill MusicBrainz recording ID from Navidrome during database scan
Navidrome provides musicBrainzId on tracks — now captured during
database updates so the MusicBrainz enrichment worker can skip
tracks that already have an MBID.

Uses COALESCE on UPDATE to never overwrite existing enrichment data
with NULL (safe for Plex/Jellyfin which don't provide this field).

Inspired by PR #279 — fixed data loss bug in the original where
unconditional UPDATE would erase existing MBIDs.
2026-04-11 18:47:54 -07:00
Broque Thomas
7fb4cf6b0e Use per-track artist in tag writer for compilations and DJ mixes
Tag preview and writer now use track_artist (per-track artist) for the
Artist tag when available, falling back to artist_name (album artist)
when NULL. Album Artist tag always uses artist_name.

Fixes #277 — DJ mix albums no longer overwrite per-track artists
(Technique, Gouryella) with the album artist (Tiësto).
2026-04-11 18:31:11 -07:00
Broque Thomas
f5ac7c9261 Include track_artist in track search queries for collab/feature matching 2026-04-11 18:13:07 -07:00
Broque Thomas
d39db04ac0 Standardize Discogs worker hover tooltip to match other enrichment buttons 2026-04-11 17:40:41 -07:00
Broque Thomas
1e078192f0 Add track_artist column for per-track artist on compilations/DJ mixes
Phase 1: data collection only — no behavior changes.

Adds nullable track_artist column to tracks table. During database
updates (incremental, full refresh, deep scan), extracts per-track
artist from the media server when it differs from the album artist:
- Plex: originalTitle field
- Jellyfin/Emby: ArtistItems[0] vs AlbumArtists[0]
- Navidrome: artist attribute vs album artist name

NULL for normal albums (track artist = album artist). Populated only
when the media server reports a different per-track artist.

UPDATE uses COALESCE to never overwrite existing data with NULL.
2026-04-11 17:12:28 -07:00
Broque Thomas
805f72c5fd Add acappella to live/commentary cleaner patterns 2026-04-11 13:56:56 -07:00
Broque Thomas
c461f0071d Skip empty search results when navigating back from artist detail page 2026-04-11 13:54:23 -07:00
Broque Thomas
b3d3c017ed Use multi-stage Docker build to reduce image size
Builder stage compiles Python dependencies with gcc/build tools.
Runtime stage only includes curl, gosu, ffmpeg, and libchromaprint-tools.
Build tools are not shipped in the final image, reducing size and
attack surface.

Inspired by kettui's PR #273.
2026-04-11 13:39:16 -07:00
BoulderBadgeDad
8977120ba8
Merge pull request #274 from kettui/feat/metadata-client-caching
Add caching for metadata clients
2026-04-11 13:07:54 -07:00
BoulderBadgeDad
6d0ffae5fb
Merge pull request #275 from kettui/fix/spotify-ratelimited-search
Add missing rate-limit handling for Spotify search requests
2026-04-11 11:26:45 -07:00
BoulderBadgeDad
2ecc434c7a
Merge pull request #276 from kettui/feat/misc-cleanup
Reduce redundant logging during watchlist scan, initialize db once at startup
2026-04-11 11:10:38 -07:00
Broque Thomas
0d55356eaf Update What's New with music videos, Lidarr source, and bug fixes
Added sections for Music Videos search/download, Lidarr download source
(development), and recent bug fixes (dismissed findings, orphan detector,
duplicate audio streams, logs directory, stale discovery re-processing).
2026-04-11 09:50:33 -07:00
Broque Thomas
26ebb68961 Mark Lidarr download source as Development in UI 2026-04-11 09:38:09 -07:00
Broque Thomas
121f221a4f Prevent duplicate audio streams from rapid play button clicks
Added _streamLock flag to startStream() that prevents concurrent stream
requests when the user clicks play multiple times before the first
request completes. Lock is released in finally block so it always
clears on success, error, or exception.

Previously, rapid clicks would fire multiple stream requests to the
backend, each creating a separate audio playback — the only way to
stop them was a browser force refresh.
2026-04-11 09:01:30 -07:00
Broque Thomas
6fb32228e0 Create logs directory before source_reuse.log handler to prevent startup crash 2026-04-11 08:39:20 -07:00
Broque Thomas
6b2352411b Add Lidarr to all remaining source lists in orchestrator and web_server 2026-04-11 08:05:54 -07:00
Broque Thomas
9926c2f700 Add Lidarr to hybrid download source priority list 2026-04-11 08:01:09 -07:00
Broque Thomas
fc38ec4787 Add Lidarr as 7th download source and validate music video path
Lidarr integration:
- New core/lidarr_download_client.py with full interface parity
  (search, download, status, cancel — same as Qobuz/Tidal/HiFi)
- Registered in download orchestrator with source routing
- Settings: URL + API key on Downloads tab with connection test
- Available as standalone source or in Hybrid mode priority order
- API key encrypted at rest
- All streaming source checks updated to include 'lidarr'

Lidarr downloads full albums via Usenet/torrent — SoulSync imports
only the tracks it needs and discards the rest.

Music video path validation:
- Empty/unconfigured path returns clear error instead of silent failure
- Write permission test before starting download
- Default changed from './MusicVideos' to empty (must be configured)
2026-04-11 07:59:12 -07:00
Antti Kettunen
337ec7309b Initialize the database only once per process
Since MusicDatabase is initialized per-thread (which I don't dare to change), we end up needlessly calling _initialize_database for each client that gets created, thus making a ton of redundant db initialization / migration calls over and over again throughout the process' lifetime
2026-04-11 14:35:58 +03:00
Antti Kettunen
4946ff0d03 Remove redundant repetition of lookback period change during watchlist scan
Unnecessary noise on the logs
2026-04-11 14:01:52 +03:00
Antti Kettunen
45f608bf12 Utilize cached spotify client in get_spotify_artist_discography 2026-04-11 13:55:14 +03:00
Antti Kettunen
fd6335a66e Add / improve metadata client caching
Clients are for the most part being initialized per-request, which leads to a lot of redundant client initialization, as well as noise on the logs, since each client initialization emits a row on the logs, eg. 'Deezer client initialized'
2026-04-11 13:55:10 +03:00
Antti Kettunen
1b979193eb Skip Spotify requests for the rest of the watchlist scan if rate-limited
State is stored per-scan
2026-04-11 13:41:05 +03:00
Antti Kettunen
36dbb3357e Add rate-limit handling for Spotify searches 2026-04-11 13:14:50 +03:00
Antti Kettunen
6b6c866d03 Add separate requirements file for dev dependencies 2026-04-11 12:58:20 +03:00
Broque Thomas
1dcdccb282 Fix music video download in global search and improve progress visibility
- Moved _downloadMusicVideo to top-level scope so global search can use
  it (was inside enhanced search conditional that only runs on downloads page)
- Global search video cards use base64 data attributes to avoid JSON
  escaping issues in onclick handlers
- Darkened thumbnail overlay during download for better progress visibility
- Larger progress ring (52px) with accent-colored glow shadow
2026-04-10 23:23:35 -07:00
Broque Thomas
c02eb0eb0a Fix global search music video click handler — remove double HTML escaping 2026-04-10 23:15:22 -07:00
Broque Thomas
54b7a0f0e8 Add music video download with progress and metadata matching
Click any video card in Music Videos tab to download. Flow:
1. Search primary metadata source for clean artist/title
2. Fall back to YouTube title parsing if no match
3. Download video via yt-dlp (best quality MP4)
4. Save to configured Music Videos folder as Artist/Title-video.mp4

UI shows circular progress ring on the thumbnail during download,
green checkmark on completion, red X on error (clickable to retry).
Cards are non-interactive while downloading.

Backend: /api/music-video/download and /api/music-video/status endpoints
YouTube client: download_music_video() method keeps video format
2026-04-10 23:14:20 -07:00
Broque Thomas
b44bb34b44 Add Music Videos search tab to enhanced and global search
New "Music Videos" pill tab alongside Spotify/Deezer/iTunes/Discogs
in both enhanced search and global search. Searches YouTube via yt-dlp
and displays results in a video card grid with 16:9 thumbnails, play
overlay, duration badge, channel name, and view count.

- Backend: /api/enhanced-search/source/youtube_videos endpoint with
  search_videos() method on YouTubeClient returning YouTubeSearchResult
- Frontend: Video grid layout with responsive cards, YouTube red tab
  color, proper section hiding when switching between metadata and
  video tabs
- Global search: Full parity with enhanced search video rendering
- No download functionality yet — display only
2026-04-10 23:07:14 -07:00
Broque Thomas
1f0ef08b48 Add Music Videos directory setting for Plex music video support
New configurable path for storing music videos separately from audio
files, following Plex's global music video folder convention.

- Settings: library.music_videos_path (default: ./MusicVideos)
- UI: Music Videos Dir field on Settings Downloads tab with lock/unlock
- Docker: /app/MusicVideos volume mount in Dockerfile and docker-compose
- Added 'library' to settings save whitelist (was missing — music_paths
  also wasn't persisting through main settings save)
- No download functionality yet — path infrastructure only
2026-04-10 22:01:27 -07:00
Broque Thomas
5be6a46fb0 Fix dismissed findings reappearing and reduce false orphan detections
The finding dedup check only looked for 'pending' and 'resolved' status,
missing 'dismissed'. Dismissed findings were recreated as new entries on
every scan. Now includes 'dismissed' in the dedup check.

Orphan file detector improvements:
- Increased path suffix matching depth from 3 to 4 segments (covers
  Genre/Artist/Album/track.flac paths)
- Added filename-based fallback when Mutagen can't read file tags —
  parses title from "NN - Title [Quality].ext" pattern and matches
  against parent/grandparent folder names as artist
2026-04-10 17:42:39 -07:00
Broque Thomas
acb4479313 Re-discover tracks with incomplete metadata in playlist pipeline
The discovery worker skipped already-discovered tracks even when their
matched_data was incomplete (missing track_number, release_date, album
ID). These stale discoveries from before the enrichment fix would
persist forever, causing the automation pipeline to keep producing
tracks with no year, no track numbers, and no cover art.

Now treats discovered tracks as undiscovered if they're missing
track_number AND have no release_date or album ID, so the enriched
discovery pipeline fills in the gaps on the next run.
2026-04-10 17:11:42 -07:00
Broque Thomas
94e0671eb4 Update What's New with metadata pipeline and matching engine fixes
Adds two new sections to the version modal covering the Unknown Artist
fix, centralized metadata source selection, Deezer cache fix, sync
completion feedback, Fix Unknown Artists maintenance job, and the
matching engine artist gate improvements.
2026-04-10 14:21:37 -07:00
Broque Thomas
603d66ba5d Add artist gate and fix substring matching in matching engine
Prevents downloading tracks from completely wrong artists by adding
minimum artist score gates:
- Soulseek: artist_score < 0.25 → reject (catches Belvedere vs Periphery)
- YouTube: artist_score < 0.15 → reject (catches lizzylou06 vs Muse)

Fixes artist substring matching to use word boundaries instead of plain
containment — "muse" no longer matches "museum", "art" no longer matches
"heart". This was causing false positives where wrong artists passed with
artist_score=1.0 due to accidental substring containment.

Improves similarity fallback by comparing against individual path segments
instead of the full filename, so misspelled artist names (Radiohedd vs
Radiohead) still match correctly.

Adjusts YouTube weights from Title 70%/Artist 10% to Title 60%/Artist 20%
to give artist more influence in YouTube matching.

Addresses user reports of unreleased albums being downloaded with garbage
content from wrong artists on Soulseek and YouTube.
2026-04-10 13:45:07 -07:00
Broque Thomas
a6117d5174 Replace all legacy metadata_service imports with canonical functions
All callers of _create_fallback_client() and _get_configured_fallback_source()
now use get_primary_client() and get_primary_source() directly. No more
legacy alias usage anywhere in the codebase.
2026-04-10 12:52:36 -07:00
Broque Thomas
10a2766557 Fix remaining Spotify-first source selection in seasonal discovery and search API
Seasonal discovery had 3 use_spotify checks using is_authenticated()
(always True) instead of deriving from the configured source. Search API
(tracks, albums, artists) also defaulted to Spotify when authenticated.

All now check configured primary source first via get_primary_source().
2026-04-10 12:47:16 -07:00
Broque Thomas
498c22e7c3 Centralize metadata source selection in core/metadata_service.py
All metadata source decisions now flow through get_primary_source() and
get_primary_client() in core/metadata_service.py. Previously 6 different
files reimplemented this logic with inconsistent defaults ('itunes' vs
'deezer') and auth checks, causing bugs when any one was missed.

Changes:
- metadata_service.py: Added canonical get_primary_source/get_primary_client
- web_server.py: _get_metadata_fallback_source() and _get_active_discovery_source()
  are now thin wrappers delegating to metadata_service
- seasonal_discovery.py: _get_source() delegates to metadata_service
- personalized_playlists.py: _get_active_source() delegates to metadata_service
- spotify_client.py: Fixed _fallback_source default from 'itunes' to 'deezer'
- watchlist_scanner.py: _get_fallback_metadata_client() delegates to metadata_service

Future changes to source selection only need to update one file.
2026-04-10 12:34:25 -07:00
Broque Thomas
52a5d93018 Add Fix Unknown Artists maintenance job
New repair job that scans the library for tracks filed under "Unknown
Artist" and corrects them. Resolves correct metadata by:
1. Reading embedded file tags (if file has correct artist)
2. Looking up by source track ID (Spotify/Deezer/iTunes)
3. Searching by title as last resort

Dry run mode (default) creates findings for review. Live mode re-tags
the audio file, moves it to the correct folder structure, and updates
the database. Includes fix handler for applying individual findings.
2026-04-10 12:17:11 -07:00
Broque Thomas
57fc18f994 Respect configured primary source in seasonal, playlists, and explorer
Seasonal discovery, personalized playlists, and playlist explorer all
defaulted to Spotify when authenticated, ignoring the user's configured
primary source. Now they read from config first.

Spotify's related_artists API (no Deezer/iTunes equivalent) is preserved
as a fallback for all users in personalized playlists. Artist discography
endpoint intentionally unchanged — ID-based lookups need the source that
owns the ID.
2026-04-10 11:43:06 -07:00
Broque Thomas
7d21385ce9 Show which tracks failed to match in sync completion toast
When a playlist sync has unmatched tracks sent to wishlist, the
completion toast now shows the specific track names instead of just
a count. Uses warning style so it stands out. The unmatched track
list is included in the sync state result so it's available for
both live status polling and notification history.

Addresses #272 — silent sync failures where users couldn't tell
which tracks out of 150+ failed to match their Plex library.
2026-04-10 11:04:21 -07:00
Broque Thomas
df14bbf745 Add one-time migration to purge stale Deezer metadata cache
Deezer album entries cached from /artist/{id}/albums lack artist info,
and track entries from search results lack track_position. Purges all
Deezer album/track cache entries on first startup so they repopulate
with complete data.
2026-04-10 10:45:47 -07:00
Broque Thomas
bbaa897cd2 Fix Deezer metadata cache storing incomplete album and track data
Deezer's /artist/{id}/albums endpoint returns albums without an artist
field, causing 98% of cached Deezer albums to have empty artist_name.
Now injects the known artist before caching.

Also fixes get_track_details cache validation — was trusting search
result cache (which has isrc but no track_position), returning
track_number=0. Now only trusts cache entries with track_position.
2026-04-10 10:43:45 -07:00
Broque Thomas
dd5291456b Fix playlist pipeline discovery data loss and Unknown Artist bug
Discovery workers now respect the user's configured primary metadata
source instead of always using Spotify when authenticated. This
completes the intent of commit 3c211ea.

The core fix addresses data loss in the discovery→sync→wishlist→download
pipeline: the Track dataclass strips album metadata to a plain string,
losing album ID, track_number, release_date, and images. Discovery
workers now enrich results via get_track_details() to recover this data.
Deezer's get_track_details() cache validation was incorrectly trusting
search-result cache (which lacks track_position), returning track_number=0.

Also fixes wishlist download processing where albums without IDs couldn't
map to artists, and the fallback read 'artist' (singular) instead of
'artists' (plural), always producing "Unknown Artist".

Includes a one-time migration to purge stale discovery cache entries.
2026-04-10 10:23:43 -07:00
Broque Thomas
e83ee23990 Add Reduce Visual Effects toggle for low-end devices
New toggle in Settings > Appearance disables backdrop blur (220
instances), animations (238), transitions (961), and box shadows
(804) across the entire UI via a single body class. Significantly
reduces GPU/CPU usage on low-end devices. Default off — no change
for existing users. Applied from localStorage on load to prevent
flash.
2026-04-10 06:34:21 -07:00
Broque Thomas
d9e2e129b0 Allow selecting which duplicate to keep in duplicate detector
Duplicate finding detail now shows each version as clickable — user
can choose which to keep instead of relying on auto-selection. Added
track_number as tiebreaker in auto-pick (higher track number wins
over 01, catching leftover duplicates from the playlist sync track
number bug). Track number displayed in the detail view for clarity.
2026-04-09 18:46:49 -07:00
Broque Thomas
959bca2b8d Add Run Script action to automation engine
New automation action that executes user scripts from a dedicated
scripts/ directory. Available as both a DO action and THEN action.
Scripts are selected from a dropdown populated by /api/scripts.

Security: only scripts in the scripts dir can run, path traversal
blocked, no shell=True, stdout/stderr capped, configurable timeout
(max 300s). Scripts receive SOULSYNC_EVENT, SOULSYNC_AUTOMATION,
and SOULSYNC_SCRIPTS_DIR environment variables.

Includes Dockerfile + docker-compose.yml changes for the scripts
volume mount, and three example scripts (hello_world.sh,
system_info.py, notify_ntfy.sh).
2026-04-09 18:20:29 -07:00
Broque Thomas
5ed819a062 Show SoulSync logo as fallback for missing sidebar album art
Sidebar media player now shows the SoulSync logo instead of a broken
image icon when no album art is available or when no track is playing.
Default src, onerror fallback, and clear-player paths all use
/static/trans2.png.
2026-04-09 10:51:48 -07:00
Broque Thomas
7cfd1cae3f Fix AcoustID scanner creating thousands of no-match findings
The scanner was creating a finding for every file that couldn't be
identified by AcoustID, flooding the findings list with non-actionable
entries. Users saw the scanner "stuck scanning the same files over
and over" because the no-match findings were dismissed but recreated
on every run. Now only genuine mismatches (AcoustID identifies a
different track) create findings. Errors are counted and shown in
the job log with actual error messages for debugging.
2026-04-09 10:16:53 -07:00