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.
This commit is contained in:
parent
2c2c006f83
commit
ebc0713348
1 changed files with 118 additions and 2 deletions
|
|
@ -332,9 +332,29 @@ class LibraryReorganizeJob(RepairJob):
|
||||||
|
|
||||||
# Pre-load album years from DB for files missing year tags
|
# Pre-load album years from DB for files missing year tags
|
||||||
db_album_years = {} # (artist, album) -> year string
|
db_album_years = {} # (artist, album) -> year string
|
||||||
if '$year' in (album_template + single_template):
|
needs_year = '$year' in (album_template + single_template)
|
||||||
|
if needs_year:
|
||||||
db_album_years = self._load_album_years(context.db)
|
db_album_years = self._load_album_years(context.db)
|
||||||
|
|
||||||
|
# API fallback: find (artist, album) pairs still missing year, batch-lookup
|
||||||
|
if needs_year and db_album_years is not None:
|
||||||
|
missing_pairs = set()
|
||||||
|
for fpath, tags in file_tags.items():
|
||||||
|
year = tags.get('year', '')
|
||||||
|
if year:
|
||||||
|
continue
|
||||||
|
artist = tags.get('artist', '') or tags.get('albumartist', '')
|
||||||
|
album = tags.get('album', '') or tags.get('title', '')
|
||||||
|
if not artist or not album:
|
||||||
|
continue
|
||||||
|
key = (artist.lower(), album.lower())
|
||||||
|
if key not in db_album_years:
|
||||||
|
missing_pairs.add((artist, album))
|
||||||
|
|
||||||
|
if missing_pairs:
|
||||||
|
api_years = self._lookup_years_from_api(context, missing_pairs)
|
||||||
|
db_album_years.update(api_years)
|
||||||
|
|
||||||
# Track claimed destinations to detect in-batch collisions
|
# Track claimed destinations to detect in-batch collisions
|
||||||
claimed_destinations = set()
|
claimed_destinations = set()
|
||||||
# Track src_dir -> dest_dir for post-pass sidecar cleanup
|
# Track src_dir -> dest_dir for post-pass sidecar cleanup
|
||||||
|
|
@ -636,12 +656,18 @@ class LibraryReorganizeJob(RepairJob):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _load_album_years(self, db) -> dict:
|
def _load_album_years(self, db) -> dict:
|
||||||
"""Load all album years from DB in one query. Returns {(artist_lower, album_lower): year_str}."""
|
"""Load all album years from DB. Returns {(artist_lower, album_lower): year_str}.
|
||||||
|
|
||||||
|
Checks the main albums table first, then falls back to discovery_pool
|
||||||
|
release_date for tracks that were playlist-synced without year metadata.
|
||||||
|
"""
|
||||||
years = {}
|
years = {}
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = db._get_connection()
|
conn = db._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Source 1: albums table (most authoritative)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT ar.name, al.title, al.year
|
SELECT ar.name, al.title, al.year
|
||||||
FROM albums al
|
FROM albums al
|
||||||
|
|
@ -653,6 +679,25 @@ class LibraryReorganizeJob(RepairJob):
|
||||||
if artist_name and album_title and year:
|
if artist_name and album_title and year:
|
||||||
key = (artist_name.lower(), album_title.lower())
|
key = (artist_name.lower(), album_title.lower())
|
||||||
years[key] = str(year)[:4]
|
years[key] = str(year)[:4]
|
||||||
|
|
||||||
|
# Source 2: discovery_pool release_date (covers playlist-synced tracks)
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT artist_name, album_name, release_date
|
||||||
|
FROM discovery_pool
|
||||||
|
WHERE release_date IS NOT NULL AND release_date != ''
|
||||||
|
""")
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
artist_name, album_name, release_date = row
|
||||||
|
if artist_name and album_name and release_date:
|
||||||
|
key = (artist_name.lower(), album_name.lower())
|
||||||
|
if key not in years: # Don't override albums table
|
||||||
|
year_str = str(release_date)[:4]
|
||||||
|
if len(year_str) == 4 and year_str.isdigit():
|
||||||
|
years[key] = year_str
|
||||||
|
except Exception:
|
||||||
|
pass # discovery_pool may not exist on all installs
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Failed to load album years from DB: %s", e)
|
logger.debug("Failed to load album years from DB: %s", e)
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -660,6 +705,77 @@ class LibraryReorganizeJob(RepairJob):
|
||||||
conn.close()
|
conn.close()
|
||||||
return years
|
return years
|
||||||
|
|
||||||
|
def _lookup_years_from_api(self, context, missing_pairs) -> dict:
|
||||||
|
"""Batch-lookup release years from Spotify/iTunes/Deezer for albums not found in DB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: JobContext with spotify_client, config_manager
|
||||||
|
missing_pairs: set of (artist, album) tuples needing year lookup
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict of {(artist_lower, album_lower): year_str}
|
||||||
|
"""
|
||||||
|
years = {}
|
||||||
|
if not missing_pairs:
|
||||||
|
return years
|
||||||
|
|
||||||
|
# Determine which client to use
|
||||||
|
search_client = None
|
||||||
|
source_name = 'unknown'
|
||||||
|
if context.spotify_client and hasattr(context.spotify_client, 'search_albums'):
|
||||||
|
try:
|
||||||
|
if context.spotify_client.is_spotify_authenticated():
|
||||||
|
search_client = context.spotify_client
|
||||||
|
source_name = 'Spotify'
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not search_client:
|
||||||
|
# Try fallback (iTunes/Deezer)
|
||||||
|
try:
|
||||||
|
from core.metadata_service import _create_fallback_client
|
||||||
|
search_client = _create_fallback_client()
|
||||||
|
source_name = 'fallback'
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not search_client or not hasattr(search_client, 'search_albums'):
|
||||||
|
return years
|
||||||
|
|
||||||
|
# Cap lookups to avoid excessive API calls
|
||||||
|
max_lookups = 50
|
||||||
|
pairs_list = list(missing_pairs)[:max_lookups]
|
||||||
|
logger.info("Looking up %d album years from %s API", len(pairs_list), source_name)
|
||||||
|
|
||||||
|
if context.report_progress:
|
||||||
|
context.report_progress(
|
||||||
|
phase=f'Looking up {len(pairs_list)} album years from {source_name}...',
|
||||||
|
log_line=f'Fetching release years for {len(pairs_list)} albums',
|
||||||
|
log_type='info'
|
||||||
|
)
|
||||||
|
|
||||||
|
for artist, album in pairs_list:
|
||||||
|
if context.check_stop():
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
results = search_client.search_albums(f"{artist} {album}", limit=3)
|
||||||
|
if results:
|
||||||
|
for r in results:
|
||||||
|
release_date = getattr(r, 'release_date', '') or ''
|
||||||
|
if release_date and len(release_date) >= 4:
|
||||||
|
year_str = release_date[:4]
|
||||||
|
if year_str.isdigit():
|
||||||
|
key = (artist.lower(), album.lower())
|
||||||
|
years[key] = year_str
|
||||||
|
break
|
||||||
|
import time
|
||||||
|
time.sleep(0.1) # Rate limit courtesy
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("API year lookup failed for %s - %s: %s", artist, album, e)
|
||||||
|
|
||||||
|
logger.info("API year lookup: found %d/%d years", len(years), len(pairs_list))
|
||||||
|
return years
|
||||||
|
|
||||||
def _update_db_path(self, db, old_path: str, new_path: str, transfer_folder: str = ''):
|
def _update_db_path(self, db, old_path: str, new_path: str, transfer_folder: str = ''):
|
||||||
"""Update file_path in the tracks table when a file is moved.
|
"""Update file_path in the tracks table when a file is moved.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue