Third sub-PR in the download orchestrator series. Strict 1:1 lift — zero behavior change. What moved: - _automatic_wishlist_cleanup_after_db_update → cleanup_wishlist_after_db_update The lifted fn takes config_manager as an arg (so core/downloads/cleanup.py doesn't need to import web_server). Other deps (wishlist_service, MusicDatabase, get_database) stay as in-function imports — matches the original deferred-import pattern. The single caller in web_server.py (missing_download_executor.submit at L18028) keeps using the same wrapper name with no signature change. Behavior parity: - Same per-profile iteration via get_all_profiles() - Same essential-field skip (no name / no artists / no spotify_track_id) - Same artist normalization (string / dict / fallback to str()) - Same 0.7 confidence threshold for db match - Same break-on-first-artist-match semantics - Same album extraction (dict.name vs string passthrough) - Same active_server pulled via config_manager.get_active_media_server() - Same per-track exception swallowing inside the loops - Same top-level exception swallow with traceback.print_exc() - Same logger messages (exact text match for "[Auto Cleanup]" prefix) Tests: 13 new under tests/downloads/test_downloads_cleanup.py covering empty wishlist short-circuit, found-in-db removal, missed track stays, low-confidence skip, missing-fields skip, dict + string artist formats, break-on-first-match, multi-profile walk, album dict/string handling, db check failure continuing to next artist, top-level exception swallow, active server propagation. Full suite: 934 passing (was 921). Ruff clean.
100 lines
4.2 KiB
Python
100 lines
4.2 KiB
Python
"""Automatic wishlist cleanup after database updates.
|
|
|
|
Runs as a background task after the library DB refresh completes — walks
|
|
every profile's wishlist, fuzzy-matches each track against the freshly
|
|
scanned library, and removes hits. Best-effort: logs and continues on
|
|
per-track failure, swallows top-level exceptions so the executor doesn't
|
|
get a propagated failure.
|
|
|
|
Lifted verbatim from web_server.py's `_automatic_wishlist_cleanup_after_db_update`.
|
|
The single global dep (`config_manager`) is passed in to keep this module
|
|
free of web_server imports.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import traceback
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def cleanup_wishlist_after_db_update(config_manager) -> None:
|
|
"""Walk all profiles' wishlists and remove tracks now present in the library."""
|
|
try:
|
|
from core.wishlist_service import get_wishlist_service
|
|
from database.music_database import MusicDatabase, get_database
|
|
|
|
wishlist_service = get_wishlist_service()
|
|
db = MusicDatabase()
|
|
active_server = config_manager.get_active_media_server()
|
|
|
|
logger.info("[Auto Cleanup] Starting automatic wishlist cleanup after database update...")
|
|
|
|
# Get all wishlist tracks (across all profiles - cleanup is global)
|
|
database = get_database()
|
|
all_profiles = database.get_all_profiles()
|
|
wishlist_tracks = []
|
|
for p in all_profiles:
|
|
wishlist_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id']))
|
|
if not wishlist_tracks:
|
|
logger.warning("[Auto Cleanup] No tracks in wishlist to clean up")
|
|
return
|
|
|
|
logger.info(f"[Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
|
|
|
|
removed_count = 0
|
|
|
|
for track in wishlist_tracks:
|
|
track_name = track.get('name', '')
|
|
artists = track.get('artists', [])
|
|
spotify_track_id = track.get('spotify_track_id') or track.get('id')
|
|
track_album = track.get('album', {}).get('name') if isinstance(track.get('album'), dict) else track.get('album')
|
|
|
|
# Skip if no essential data
|
|
if not track_name or not artists or not spotify_track_id:
|
|
continue
|
|
|
|
# Check each artist
|
|
found_in_db = False
|
|
for artist in artists:
|
|
# Handle both string format and dict format
|
|
if isinstance(artist, str):
|
|
artist_name = artist
|
|
elif isinstance(artist, dict) and 'name' in artist:
|
|
artist_name = artist['name']
|
|
else:
|
|
artist_name = str(artist)
|
|
|
|
try:
|
|
db_track, confidence = db.check_track_exists(
|
|
track_name, artist_name,
|
|
confidence_threshold=0.7,
|
|
server_source=active_server,
|
|
album=track_album,
|
|
)
|
|
|
|
if db_track and confidence >= 0.7:
|
|
found_in_db = True
|
|
logger.info(f"[Auto Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
|
|
break
|
|
|
|
except Exception as db_error:
|
|
logger.error(f"[Auto Cleanup] Error checking database for track '{track_name}': {db_error}")
|
|
continue
|
|
|
|
# If found in database, remove from wishlist
|
|
if found_in_db:
|
|
try:
|
|
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
|
|
if removed:
|
|
removed_count += 1
|
|
logger.info(f"[Auto Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
|
|
except Exception as remove_error:
|
|
logger.error(f"[Auto Cleanup] Error removing track from wishlist: {remove_error}")
|
|
|
|
logger.info(f"[Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist")
|
|
|
|
except Exception as e:
|
|
logger.error(f"[Auto Cleanup] Error in automatic wishlist cleanup: {e}")
|
|
traceback.print_exc()
|