soulsync/core
Broque Thomas 89246a7304 Write artist.jpg to artist folder so Navidrome shows real photos
Closes #572 (rhwc).

Navidrome has no API for setting an artist image — it reads
`artist.jpg` (or `folder.jpg`) from the artist folder during
library scans. SoulSync's `update_artist_poster` for Navidrome
was a no-op, so users only ever saw album-art-derived thumbnails
as the artist photo.

- new "Write Artist Image" button on artist detail page
- POST /api/artist/<id>/write-image-to-disk derives the artist
  folder from any track's resolved file_path (reuses
  _resolve_library_file_path so docker mount translation +
  library.music_paths probes from #558 apply), fetches the photo
  from the configured metadata source priority chain, downloads
  with content-type validation, writes atomically via
  `<filename>.tmp + os.replace`
- when active server is Navidrome, triggers a library scan
  immediately so the file is picked up
- respects existing artist.jpg (frontend prompts before
  overwriting) so user-supplied photos aren't clobbered
- works for plex / jellyfin too as a fallback layer — both
  servers also read artist.jpg from disk

26 tests pin the pure helpers in core/library/artist_image.py:
folder derivation (trailing sep / empty / non-string), URL
picking (missing attr / whitespace / non-string), download
(non-image content-type / 404 / timeout / empty body), atomic
write (replace / temp-cleanup-on-failure / overwrite guard /
missing folder).
2026-05-13 11:48:09 -07:00
..
artists
automation Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
discovery Server playlist sync: append mode (preserve user-added tracks) 2026-05-10 22:52:11 -07:00
download_engine Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
download_plugins
downloads Manual picks: stream results, don't auto-retry, fix stuck-at-0% 2026-05-08 15:12:58 -07:00
enrichment Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
imports Auto-import: album duration = album total + conservative re-import UPDATE path 2026-05-09 21:19:35 -07:00
library Write artist.jpg to artist folder so Navidrome shows real photos 2026-05-13 11:48:09 -07:00
matching AcoustID scanner: handle multi-value artist credits 2026-05-10 19:17:59 -07:00
media_server Server playlist sync: append mode (preserve user-added tracks) 2026-05-10 22:52:11 -07:00
metadata Fix $albumtype defaulting to album for non-Spotify sources 2026-05-12 21:09:16 -07:00
playlists Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
repair_jobs AcoustID scanner: file-tag fallback for legacy compilation tracks 2026-05-11 11:39:19 -07:00
search
stats Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
streaming
watchlist Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
wishlist Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
workers Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
acoustid_client.py
acoustid_verification.py Tighten alias-lookup trust + add ambiguity gate + diagnostic log 2026-05-10 17:38:03 -07:00
album_consistency.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
api_call_tracker.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
artist_source_detail.py
artist_source_lookup.py
audiodb_client.py
audiodb_worker.py AudioDB worker: stop infinite loop on direct-ID lookup failure (#553) 2026-05-11 14:18:55 -07:00
auto_import_worker.py Auto-import: fall through to other metadata sources when primary returns no match 2026-05-12 12:32:18 -07:00
automation_engine.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
connection_detect.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
connection_test.py
database_update_worker.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
debug_info.py Fix Copy Debug Info music_source + surface missing services 2026-05-12 16:47:55 -07:00
deezer_client.py Deezer cover art: upgrade CDN URL to 1900×1900 (was embedding 1000×1000) 2026-05-10 18:15:40 -07:00
deezer_download_client.py
deezer_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
discogs_client.py
discogs_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
download_orchestrator.py
genius_client.py
genius_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
genre_filter.py
hifi_client.py
hydrabase_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
hydrabase_worker.py
itunes_client.py
itunes_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
jellyfin_client.py Server playlist sync: append mode (preserve user-added tracks) 2026-05-10 22:52:11 -07:00
lastfm_client.py
lastfm_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
library_reorganize.py Reorganize: hint at Unknown Artist Fixer for placeholder-metadata rows 2026-05-10 20:16:28 -07:00
lidarr_download_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
listenbrainz_client.py
listenbrainz_manager.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
listening_stats_worker.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
lyrics_client.py
matching_engine.py
metadata_service.py
musicbrainz_client.py
musicbrainz_search.py
musicbrainz_service.py Tighten alias-lookup trust + add ambiguity gate + diagnostic log 2026-05-10 17:38:03 -07:00
musicbrainz_worker.py Alias resolution polish: lazy-fire on direct-match failure + worker backfill 2026-05-10 17:02:02 -07:00
navidrome_client.py Server playlist sync: append mode (preserve user-added tracks) 2026-05-10 22:52:11 -07:00
personalized_playlists.py Discover: sharpen track selection (diversity, source-aware popularity, library dedup, SQL genre) 2026-05-08 08:49:22 -07:00
plex_client.py Server playlist sync: append mode (preserve user-added tracks) 2026-05-10 22:52:11 -07:00
qobuz_client.py
qobuz_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
reorganize_queue.py
reorganize_runner.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
repair_worker.py Repair worker: log debug instead of bare pass on active-server lookup 2026-05-12 14:14:19 -07:00
replaygain.py Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
runtime_state.py Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
seasonal_discovery.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
socketio_cors.py
soulid_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
soulseek_client.py Add min delay between slskd searches (Bell Canada anti-abuse fix) 2026-05-12 19:11:12 -07:00
soulsync_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
soundcloud_client.py
spotify_client.py Add download buttons + bulk action to artist top-tracks sidebar 2026-05-07 15:44:47 -07:00
spotify_public_scraper.py
spotify_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
tag_writer.py Deezer cover-art download: fallback to original URL on CDN refusal 2026-05-10 18:29:36 -07:00
tidal_client.py Your Albums: selectable wishlist modal + Tidal album resolution 2026-05-11 12:36:16 -07:00
tidal_download_client.py
tidal_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
watchlist_scanner.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
web_scan_manager.py
wishlist_service.py
worker_utils.py
youtube_client.py Drop redundant import sys inside _auto_download_disabled 2026-05-08 15:51:16 -07:00