Merge pull request #516 from Nezreka/fix/silent-exception-swallowing
Fix/silent exception swallowing
This commit is contained in:
commit
627d32cebd
92 changed files with 743 additions and 704 deletions
|
|
@ -2,10 +2,14 @@
|
||||||
Search endpoints — search external sources (Spotify, iTunes, Hydrabase).
|
Search endpoints — search external sources (Spotify, iTunes, Hydrabase).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from flask import request, current_app
|
from flask import request, current_app
|
||||||
from .auth import require_api_key
|
from .auth import require_api_key
|
||||||
from .helpers import api_success, api_error
|
from .helpers import api_success, api_error
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def register_routes(bp):
|
def register_routes(bp):
|
||||||
|
|
||||||
|
|
@ -38,8 +42,8 @@ def register_routes(bp):
|
||||||
if hydra_results:
|
if hydra_results:
|
||||||
tracks = [_serialize_track(t) for t in hydra_results]
|
tracks = [_serialize_track(t) for t in hydra_results]
|
||||||
return api_success({"tracks": tracks, "source": "hydrabase"})
|
return api_success({"tracks": tracks, "source": "hydrabase"})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hydrabase search failed: %s", e)
|
||||||
|
|
||||||
spotify = ctx.get("spotify_client")
|
spotify = ctx.get("spotify_client")
|
||||||
from core.metadata_service import get_primary_source, get_primary_client
|
from core.metadata_service import get_primary_source, get_primary_client
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@
|
||||||
System endpoints — status, activity feed, stats.
|
System endpoints — status, activity feed, stats.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from .auth import require_api_key
|
from .auth import require_api_key
|
||||||
from .helpers import api_success, api_error
|
from .helpers import api_success, api_error
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def register_routes(bp):
|
def register_routes(bp):
|
||||||
|
|
||||||
|
|
@ -35,8 +38,8 @@ def register_routes(bp):
|
||||||
try:
|
try:
|
||||||
ws, _ = hydrabase.get_ws_and_lock()
|
ws, _ = hydrabase.get_ws_and_lock()
|
||||||
hydrabase_ok = ws is not None and ws.connected
|
hydrabase_ok = ws is not None and ws.connected
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hydrabase status probe failed: %s", e)
|
||||||
|
|
||||||
return api_success({
|
return api_success({
|
||||||
"uptime": f"{hours}h {minutes}m {seconds}s",
|
"uptime": f"{hours}h {minutes}m {seconds}s",
|
||||||
|
|
|
||||||
|
|
@ -2612,7 +2612,7 @@ class BeatportUnifiedScraper:
|
||||||
result['title'] = title
|
result['title'] = title
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass # Silently handle URL extraction errors
|
logger.debug("URL slug extraction: %s", e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -3186,8 +3186,8 @@ class BeatportUnifiedScraper:
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("parse release tracks failed: %s", e)
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,8 +195,8 @@ def _find_best_release(album_name, artist_name, track_count, mb_service):
|
||||||
sr_id = sr.get('id', '')
|
sr_id = sr.get('id', '')
|
||||||
if sr_id and sr_id not in candidate_mbids:
|
if sr_id and sr_id not in candidate_mbids:
|
||||||
candidate_mbids.append(sr_id)
|
candidate_mbids.append(sr_id)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("search_release fallback failed: %s", e)
|
||||||
|
|
||||||
if not candidate_mbids:
|
if not candidate_mbids:
|
||||||
logger.info(f"No MB release found for '{album_name}' by '{artist_name}'")
|
logger.info(f"No MB release found for '{album_name}' by '{artist_name}'")
|
||||||
|
|
|
||||||
|
|
@ -302,8 +302,8 @@ class ApiCallTracker:
|
||||||
try:
|
try:
|
||||||
if os.path.exists(_PERSIST_PATH + '.tmp'):
|
if os.path.exists(_PERSIST_PATH + '.tmp'):
|
||||||
os.remove(_PERSIST_PATH + '.tmp')
|
os.remove(_PERSIST_PATH + '.tmp')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("remove stale tmp file failed: %s", e)
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
"""Restore 24h minute history from disk. Called on init."""
|
"""Restore 24h minute history from disk. Called on init."""
|
||||||
|
|
|
||||||
|
|
@ -89,20 +89,20 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
|
||||||
search_clients['spotify'] = spotify_client
|
search_clients['spotify'] = spotify_client
|
||||||
try:
|
try:
|
||||||
search_clients['itunes'] = _get_itunes_client()
|
search_clients['itunes'] = _get_itunes_client()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("itunes client init failed: %s", e)
|
||||||
try:
|
try:
|
||||||
search_clients['deezer'] = _get_deezer_client()
|
search_clients['deezer'] = _get_deezer_client()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("deezer client init failed: %s", e)
|
||||||
try:
|
try:
|
||||||
dc = _get_discogs_client()
|
dc = _get_discogs_client()
|
||||||
# Only use Discogs if token is configured
|
# Only use Discogs if token is configured
|
||||||
from config.settings import config_manager as _cm
|
from config.settings import config_manager as _cm
|
||||||
if _cm.get('discogs.token', ''):
|
if _cm.get('discogs.token', ''):
|
||||||
search_clients['discogs'] = dc
|
search_clients['discogs'] = dc
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discogs client init failed: %s", e)
|
||||||
|
|
||||||
# Reuse watchlist scanner's fuzzy matching logic
|
# Reuse watchlist scanner's fuzzy matching logic
|
||||||
from core.watchlist_scanner import WatchlistScanner
|
from core.watchlist_scanner import WatchlistScanner
|
||||||
|
|
@ -167,8 +167,8 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
|
||||||
harvested_ids[col] = str(r[col])
|
harvested_ids[col] = str(r[col])
|
||||||
if _valid_image(r.get('thumb_url')):
|
if _valid_image(r.get('thumb_url')):
|
||||||
best_image = r['thumb_url']
|
best_image = r['thumb_url']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("library artist lookup failed: %s", e)
|
||||||
|
|
||||||
# 2. Watchlist artists
|
# 2. Watchlist artists
|
||||||
try:
|
try:
|
||||||
|
|
@ -186,8 +186,8 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
|
||||||
harvested_ids[col] = str(wl[col])
|
harvested_ids[col] = str(wl[col])
|
||||||
if _valid_image(wl.get('image_url')) and not best_image:
|
if _valid_image(wl.get('image_url')) and not best_image:
|
||||||
best_image = wl['image_url']
|
best_image = wl['image_url']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist artist lookup failed: %s", e)
|
||||||
|
|
||||||
# 3. Metadata cache (all sources)
|
# 3. Metadata cache (all sources)
|
||||||
try:
|
try:
|
||||||
|
|
@ -203,8 +203,8 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
|
||||||
harvested_ids[col] = row['entity_id']
|
harvested_ids[col] = row['entity_id']
|
||||||
if _valid_image(row['image_url']) and not best_image:
|
if _valid_image(row['image_url']) and not best_image:
|
||||||
best_image = row['image_url']
|
best_image = row['image_url']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("metadata cache lookup failed: %s", e)
|
||||||
|
|
||||||
# --- API STRATEGIES (search each missing source) ---
|
# --- API STRATEGIES (search each missing source) ---
|
||||||
# Same pattern as watchlist scanner's _backfill_missing_ids
|
# Same pattern as watchlist scanner's _backfill_missing_ids
|
||||||
|
|
@ -287,8 +287,8 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic
|
||||||
artist_data = sp.sp.artist(r['spotify_artist_id'])
|
artist_data = sp.sp.artist(r['spotify_artist_id'])
|
||||||
if artist_data and artist_data.get('images'):
|
if artist_data and artist_data.get('images'):
|
||||||
image_url = artist_data['images'][0]['url']
|
image_url = artist_data['images'][0]['url']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify artist image fetch failed: %s", e)
|
||||||
|
|
||||||
# Try Deezer (direct image URL from ID)
|
# Try Deezer (direct image URL from ID)
|
||||||
if not image_url and r.get('deezer_artist_id'):
|
if not image_url and r.get('deezer_artist_id'):
|
||||||
|
|
@ -302,8 +302,8 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic
|
||||||
(image_url, r['id'])
|
(image_url, r['id'])
|
||||||
)
|
)
|
||||||
filled += 1
|
filled += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("liked artist image update failed: %s", e)
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
|
||||||
|
|
@ -158,8 +158,8 @@ def get_artist_map_data():
|
||||||
if r.get('genres'):
|
if r.get('genres'):
|
||||||
try:
|
try:
|
||||||
genres = json.loads(r['genres'])
|
genres = json.loads(r['genres'])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("similar node genres parse failed: %s", e)
|
||||||
nodes.append({
|
nodes.append({
|
||||||
'id': idx,
|
'id': idx,
|
||||||
'name': r['similar_artist_name'],
|
'name': r['similar_artist_name'],
|
||||||
|
|
@ -232,8 +232,8 @@ def get_artist_map_data():
|
||||||
if cr['genres']:
|
if cr['genres']:
|
||||||
try:
|
try:
|
||||||
genres = json.loads(cr['genres']) if isinstance(cr['genres'], str) else []
|
genres = json.loads(cr['genres']) if isinstance(cr['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("backfill cache genres parse failed: %s", e)
|
||||||
cache_by_name[cn][source] = {
|
cache_by_name[cn][source] = {
|
||||||
'id': cr['entity_id'],
|
'id': cr['entity_id'],
|
||||||
'image_url': cr['image_url'] or '',
|
'image_url': cr['image_url'] or '',
|
||||||
|
|
@ -280,8 +280,8 @@ def get_artist_map_data():
|
||||||
an = _norm(r['artist_name'])
|
an = _norm(r['artist_name'])
|
||||||
if an and an not in _album_art:
|
if an and an not in _album_art:
|
||||||
_album_art[an] = r['image_url']
|
_album_art[an] = r['image_url']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist map album-art cache build failed: %s", e)
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
if not n.get('image_url') or not n['image_url'].startswith('http'):
|
if not n.get('image_url') or not n['image_url'].startswith('http'):
|
||||||
nn = _norm(n['name'])
|
nn = _norm(n['name'])
|
||||||
|
|
@ -330,8 +330,8 @@ def get_artist_map_genre_list():
|
||||||
if g and isinstance(g, str):
|
if g and isinstance(g, str):
|
||||||
gl = g.lower().strip()
|
gl = g.lower().strip()
|
||||||
genre_counts[gl] = genre_counts.get(gl, 0) + 1
|
genre_counts[gl] = genre_counts.get(gl, 0) + 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("genre count row parse failed: %s", e)
|
||||||
|
|
||||||
# Sort by count descending
|
# Sort by count descending
|
||||||
sorted_genres = sorted(genre_counts.items(), key=lambda x: -x[1])
|
sorted_genres = sorted(genre_counts.items(), key=lambda x: -x[1])
|
||||||
|
|
@ -404,8 +404,8 @@ def get_artist_map_genres():
|
||||||
if r['genres']:
|
if r['genres']:
|
||||||
try:
|
try:
|
||||||
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache artist genres parse failed: %s", e)
|
||||||
src_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'}
|
src_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'}
|
||||||
kwargs = {src_map.get(r['source'], 'spotify_id'): r['entity_id']}
|
kwargs = {src_map.get(r['source'], 'spotify_id'): r['entity_id']}
|
||||||
_add(r['name'], image_url=r['image_url'], genres=genres, source='cache', popularity=r['popularity'] or 0, **kwargs)
|
_add(r['name'], image_url=r['image_url'], genres=genres, source='cache', popularity=r['popularity'] or 0, **kwargs)
|
||||||
|
|
@ -421,8 +421,8 @@ def get_artist_map_genres():
|
||||||
if r['genres']:
|
if r['genres']:
|
||||||
try:
|
try:
|
||||||
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("similar artist genres parse failed: %s", e)
|
||||||
_add(r['similar_artist_name'], image_url=r['image_url'], genres=genres,
|
_add(r['similar_artist_name'], image_url=r['image_url'], genres=genres,
|
||||||
spotify_id=r['similar_artist_spotify_id'], itunes_id=r['similar_artist_itunes_id'],
|
spotify_id=r['similar_artist_spotify_id'], itunes_id=r['similar_artist_itunes_id'],
|
||||||
deezer_id=r['similar_artist_deezer_id'], source='similar', popularity=r['popularity'] or 0)
|
deezer_id=r['similar_artist_deezer_id'], source='similar', popularity=r['popularity'] or 0)
|
||||||
|
|
@ -445,8 +445,8 @@ def get_artist_map_genres():
|
||||||
if r['genres']:
|
if r['genres']:
|
||||||
try:
|
try:
|
||||||
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("library artist genres parse failed: %s", e)
|
||||||
img = r['thumb_url'] if r['thumb_url'] and r['thumb_url'].startswith('http') else None
|
img = r['thumb_url'] if r['thumb_url'] and r['thumb_url'].startswith('http') else None
|
||||||
_add(r['name'], image_url=img, genres=genres, source='library')
|
_add(r['name'], image_url=img, genres=genres, source='library')
|
||||||
|
|
||||||
|
|
@ -550,8 +550,8 @@ def get_artist_map_genres():
|
||||||
nn = (r['artist_name'] or '').lower().strip()
|
nn = (r['artist_name'] or '').lower().strip()
|
||||||
if nn and nn not in _album_art_cache:
|
if nn and nn not in _album_art_cache:
|
||||||
_album_art_cache[nn] = r['image_url']
|
_album_art_cache[nn] = r['image_url']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("genre map cache build failed: %s", e)
|
||||||
|
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
img = n.get('image_url', '')
|
img = n.get('image_url', '')
|
||||||
|
|
@ -650,8 +650,8 @@ def get_artist_map_explore():
|
||||||
if row['genres']:
|
if row['genres']:
|
||||||
try:
|
try:
|
||||||
center_genres = json.loads(row['genres']) if isinstance(row['genres'], str) else []
|
center_genres = json.loads(row['genres']) if isinstance(row['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("initial center genres parse failed: %s", e)
|
||||||
|
|
||||||
# Check watchlist + library if not in cache
|
# Check watchlist + library if not in cache
|
||||||
if not artist_found and not artist_id:
|
if not artist_found and not artist_id:
|
||||||
|
|
@ -722,8 +722,8 @@ def get_artist_map_explore():
|
||||||
if r['genres'] and not center_genres:
|
if r['genres'] and not center_genres:
|
||||||
try:
|
try:
|
||||||
center_genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
center_genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("center genres parse failed: %s", e)
|
||||||
|
|
||||||
# Add center node
|
# Add center node
|
||||||
center_idx = 0
|
center_idx = 0
|
||||||
|
|
@ -787,8 +787,8 @@ def get_artist_map_explore():
|
||||||
popularity=sa.get('popularity', 0),
|
popularity=sa.get('popularity', 0),
|
||||||
similar_artist_deezer_id=sa.get('deezer_id')
|
similar_artist_deezer_id=sa.get('deezer_id')
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("similar artist insert failed: %s", e)
|
||||||
# Re-query from DB to get consistent format
|
# Re-query from DB to get consistent format
|
||||||
if id_values:
|
if id_values:
|
||||||
placeholders = ','.join(['?'] * len(id_values))
|
placeholders = ','.join(['?'] * len(id_values))
|
||||||
|
|
@ -828,8 +828,8 @@ def get_artist_map_explore():
|
||||||
if r['genres']:
|
if r['genres']:
|
||||||
try:
|
try:
|
||||||
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("ring1 genres parse failed: %s", e)
|
||||||
img = r['image_url'] if r['image_url'] and r['image_url'].startswith('http') else ''
|
img = r['image_url'] if r['image_url'] and r['image_url'].startswith('http') else ''
|
||||||
nodes.append({
|
nodes.append({
|
||||||
'id': idx, 'name': r['similar_artist_name'], 'image_url': img,
|
'id': idx, 'name': r['similar_artist_name'], 'image_url': img,
|
||||||
|
|
@ -889,8 +889,8 @@ def get_artist_map_explore():
|
||||||
if r['genres']:
|
if r['genres']:
|
||||||
try:
|
try:
|
||||||
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
genres = json.loads(r['genres']) if isinstance(r['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("ring2 genres parse failed: %s", e)
|
||||||
img = r['image_url'] if r['image_url'] and r['image_url'].startswith('http') else ''
|
img = r['image_url'] if r['image_url'] and r['image_url'].startswith('http') else ''
|
||||||
nodes.append({
|
nodes.append({
|
||||||
'id': idx, 'name': r['similar_artist_name'], 'image_url': img,
|
'id': idx, 'name': r['similar_artist_name'], 'image_url': img,
|
||||||
|
|
@ -928,8 +928,8 @@ def get_artist_map_explore():
|
||||||
if not n['genres'] and cr['genres']:
|
if not n['genres'] and cr['genres']:
|
||||||
try:
|
try:
|
||||||
n['genres'] = json.loads(cr['genres'])[:5] if isinstance(cr['genres'], str) else []
|
n['genres'] = json.loads(cr['genres'])[:5] if isinstance(cr['genres'], str) else []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("explorer node genres parse failed: %s", e)
|
||||||
# Harvest missing IDs from cache
|
# Harvest missing IDs from cache
|
||||||
src_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'}
|
src_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'}
|
||||||
k = src_map.get(cr['source'])
|
k = src_map.get(cr['source'])
|
||||||
|
|
@ -951,8 +951,8 @@ def get_artist_map_explore():
|
||||||
n['image_url'] = artist_data['images'][0]['url']
|
n['image_url'] = artist_data['images'][0]['url']
|
||||||
if not n['genres'] and artist_data.get('genres'):
|
if not n['genres'] and artist_data.get('genres'):
|
||||||
n['genres'] = artist_data['genres'][:5]
|
n['genres'] = artist_data['genres'][:5]
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify artist image fallback failed: %s", e)
|
||||||
|
|
||||||
# Album art fallback (iTunes artists have no artist images)
|
# Album art fallback (iTunes artists have no artist images)
|
||||||
if not n['image_url']:
|
if not n['image_url']:
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,8 @@ class AudioDBWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null id table resolve failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -596,8 +596,8 @@ class AutoImportWorker:
|
||||||
# Keep weak AcoustID result as fallback
|
# Keep weak AcoustID result as fallback
|
||||||
if fp_result2 and (not result or fp_result2.get('identification_confidence', 0) > result.get('identification_confidence', 0)):
|
if fp_result2 and (not result or fp_result2.get('identification_confidence', 0) > result.get('identification_confidence', 0)):
|
||||||
result = fp_result2
|
result = fp_result2
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("acoustid fingerprint fallback failed: %s", e)
|
||||||
|
|
||||||
# If we have good tag data (artist + title), prefer tag-based identification
|
# If we have good tag data (artist + title), prefer tag-based identification
|
||||||
# over a weak metadata/AcoustID result — tags from post-processed files are reliable
|
# over a weak metadata/AcoustID result — tags from post-processed files are reliable
|
||||||
|
|
@ -1100,8 +1100,8 @@ class AutoImportWorker:
|
||||||
if folder_artist and folder_artist.lower() != artist_name.lower():
|
if folder_artist and folder_artist.lower() != artist_name.lower():
|
||||||
logger.info(f"[Auto-Import] Parent folder artist '{folder_artist}' differs from tag artist '{artist_name}' — using folder artist")
|
logger.info(f"[Auto-Import] Parent folder artist '{folder_artist}' differs from tag artist '{artist_name}' — using folder artist")
|
||||||
artist_name = folder_artist
|
artist_name = folder_artist
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("folder artist override failed: %s", e)
|
||||||
release_date = identification.get('release_date', '') or album_data.get('release_date', '')
|
release_date = identification.get('release_date', '') or album_data.get('release_date', '')
|
||||||
|
|
||||||
# Compute total discs
|
# Compute total discs
|
||||||
|
|
@ -1202,8 +1202,8 @@ class AutoImportWorker:
|
||||||
'completed_tracks': str(processed),
|
'completed_tracks': str(processed),
|
||||||
'failed_tracks': str(len(errors)),
|
'failed_tracks': str(len(errors)),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("automation emit failed: %s", e)
|
||||||
|
|
||||||
return processed > 0
|
return processed > 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ def update_progress(
|
||||||
if socketio_emit is not None:
|
if socketio_emit is not None:
|
||||||
try:
|
try:
|
||||||
socketio_emit('automation:progress', {str(automation_id): dict(state)})
|
socketio_emit('automation:progress', {str(automation_id): dict(state)})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("socketio progress emit: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def get_running_progress() -> dict[str, dict]:
|
def get_running_progress() -> dict[str, dict]:
|
||||||
|
|
@ -121,8 +121,8 @@ def record_history(
|
||||||
t0 = datetime.fromisoformat(started_at)
|
t0 = datetime.fromisoformat(started_at)
|
||||||
t1 = datetime.fromisoformat(finished_at)
|
t1 = datetime.fromisoformat(finished_at)
|
||||||
duration = (t1 - t0).total_seconds()
|
duration = (t1 - t0).total_seconds()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("duration parse: %s", e)
|
||||||
|
|
||||||
r_status = result.get('status', 'completed') if result else 'completed'
|
r_status = result.get('status', 'completed') if result else 'completed'
|
||||||
if r_status == 'error':
|
if r_status == 'error':
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ names from the saved automation set so the builder UI can autocomplete.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def collect_known_signals(database) -> list[str]:
|
def collect_known_signals(database) -> list[str]:
|
||||||
|
|
@ -38,6 +41,6 @@ def collect_known_signals(database) -> list[str]:
|
||||||
signals.add(sig)
|
signals.add(sig)
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("collect known signals failed: %s", e)
|
||||||
return sorted(signals)
|
return sorted(signals)
|
||||||
|
|
|
||||||
|
|
@ -455,8 +455,10 @@ class AutomationEngine:
|
||||||
if delay_minutes and delay_minutes > 0:
|
if delay_minutes and delay_minutes > 0:
|
||||||
# Initialize progress BEFORE delay so card glows during wait
|
# Initialize progress BEFORE delay so card glows during wait
|
||||||
if self._progress_init_fn:
|
if self._progress_init_fn:
|
||||||
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
try:
|
||||||
except Exception: pass
|
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("event progress init (delay): %s", e)
|
||||||
_delay_already_inited = True
|
_delay_already_inited = True
|
||||||
|
|
||||||
delay_seconds = int(delay_minutes) * 60
|
delay_seconds = int(delay_minutes) * 60
|
||||||
|
|
@ -486,13 +488,17 @@ class AutomationEngine:
|
||||||
logger.info(f"Event automation '{auto.get('name')}' skipped — {action_type} busy")
|
logger.info(f"Event automation '{auto.get('name')}' skipped — {action_type} busy")
|
||||||
# If progress was initialized during delay, finalize it
|
# If progress was initialized during delay, finalize it
|
||||||
if _delay_already_inited and self._progress_finish_fn:
|
if _delay_already_inited and self._progress_finish_fn:
|
||||||
try: self._progress_finish_fn(automation_id, result)
|
try:
|
||||||
except Exception: pass
|
self._progress_finish_fn(automation_id, result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("event progress finish (skipped): %s", e)
|
||||||
else:
|
else:
|
||||||
# Initialize progress tracking (skip if already done during delay)
|
# Initialize progress tracking (skip if already done during delay)
|
||||||
if not _delay_already_inited and self._progress_init_fn:
|
if not _delay_already_inited and self._progress_init_fn:
|
||||||
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
try:
|
||||||
except Exception: pass
|
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("event progress init: %s", e)
|
||||||
try:
|
try:
|
||||||
result = handler_info['handler'](action_config) or {}
|
result = handler_info['handler'](action_config) or {}
|
||||||
logger.info(f"Event automation '{auto.get('name')}' executed: {result.get('status', 'ok')}")
|
logger.info(f"Event automation '{auto.get('name')}' executed: {result.get('status', 'ok')}")
|
||||||
|
|
@ -501,8 +507,10 @@ class AutomationEngine:
|
||||||
logger.error(f"Event automation '{auto.get('name')}' action failed: {e}")
|
logger.error(f"Event automation '{auto.get('name')}' action failed: {e}")
|
||||||
# Finalize progress tracking
|
# Finalize progress tracking
|
||||||
if self._progress_finish_fn:
|
if self._progress_finish_fn:
|
||||||
try: self._progress_finish_fn(automation_id, result)
|
try:
|
||||||
except Exception: pass
|
self._progress_finish_fn(automation_id, result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("event progress finish: %s", e)
|
||||||
|
|
||||||
# Merge event data into result for then-action variables
|
# Merge event data into result for then-action variables
|
||||||
merged = {**event_data, **result}
|
merged = {**event_data, **result}
|
||||||
|
|
@ -531,8 +539,8 @@ class AutomationEngine:
|
||||||
if self._history_record_fn:
|
if self._history_record_fn:
|
||||||
try:
|
try:
|
||||||
self._history_record_fn(automation_id, result)
|
self._history_record_fn(automation_id, result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("history record failed: %s", e)
|
||||||
|
|
||||||
# --- Schedule Execution (timer-based) ---
|
# --- Schedule Execution (timer-based) ---
|
||||||
|
|
||||||
|
|
@ -580,8 +588,10 @@ class AutomationEngine:
|
||||||
if not skip_delay and delay_minutes and delay_minutes > 0:
|
if not skip_delay and delay_minutes and delay_minutes > 0:
|
||||||
# Initialize progress BEFORE delay so card glows during wait
|
# Initialize progress BEFORE delay so card glows during wait
|
||||||
if self._progress_init_fn:
|
if self._progress_init_fn:
|
||||||
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
try:
|
||||||
except Exception: pass
|
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("scheduled progress init (delay): %s", e)
|
||||||
_delay_already_inited = True
|
_delay_already_inited = True
|
||||||
|
|
||||||
delay_seconds = int(delay_minutes) * 60
|
delay_seconds = int(delay_minutes) * 60
|
||||||
|
|
@ -603,15 +613,19 @@ class AutomationEngine:
|
||||||
logger.info(f"Automation '{auto['name']}' skipped — {action_type} already running")
|
logger.info(f"Automation '{auto['name']}' skipped — {action_type} already running")
|
||||||
# If progress was initialized during delay, finalize it
|
# If progress was initialized during delay, finalize it
|
||||||
if _delay_already_inited and self._progress_finish_fn:
|
if _delay_already_inited and self._progress_finish_fn:
|
||||||
try: self._progress_finish_fn(automation_id, result)
|
try:
|
||||||
except Exception: pass
|
self._progress_finish_fn(automation_id, result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("scheduled progress finish (skipped): %s", e)
|
||||||
self._finish_run(auto, automation_id, result, error=None)
|
self._finish_run(auto, automation_id, result, error=None)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initialize progress tracking (skip if already done during delay)
|
# Initialize progress tracking (skip if already done during delay)
|
||||||
if not _delay_already_inited and self._progress_init_fn:
|
if not _delay_already_inited and self._progress_init_fn:
|
||||||
try: self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
try:
|
||||||
except Exception: pass
|
self._progress_init_fn(automation_id, auto.get('name', ''), action_type)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("scheduled progress init: %s", e)
|
||||||
|
|
||||||
# Execute the action
|
# Execute the action
|
||||||
error = None
|
error = None
|
||||||
|
|
@ -637,8 +651,10 @@ class AutomationEngine:
|
||||||
|
|
||||||
# Finalize progress tracking
|
# Finalize progress tracking
|
||||||
if self._progress_finish_fn:
|
if self._progress_finish_fn:
|
||||||
try: self._progress_finish_fn(automation_id, result)
|
try:
|
||||||
except Exception: pass
|
self._progress_finish_fn(automation_id, result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("scheduled progress finish: %s", e)
|
||||||
|
|
||||||
# Execute then-actions (notifications + fire_signal)
|
# Execute then-actions (notifications + fire_signal)
|
||||||
try:
|
try:
|
||||||
|
|
@ -673,8 +689,8 @@ class AutomationEngine:
|
||||||
delay = self._calc_delay_seconds(trigger_config)
|
delay = self._calc_delay_seconds(trigger_config)
|
||||||
if delay:
|
if delay:
|
||||||
next_run_str = _utc_after(delay)
|
next_run_str = _utc_after(delay)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("next run calc failed: %s", e)
|
||||||
|
|
||||||
last_result = json.dumps(result) if result else None
|
last_result = json.dumps(result) if result else None
|
||||||
self.db.update_automation_run(automation_id, next_run=next_run_str, error=error, last_result=last_result)
|
self.db.update_automation_run(automation_id, next_run=next_run_str, error=error, last_result=last_result)
|
||||||
|
|
@ -682,8 +698,8 @@ class AutomationEngine:
|
||||||
if self._history_record_fn:
|
if self._history_record_fn:
|
||||||
try:
|
try:
|
||||||
self._history_record_fn(automation_id, result)
|
self._history_record_fn(automation_id, result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("history record failed: %s", e)
|
||||||
|
|
||||||
if self._running:
|
if self._running:
|
||||||
self.schedule_automation(automation_id)
|
self.schedule_automation(automation_id)
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ def run_detection(server_type):
|
||||||
api_response = requests.get(api_url, timeout=1)
|
api_response = requests.get(api_url, timeout=1)
|
||||||
if api_response.status_code == 200 and 'MediaContainer' in api_response.text:
|
if api_response.status_code == 200 and 'MediaContainer' in api_response.text:
|
||||||
return f"http://{ip}:{port}"
|
return f"http://{ip}:{port}"
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex probe %s: %s", ip, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_jellyfin_server(ip, port=8096):
|
def test_jellyfin_server(ip, port=8096):
|
||||||
|
|
@ -101,9 +101,9 @@ def run_detection(server_type):
|
||||||
web_response = requests.get(web_url, timeout=1)
|
web_response = requests.get(web_url, timeout=1)
|
||||||
if web_response.status_code == 200 and 'jellyfin' in web_response.text.lower():
|
if web_response.status_code == 200 and 'jellyfin' in web_response.text.lower():
|
||||||
return f"http://{ip}:{port}"
|
return f"http://{ip}:{port}"
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("jellyfin probe %s: %s", ip, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_slskd_server(ip, port=5030):
|
def test_slskd_server(ip, port=5030):
|
||||||
|
|
@ -117,8 +117,8 @@ def run_detection(server_type):
|
||||||
if response.status_code in [200, 401]:
|
if response.status_code in [200, 401]:
|
||||||
return f"http://{ip}:{port}"
|
return f"http://{ip}:{port}"
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("slskd probe %s: %s", ip, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_navidrome_server(ip, port=4533):
|
def test_navidrome_server(ip, port=4533):
|
||||||
|
|
@ -140,8 +140,8 @@ def run_detection(server_type):
|
||||||
# Check for Subsonic/Navidrome API response structure
|
# Check for Subsonic/Navidrome API response structure
|
||||||
if 'subsonic-response' in data:
|
if 'subsonic-response' in data:
|
||||||
return f"http://{ip}:{port}"
|
return f"http://{ip}:{port}"
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("navidrome json parse: %s", e)
|
||||||
|
|
||||||
# Also try the web interface
|
# Also try the web interface
|
||||||
web_url = f"http://{ip}:{port}/"
|
web_url = f"http://{ip}:{port}/"
|
||||||
|
|
@ -149,8 +149,8 @@ def run_detection(server_type):
|
||||||
if web_response.status_code == 200 and 'navidrome' in web_response.text.lower():
|
if web_response.status_code == 200 and 'navidrome' in web_response.text.lower():
|
||||||
return f"http://{ip}:{port}"
|
return f"http://{ip}:{port}"
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("navidrome probe %s: %s", ip, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1051,8 +1051,8 @@ class DatabaseUpdateWorker:
|
||||||
batch + [self.server_type])
|
batch + [self.server_type])
|
||||||
cascade_album_ids.update(row[0] for row in cursor.fetchall())
|
cascade_album_ids.update(row[0] for row in cursor.fetchall())
|
||||||
removed_album_ids -= cascade_album_ids
|
removed_album_ids -= cascade_album_ids
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # If this optimization fails, double-delete is harmless
|
logger.debug("cascade album cleanup optimization: %s", e)
|
||||||
|
|
||||||
if not removed_artist_ids and not removed_album_ids:
|
if not removed_artist_ids and not removed_album_ids:
|
||||||
logger.info("Removal detection: no stale content found")
|
logger.info("Removal detection: no stale content found")
|
||||||
|
|
|
||||||
|
|
@ -260,8 +260,8 @@ def get_debug_info():
|
||||||
for _pid, st in list(tidal_discovery_states.items()):
|
for _pid, st in list(tidal_discovery_states.items()):
|
||||||
if st.get('phase') == 'syncing':
|
if st.get('phase') == 'syncing':
|
||||||
active_syncs += 1
|
active_syncs += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("count active syncs failed: %s", e)
|
||||||
info['active_downloads'] = active_downloads
|
info['active_downloads'] = active_downloads
|
||||||
info['active_syncs'] = active_syncs
|
info['active_syncs'] = active_syncs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -307,8 +307,8 @@ class DeezerClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
tracks.append(Track.from_deezer_track(raw))
|
tracks.append(Track.from_deezer_track(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Track.from_deezer_track cache parse: %s", e)
|
||||||
if tracks:
|
if tracks:
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
@ -341,8 +341,8 @@ class DeezerClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
artists.append(Artist.from_deezer_artist(raw))
|
artists.append(Artist.from_deezer_artist(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Artist.from_deezer_artist cache parse: %s", e)
|
||||||
if artists:
|
if artists:
|
||||||
return artists
|
return artists
|
||||||
|
|
||||||
|
|
@ -375,8 +375,8 @@ class DeezerClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
albums.append(Album.from_deezer_album(raw))
|
albums.append(Album.from_deezer_album(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Album.from_deezer_album cache parse: %s", e)
|
||||||
if albums:
|
if albums:
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
|
|
@ -842,8 +842,8 @@ class DeezerClient:
|
||||||
try:
|
try:
|
||||||
cache = get_metadata_cache()
|
cache = get_metadata_cache()
|
||||||
cache.store_entity('deezer', 'artist', str(result.get('id', '')), result)
|
cache.store_entity('deezer', 'artist', str(result.get('id', '')), result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache store_entity artist search: %s", e)
|
||||||
logger.debug(f"Found artist for query: {artist_name}")
|
logger.debug(f"Found artist for query: {artist_name}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -887,8 +887,8 @@ class DeezerClient:
|
||||||
try:
|
try:
|
||||||
cache = get_metadata_cache()
|
cache = get_metadata_cache()
|
||||||
cache.store_entity('deezer', 'album', str(result.get('id', '')), result)
|
cache.store_entity('deezer', 'album', str(result.get('id', '')), result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache store_entity album search: %s", e)
|
||||||
logger.debug(f"Found album for query: {artist_name} - {album_title}")
|
logger.debug(f"Found album for query: {artist_name} - {album_title}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -932,8 +932,8 @@ class DeezerClient:
|
||||||
try:
|
try:
|
||||||
cache = get_metadata_cache()
|
cache = get_metadata_cache()
|
||||||
cache.store_entity('deezer', 'track', str(result.get('id', '')), result)
|
cache.store_entity('deezer', 'track', str(result.get('id', '')), result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache store_entity track search: %s", e)
|
||||||
logger.debug(f"Found track for query: {artist_name} - {track_title}")
|
logger.debug(f"Found track for query: {artist_name} - {track_title}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -965,8 +965,8 @@ class DeezerClient:
|
||||||
# Cache hit with full details (has label = was a get_album response, not just search)
|
# Cache hit with full details (has label = was a get_album response, not just search)
|
||||||
logger.debug(f"Cache hit for album {album_id}")
|
logger.debug(f"Cache hit for album {album_id}")
|
||||||
return cached
|
return cached
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache get_entity album: %s", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
|
|
@ -984,8 +984,8 @@ class DeezerClient:
|
||||||
try:
|
try:
|
||||||
cache = get_metadata_cache()
|
cache = get_metadata_cache()
|
||||||
cache.store_entity('deezer', 'album', str(album_id), data)
|
cache.store_entity('deezer', 'album', str(album_id), data)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache store_entity album full: %s", e)
|
||||||
logger.debug(f"Got full album details for ID: {album_id}")
|
logger.debug(f"Got full album details for ID: {album_id}")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -1013,8 +1013,8 @@ class DeezerClient:
|
||||||
if cached and cached.get('bpm'):
|
if cached and cached.get('bpm'):
|
||||||
logger.debug(f"Cache hit for track {track_id}")
|
logger.debug(f"Cache hit for track {track_id}")
|
||||||
return cached
|
return cached
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache get_entity track: %s", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
|
|
@ -1032,8 +1032,8 @@ class DeezerClient:
|
||||||
try:
|
try:
|
||||||
cache = get_metadata_cache()
|
cache = get_metadata_cache()
|
||||||
cache.store_entity('deezer', 'track', str(track_id), data)
|
cache.store_entity('deezer', 'track', str(track_id), data)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache store_entity track full: %s", e)
|
||||||
logger.debug(f"Got full track details for ID: {track_id}")
|
logger.debug(f"Got full track details for ID: {track_id}")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -430,8 +430,8 @@ class DeezerDownloadClient(DownloadSourcePlugin):
|
||||||
if cached and cached.get('release_date'):
|
if cached and cached.get('release_date'):
|
||||||
album_release_dates[aid] = cached['release_date']
|
album_release_dates[aid] = cached['release_date']
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache get_entity album release_date: %s", e)
|
||||||
# Cache miss — fetch from API
|
# Cache miss — fetch from API
|
||||||
try:
|
try:
|
||||||
time.sleep(0.3) # Respect rate limits
|
time.sleep(0.3) # Respect rate limits
|
||||||
|
|
@ -443,10 +443,10 @@ class DeezerDownloadClient(DownloadSourcePlugin):
|
||||||
if cache:
|
if cache:
|
||||||
try:
|
try:
|
||||||
cache.store_entity('deezer', 'album', aid, a_data)
|
cache.store_entity('deezer', 'album', aid, a_data)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache store_entity album release_date: %s", e)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("fetch deezer album release_date %s: %s", aid, e)
|
||||||
|
|
||||||
tracks = []
|
tracks = []
|
||||||
for i, t in enumerate(raw_tracks, start=1):
|
for i, t in enumerate(raw_tracks, start=1):
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,8 @@ class DeezerWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null id table resolve failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -320,8 +320,8 @@ class DiscogsClient:
|
||||||
try:
|
try:
|
||||||
from config.settings import config_manager
|
from config.settings import config_manager
|
||||||
self.token = config_manager.get('discogs.token', '')
|
self.token = config_manager.get('discogs.token', '')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("load discogs.token from config: %s", e)
|
||||||
|
|
||||||
if self.token:
|
if self.token:
|
||||||
self.session.headers['Authorization'] = f'Discogs token={self.token}'
|
self.session.headers['Authorization'] = f'Discogs token={self.token}'
|
||||||
|
|
@ -499,8 +499,8 @@ class DiscogsClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
artists.append(Artist.from_discogs_artist(raw))
|
artists.append(Artist.from_discogs_artist(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Artist.from_discogs_artist cache parse: %s", e)
|
||||||
if artists:
|
if artists:
|
||||||
return artists
|
return artists
|
||||||
|
|
||||||
|
|
@ -536,8 +536,8 @@ class DiscogsClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
albums.append(Album.from_discogs_release(raw))
|
albums.append(Album.from_discogs_release(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Album.from_discogs_release cache parse: %s", e)
|
||||||
if albums:
|
if albums:
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -298,8 +298,8 @@ class DiscogsWorker:
|
||||||
self.stats['errors'] += 1
|
self.stats['errors'] += 1
|
||||||
try:
|
try:
|
||||||
self._mark_status(item['type'], item['id'], 'error')
|
self._mark_status(item['type'], item['id'], 'error')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("mark item status error failed: %s", e)
|
||||||
|
|
||||||
def _get_existing_id(self, entity_type: str, entity_id) -> Optional[str]:
|
def _get_existing_id(self, entity_type: str, entity_id) -> Optional[str]:
|
||||||
"""Check if entity already has a discogs_id."""
|
"""Check if entity already has a discogs_id."""
|
||||||
|
|
|
||||||
|
|
@ -196,8 +196,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
|
||||||
current_item=track_name,
|
current_item=track_name,
|
||||||
log_line=f'{track_name} → {cached_match.get("name", "?")} (cache)', log_type='success')
|
log_line=f'{track_name} → {cached_match.get("name", "?")} (cache)', log_type='success')
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discovery cache lookup failed: %s", e)
|
||||||
|
|
||||||
# Step 2: Generate search queries
|
# Step 2: Generate search queries
|
||||||
try:
|
try:
|
||||||
|
|
@ -252,8 +252,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
|
||||||
if match and confidence > best_confidence:
|
if match and confidence > best_confidence:
|
||||||
best_confidence = confidence
|
best_confidence = confidence
|
||||||
best_match = match
|
best_match = match
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("extended discovery search failed: %s", e)
|
||||||
|
|
||||||
# Step 4: Store results
|
# Step 4: Store results
|
||||||
if best_match and best_confidence >= min_confidence:
|
if best_match and best_confidence >= min_confidence:
|
||||||
|
|
@ -290,8 +290,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
|
||||||
if _raw:
|
if _raw:
|
||||||
track_number = _raw.get('track_number')
|
track_number = _raw.get('track_number')
|
||||||
disc_number = _raw.get('disc_number')
|
disc_number = _raw.get('disc_number')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("metadata cache lookup for album enrichment failed: %s", e)
|
||||||
|
|
||||||
matched_data = {
|
matched_data = {
|
||||||
'id': best_match.id if hasattr(best_match, 'id') else '',
|
'id': best_match.id if hasattr(best_match, 'id') else '',
|
||||||
|
|
@ -323,8 +323,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
|
||||||
best_confidence, matched_data,
|
best_confidence, matched_data,
|
||||||
track_name, artist_name
|
track_name, artist_name
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("save discovery cache match failed: %s", e)
|
||||||
|
|
||||||
logger.info(f"[{i+1}/{len(undiscovered_tracks)}] {track_name} → {matched_data['name']} ({best_confidence:.2f})")
|
logger.info(f"[{i+1}/{len(undiscovered_tracks)}] {track_name} → {matched_data['name']} ({best_confidence:.2f})")
|
||||||
deps.update_automation_progress(automation_id,
|
deps.update_automation_progress(automation_id,
|
||||||
|
|
@ -366,8 +366,8 @@ def run_playlist_discovery_worker(playlists, automation_id=None, deps: PlaylistD
|
||||||
'failed_count': str(total_failed),
|
'failed_count': str(total_failed),
|
||||||
'skipped_count': str(total_skipped),
|
'skipped_count': str(total_skipped),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discovery_completed emit failed: %s", e)
|
||||||
|
|
||||||
logger.error(f"Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped")
|
logger.error(f"Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped")
|
||||||
deps.update_automation_progress(automation_id, status='finished', progress=100,
|
deps.update_automation_progress(automation_id, status='finished', progress=100,
|
||||||
|
|
|
||||||
|
|
@ -648,8 +648,8 @@ def run_quality_scanner(scope='watchlist', profile_id=1, deps: QualityScannerDep
|
||||||
'low_quality': str(deps.quality_scanner_state.get('low_quality', 0)),
|
'low_quality': str(deps.quality_scanner_state.get('low_quality', 0)),
|
||||||
'total_scanned': str(deps.quality_scanner_state.get('processed', 0)),
|
'total_scanned': str(deps.quality_scanner_state.get('processed', 0)),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("emit quality_scan_completed failed: %s", e)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Quality Scanner] Critical error: {e}")
|
logger.error(f"[Quality Scanner] Critical error: {e}")
|
||||||
|
|
|
||||||
|
|
@ -290,8 +290,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
|
||||||
logger.debug(f"Sync cache hit: '{original_title}' → server track {cached['server_track_id']}")
|
logger.debug(f"Sync cache hit: '{original_title}' → server track {cached['server_track_id']}")
|
||||||
return DatabaseTrackCached(db_track_check), cached['confidence']
|
return DatabaseTrackCached(db_track_check), cached['confidence']
|
||||||
logger.warning(f"Sync cache stale for '{original_title}' — track gone")
|
logger.warning(f"Sync cache stale for '{original_title}' — track gone")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("sync match cache fast-path failed: %s", e)
|
||||||
# --- End cache fast-path ---
|
# --- End cache fast-path ---
|
||||||
|
|
||||||
# Try each artist (same logic as original)
|
# Try each artist (same logic as original)
|
||||||
|
|
@ -322,8 +322,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
|
||||||
spotify_id, me.clean_title(original_title), me.clean_artist(artist_name),
|
spotify_id, me.clean_title(original_title), me.clean_artist(artist_name),
|
||||||
active_server, db_track.id, db_track.title, confidence
|
active_server, db_track.id, db_track.title, confidence
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("save sync match cache failed: %s", e)
|
||||||
|
|
||||||
# Create mock track object for playlist creation
|
# Create mock track object for playlist creation
|
||||||
class DatabaseTrackMock:
|
class DatabaseTrackMock:
|
||||||
|
|
@ -429,8 +429,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
|
||||||
entry = db.get_sync_history_entry(_resync_entry_id)
|
entry = db.get_sync_history_entry(_resync_entry_id)
|
||||||
if entry:
|
if entry:
|
||||||
target_batch_id = entry.get('batch_id', sync_batch_id)
|
target_batch_id = entry.get('batch_id', sync_batch_id)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("resync history lookup failed: %s", e)
|
||||||
else:
|
else:
|
||||||
db.update_sync_history_completion(sync_batch_id, matched, synced, failed)
|
db.update_sync_history_completion(sync_batch_id, matched, synced, failed)
|
||||||
|
|
||||||
|
|
@ -466,8 +466,8 @@ def run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, p
|
||||||
'synced_tracks': str(getattr(result, 'synced_tracks', 0)),
|
'synced_tracks': str(getattr(result, 'synced_tracks', 0)),
|
||||||
'failed_tracks': str(getattr(result, 'failed_tracks', 0)),
|
'failed_tracks': str(getattr(result, 'failed_tracks', 0)),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("playlist_synced emit failed: %s", e)
|
||||||
|
|
||||||
# Save sync status with match counts and track hash for smart-skip on next scheduled sync
|
# Save sync status with match counts and track hash for smart-skip on next scheduled sync
|
||||||
import hashlib as _hl
|
import hashlib as _hl
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@ module-level constant in the client file.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RateLimitPolicy:
|
class RateLimitPolicy:
|
||||||
|
|
@ -63,8 +66,8 @@ def resolve_policy(plugin) -> RateLimitPolicy:
|
||||||
policy = method()
|
policy = method()
|
||||||
if isinstance(policy, RateLimitPolicy):
|
if isinstance(policy, RateLimitPolicy):
|
||||||
return policy
|
return policy
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plugin rate_limit_policy() call failed: %s", e)
|
||||||
|
|
||||||
declared = getattr(plugin, 'RATE_LIMIT_POLICY', None)
|
declared = getattr(plugin, 'RATE_LIMIT_POLICY', None)
|
||||||
if isinstance(declared, RateLimitPolicy):
|
if isinstance(declared, RateLimitPolicy):
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,8 @@ def attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
|
||||||
if _bl_db.is_blacklisted(candidate.username, candidate.filename):
|
if _bl_db.is_blacklisted(candidate.username, candidate.filename):
|
||||||
logger.info(f"[Modal Worker] Skipping blacklisted source: {source_key}")
|
logger.info(f"[Modal Worker] Skipping blacklisted source: {source_key}")
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("blacklist check failed: %s", e)
|
||||||
|
|
||||||
# CRITICAL: Add source to used_sources IMMEDIATELY to prevent race conditions
|
# CRITICAL: Add source to used_sources IMMEDIATELY to prevent race conditions
|
||||||
# This must happen BEFORE starting download to prevent multiple retries from picking same source
|
# This must happen BEFORE starting download to prevent multiple retries from picking same source
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,8 @@ def on_download_completed(batch_id: str, task_id: str, success: bool, deps: Life
|
||||||
'title': track_info.get('track_name', ''),
|
'title': track_info.get('track_name', ''),
|
||||||
'reason': track_info.get('failure_reason', 'Unknown'),
|
'reason': track_info.get('failure_reason', 'Unknown'),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("download_failed emit failed: %s", e)
|
||||||
|
|
||||||
# WISHLIST REMOVAL: Handle successful downloads for wishlist removal
|
# WISHLIST REMOVAL: Handle successful downloads for wishlist removal
|
||||||
if success and task_id in download_tasks:
|
if success and task_id in download_tasks:
|
||||||
|
|
@ -364,8 +364,8 @@ def on_download_completed(batch_id: str, task_id: str, success: bool, deps: Life
|
||||||
'completed_tracks': str(successful_downloads),
|
'completed_tracks': str(successful_downloads),
|
||||||
'failed_tracks': str(failed_count),
|
'failed_tracks': str(failed_count),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("batch_complete emit failed: %s", e)
|
||||||
|
|
||||||
# Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist
|
# Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist
|
||||||
playlist_id = batch.get('playlist_id')
|
playlist_id = batch.get('playlist_id')
|
||||||
|
|
@ -569,8 +569,8 @@ def check_batch_completion_v2(batch_id: str, deps: LifecycleDeps) -> Optional[bo
|
||||||
'completed_tracks': str(successful_downloads),
|
'completed_tracks': str(successful_downloads),
|
||||||
'failed_tracks': str(failed_count),
|
'failed_tracks': str(failed_count),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("batch_complete emit failed: %s", e)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing")
|
logger.warning(f"[Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing")
|
||||||
return True # Already complete
|
return True # Already complete
|
||||||
|
|
|
||||||
|
|
@ -296,8 +296,8 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma
|
||||||
})
|
})
|
||||||
if track_results:
|
if track_results:
|
||||||
db_sh.update_sync_history_track_results(batch_id, json.dumps(track_results))
|
db_sh.update_sync_history_track_results(batch_id, json.dumps(track_results))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("update sync_history track results failed: %s", e)
|
||||||
|
|
||||||
is_auto_batch = False
|
is_auto_batch = False
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
|
|
|
||||||
|
|
@ -280,8 +280,8 @@ class WebUIDownloadMonitor:
|
||||||
all_downloads = run_async(
|
all_downloads = run_async(
|
||||||
download_orchestrator.engine.get_all_downloads(exclude=('soulseek',))
|
download_orchestrator.engine.get_all_downloads(exclude=('soulseek',))
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("get_all_downloads failed: %s", e)
|
||||||
for download in all_downloads:
|
for download in all_downloads:
|
||||||
key = _make_context_key(download.username, download.filename)
|
key = _make_context_key(download.username, download.filename)
|
||||||
# Convert DownloadStatus to transfer dict format for monitor compatibility
|
# Convert DownloadStatus to transfer dict format for monitor compatibility
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,8 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
|
||||||
'title': track_name,
|
'title': track_name,
|
||||||
'reason': failed_track_info.get('failure_reason', ''),
|
'reason': failed_track_info.get('failure_reason', ''),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("emit wishlist_item_added failed: %s", e)
|
||||||
else:
|
else:
|
||||||
logger.error(f"[Wishlist Processing] Failed to add {track_name} to wishlist")
|
logger.error(f"[Wishlist Processing] Failed to add {track_name} to wishlist")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,8 @@ def _drop_auto_pause_marker(service: EnrichmentService) -> None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
_auto_paused_discard(service.auto_pause_token)
|
_auto_paused_discard(service.auto_pause_token)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("auto-pause marker discard: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _add_yield_override(service: EnrichmentService) -> None:
|
def _add_yield_override(service: EnrichmentService) -> None:
|
||||||
|
|
@ -76,8 +76,8 @@ def _add_yield_override(service: EnrichmentService) -> None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
_yield_override_add(service.auto_pause_token)
|
_yield_override_add(service.auto_pause_token)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("yield override add: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def create_blueprint() -> Blueprint:
|
def create_blueprint() -> Blueprint:
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,8 @@ class GeniusWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null-id item type lookup: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._process_item(item)
|
self._process_item(item)
|
||||||
|
|
@ -367,8 +367,8 @@ class GeniusWorker:
|
||||||
if song_url:
|
if song_url:
|
||||||
try:
|
try:
|
||||||
lyrics = self.client.get_lyrics(song_url)
|
lyrics = self.client.get_lyrics(song_url)
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("genius lyrics scrape: %s", _e)
|
||||||
self._update_track(track_id, full_song, full_song, lyrics)
|
self._update_track(track_id, full_song, full_song, lyrics)
|
||||||
self.stats['matched'] += 1
|
self.stats['matched'] += 1
|
||||||
logger.info(f"Enriched track '{track_name}' from existing Genius ID: {existing_id}")
|
logger.info(f"Enriched track '{track_name}' from existing Genius ID: {existing_id}")
|
||||||
|
|
|
||||||
|
|
@ -525,8 +525,8 @@ class HydrabaseClient:
|
||||||
for album in albums:
|
for album in albums:
|
||||||
if album.image_url:
|
if album.image_url:
|
||||||
return album.image_url
|
return album.image_url
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("get artist image from albums failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:
|
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:
|
||||||
|
|
|
||||||
|
|
@ -362,8 +362,8 @@ def downsample_hires_flac(final_path, context):
|
||||||
if os.path.exists(temp_path):
|
if os.path.exists(temp_path):
|
||||||
try:
|
try:
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("cleanup downsample temp: %s", _e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -468,8 +468,8 @@ def create_lossy_copy(final_path):
|
||||||
if os.path.exists(out_path):
|
if os.path.exists(out_path):
|
||||||
try:
|
try:
|
||||||
os.remove(out_path)
|
os.remove(out_path)
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("cleanup lossy copy artifact: %s", _e)
|
||||||
return None
|
return None
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
logger.warning(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")
|
logger.warning(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,8 @@ def move_to_quarantine(file_path: str, context: dict, reason: str, automation_en
|
||||||
"reason": reason or "Unknown",
|
"reason": reason or "Unknown",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("emit download_quarantined failed: %s", e)
|
||||||
|
|
||||||
return str(quarantine_path)
|
return str(quarantine_path)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,8 +188,8 @@ def _replace_template_variables(template: str, context: dict) -> str:
|
||||||
resolved = resolved_client.resolve_primary_artist(itunes_artist_id)
|
resolved = resolved_client.resolve_primary_artist(itunes_artist_id)
|
||||||
if resolved and resolved != album_artist_value:
|
if resolved and resolved != album_artist_value:
|
||||||
album_artist_value = resolved
|
album_artist_value = resolved
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("resolve primary artist failed: %s", e)
|
||||||
|
|
||||||
# $cdnum — smart CD label for multi-disc filenames. Produces "CD01" /
|
# $cdnum — smart CD label for multi-disc filenames. Produces "CD01" /
|
||||||
# "CD02" etc. when the album has 2+ discs, empty string otherwise.
|
# "CD02" etc. when the album has 2+ discs, empty string otherwise.
|
||||||
|
|
|
||||||
|
|
@ -434,8 +434,8 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta
|
||||||
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
|
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("delete quarantine fallback: %s", e)
|
||||||
|
|
||||||
context['_bitdepth_rejected'] = True
|
context['_bitdepth_rejected'] = True
|
||||||
with matched_context_lock:
|
with matched_context_lock:
|
||||||
|
|
@ -562,8 +562,8 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta
|
||||||
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
|
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("delete quarantine fallback: %s", e)
|
||||||
|
|
||||||
context['_bitdepth_rejected'] = True
|
context['_bitdepth_rejected'] = True
|
||||||
with matched_context_lock:
|
with matched_context_lock:
|
||||||
|
|
|
||||||
|
|
@ -407,8 +407,8 @@ def get_single_track_import_context(
|
||||||
'genres',
|
'genres',
|
||||||
default=[],
|
default=[],
|
||||||
) or []
|
) or []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("override artist genres: %s", e)
|
||||||
return payload
|
return payload
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Override track lookup failed on %s for %s: %s", chosen_source, override_id, exc)
|
logger.debug("Override track lookup failed on %s for %s: %s", chosen_source, override_id, exc)
|
||||||
|
|
@ -461,8 +461,8 @@ def get_single_track_import_context(
|
||||||
'genres',
|
'genres',
|
||||||
default=[],
|
default=[],
|
||||||
) or []
|
) or []
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist genres lookup: %s", e)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
return _build_single_import_fallback_context(title, artist, source_priority)
|
return _build_single_import_fallback_context(title, artist, source_priority)
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ def emit_track_downloaded(context: Dict[str, Any], automation_engine=None) -> No
|
||||||
"quality": context.get("_audio_quality", "Unknown"),
|
"quality": context.get("_audio_quality", "Unknown"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("track_downloaded emit failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def record_library_history_download(context: Dict[str, Any]) -> None:
|
def record_library_history_download(context: Dict[str, Any]) -> None:
|
||||||
|
|
@ -143,8 +143,8 @@ def record_library_history_download(context: Dict[str, Any]) -> None:
|
||||||
acoustid_result=acoustid_result,
|
acoustid_result=acoustid_result,
|
||||||
source_artist=source_artist,
|
source_artist=source_artist,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("library history record failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def record_download_provenance(context: Dict[str, Any]) -> None:
|
def record_download_provenance(context: Dict[str, Any]) -> None:
|
||||||
|
|
@ -188,8 +188,8 @@ def record_download_provenance(context: Dict[str, Any]) -> None:
|
||||||
sample_rate = getattr(audio.info, "sample_rate", None)
|
sample_rate = getattr(audio.info, "sample_rate", None)
|
||||||
bitrate = getattr(audio.info, "bitrate", None)
|
bitrate = getattr(audio.info, "bitrate", None)
|
||||||
bit_depth = getattr(audio.info, "bits_per_sample", None)
|
bit_depth = getattr(audio.info, "bits_per_sample", None)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("audio info probe failed: %s", e)
|
||||||
|
|
||||||
# Pull the metadata-source IDs out of context. ``embed_source_ids``
|
# Pull the metadata-source IDs out of context. ``embed_source_ids``
|
||||||
# in core/metadata/source.py wrote them to ``_embedded_id_tags``
|
# in core/metadata/source.py wrote them to ``_embedded_id_tags``
|
||||||
|
|
@ -239,8 +239,8 @@ def record_download_provenance(context: Dict[str, Any]) -> None:
|
||||||
soul_id=soul_id,
|
soul_id=soul_id,
|
||||||
isrc=isrc,
|
isrc=isrc,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("record_download_provenance failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[str, Any], album_info: Dict[str, Any]) -> None:
|
def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[str, Any], album_info: Dict[str, Any]) -> None:
|
||||||
|
|
@ -321,8 +321,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
|
||||||
audio = MutagenFile(final_path)
|
audio = MutagenFile(final_path)
|
||||||
if audio and hasattr(audio, "info") and audio.info and hasattr(audio.info, "bitrate"):
|
if audio and hasattr(audio, "info") and audio.info and hasattr(audio.info, "bitrate"):
|
||||||
bitrate = int(audio.info.bitrate / 1000) if audio.info.bitrate else 0
|
bitrate = int(audio.info.bitrate / 1000) if audio.info.bitrate else 0
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("bitrate read failed: %s", e)
|
||||||
|
|
||||||
# File size on disk (powers Library Disk Usage card on Stats).
|
# File size on disk (powers Library Disk Usage card on Stats).
|
||||||
# SoulSync standalone is the only path where the file is local
|
# SoulSync standalone is the only path where the file is local
|
||||||
|
|
@ -371,8 +371,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
|
||||||
f"UPDATE artists SET {artist_source_col} = ? WHERE id = ?",
|
f"UPDATE artists SET {artist_source_col} = ? WHERE id = ?",
|
||||||
(artist_source_id, artist_id),
|
(artist_source_id, artist_id),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist source-id update failed: %s", e)
|
||||||
|
|
||||||
cursor.execute("SELECT id FROM albums WHERE id = ? AND server_source = 'soulsync'", (album_id,))
|
cursor.execute("SELECT id FROM albums WHERE id = ? AND server_source = 'soulsync'", (album_id,))
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
|
|
@ -402,8 +402,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
|
||||||
f"UPDATE albums SET {album_source_col} = ? WHERE id = ?",
|
f"UPDATE albums SET {album_source_col} = ? WHERE id = ?",
|
||||||
(album_source_id, album_id),
|
(album_source_id, album_id),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album source-id update failed: %s", e)
|
||||||
|
|
||||||
track_artist = None
|
track_artist = None
|
||||||
track_artists_list = track_info.get("artists", []) or original_search.get("artists", [])
|
track_artists_list = track_info.get("artists", []) or original_search.get("artists", [])
|
||||||
|
|
@ -451,8 +451,8 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[
|
||||||
f"UPDATE tracks SET {track_album_col} = ? WHERE id = ?",
|
f"UPDATE tracks SET {track_album_col} = ? WHERE id = ?",
|
||||||
(album_source_id, track_id),
|
(album_source_id, track_id),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("track source-id update failed: %s", e)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info("[SoulSync Library] Added: %s / %s / %s", artist_name, album_name, track_name)
|
logger.info("[SoulSync Library] Added: %s / %s / %s", artist_name, album_name, track_name)
|
||||||
|
|
|
||||||
|
|
@ -380,8 +380,8 @@ class iTunesClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
tracks.append(Track.from_itunes_track(raw))
|
tracks.append(Track.from_itunes_track(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Track.from_itunes_track cache parse: %s", e)
|
||||||
if tracks:
|
if tracks:
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
@ -569,8 +569,8 @@ class iTunesClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
albums.append(Album.from_itunes_album(raw))
|
albums.append(Album.from_itunes_album(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Album.from_itunes_album cache parse: %s", e)
|
||||||
if albums:
|
if albums:
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
|
|
@ -893,8 +893,8 @@ class iTunesClient:
|
||||||
for item in results:
|
for item in results:
|
||||||
if item.get('wrapperType') == 'artist' and item.get('artistName'):
|
if item.get('wrapperType') == 'artist' and item.get('artistName'):
|
||||||
return item['artistName']
|
return item['artistName']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("itunes lookup artistId %s: %s", artist_id, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
|
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
|
||||||
|
|
@ -911,8 +911,8 @@ class iTunesClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
artists.append(Artist.from_itunes_artist(raw))
|
artists.append(Artist.from_itunes_artist(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Artist.from_itunes_artist cache parse: %s", e)
|
||||||
if artists:
|
if artists:
|
||||||
return artists
|
return artists
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,8 +144,8 @@ class iTunesWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null id table resolve failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._process_item(item)
|
self._process_item(item)
|
||||||
|
|
|
||||||
|
|
@ -1441,8 +1441,8 @@ class JellyfinClient(MediaServerClient):
|
||||||
response = requests.delete(url, headers=headers, timeout=10)
|
response = requests.delete(url, headers=headers, timeout=10)
|
||||||
if response.status_code in [200, 204]:
|
if response.status_code in [200, 204]:
|
||||||
logger.info(f"Deleted existing backup playlist '{target_name}'")
|
logger.info(f"Deleted existing backup playlist '{target_name}'")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Target doesn't exist, which is fine
|
logger.debug("backup playlist precheck: %s", e)
|
||||||
|
|
||||||
# Create new playlist with copied tracks
|
# Create new playlist with copied tracks
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,8 @@ class LastFMWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null id table resolve failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._process_item(item)
|
self._process_item(item)
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,8 @@ def _run_duplicate_cleaner():
|
||||||
'duplicates_found': str(duplicates_found),
|
'duplicates_found': str(duplicates_found),
|
||||||
'space_freed': f"{space_mb:.1f} MB",
|
'space_freed': f"{space_mb:.1f} MB",
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("emit duplicate_scan_completed failed: %s", e)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Duplicate Cleaner] Critical error: {e}")
|
logger.error(f"[Duplicate Cleaner] Critical error: {e}")
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ def _collect_base_dirs(
|
||||||
candidates.append(_docker_resolve_path(transfer_cfg))
|
candidates.append(_docker_resolve_path(transfer_cfg))
|
||||||
if download_cfg:
|
if download_cfg:
|
||||||
candidates.append(_docker_resolve_path(download_cfg))
|
candidates.append(_docker_resolve_path(download_cfg))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("soulseek paths read failed: %s", e)
|
||||||
|
|
||||||
# Plex-reported library locations (handles "Plex scanned at /music but
|
# Plex-reported library locations (handles "Plex scanned at /music but
|
||||||
# SoulSync mounts at /library" cases).
|
# SoulSync mounts at /library" cases).
|
||||||
|
|
@ -96,8 +96,8 @@ def _collect_base_dirs(
|
||||||
for loc in getattr(music_library, "locations", []) or []:
|
for loc in getattr(music_library, "locations", []) or []:
|
||||||
if loc:
|
if loc:
|
||||||
candidates.append(loc)
|
candidates.append(loc)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex locations read failed: %s", e)
|
||||||
|
|
||||||
# User-configured library music paths (Settings → Library → Music Paths).
|
# User-configured library music paths (Settings → Library → Music Paths).
|
||||||
if config_manager is not None:
|
if config_manager is not None:
|
||||||
|
|
@ -107,8 +107,8 @@ def _collect_base_dirs(
|
||||||
for p in music_paths:
|
for p in music_paths:
|
||||||
if isinstance(p, str) and p.strip():
|
if isinstance(p, str) and p.strip():
|
||||||
candidates.append(_docker_resolve_path(p.strip()))
|
candidates.append(_docker_resolve_path(p.strip()))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("music paths read failed: %s", e)
|
||||||
|
|
||||||
# De-duplicate while preserving order, drop empties / non-existent dirs.
|
# De-duplicate while preserving order, drop empties / non-existent dirs.
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
|
|
|
||||||
|
|
@ -265,8 +265,8 @@ def execute_retag(group_id, album_id, deps: RetagDeps):
|
||||||
try:
|
try:
|
||||||
os.remove(old_cover)
|
os.remove(old_cover)
|
||||||
logger.warning("[Retag] Removed orphaned cover.jpg from old directory")
|
logger.warning("[Retag] Removed orphaned cover.jpg from old directory")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("remove orphaned cover failed: %s", e)
|
||||||
|
|
||||||
# Cleanup old empty directories
|
# Cleanup old empty directories
|
||||||
transfer_dir = deps.docker_resolve_path(deps.config_manager.get('soulseek.transfer_path', './Transfer'))
|
transfer_dir = deps.docker_resolve_path(deps.config_manager.get('soulseek.transfer_path', './Transfer'))
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ def find_library_track_by_external_id(
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -287,7 +287,7 @@ def find_provenance_by_external_id(
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -506,7 +506,7 @@ def load_album_and_tracks(db, album_id):
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -938,8 +938,8 @@ class _RunContext:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.on_progress(updates)
|
self.on_progress(updates)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("progress emit failed: %s", e)
|
||||||
|
|
||||||
def record_error(self, track_id, title, message, kind: str = 'skipped') -> None:
|
def record_error(self, track_id, title, message, kind: str = 'skipped') -> None:
|
||||||
with self.state_lock:
|
with self.state_lock:
|
||||||
|
|
@ -1185,8 +1185,8 @@ def reorganize_album(
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
on_progress(updates)
|
on_progress(updates)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("reorganize progress callback failed: %s", e)
|
||||||
|
|
||||||
# Load album + tracks
|
# Load album + tracks
|
||||||
album_data, tracks = load_album_and_tracks(db, album_id)
|
album_data, tracks = load_album_and_tracks(db, album_id)
|
||||||
|
|
@ -1329,7 +1329,7 @@ def reorganize_album(
|
||||||
try:
|
try:
|
||||||
if os.path.isdir(staging_album_dir):
|
if os.path.isdir(staging_album_dir):
|
||||||
shutil.rmtree(staging_album_dir, ignore_errors=True)
|
shutil.rmtree(staging_album_dir, ignore_errors=True)
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Best-effort cleanup of source directories. For each touched dir
|
# Best-effort cleanup of source directories. For each touched dir
|
||||||
|
|
@ -1343,14 +1343,14 @@ def reorganize_album(
|
||||||
if _has_remaining_audio(src_dir):
|
if _has_remaining_audio(src_dir):
|
||||||
continue
|
continue
|
||||||
_delete_album_sidecars(src_dir)
|
_delete_album_sidecars(src_dir)
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if cleanup_empty_dir_fn:
|
if cleanup_empty_dir_fn:
|
||||||
for src_dir in src_dirs_touched:
|
for src_dir in src_dirs_touched:
|
||||||
try:
|
try:
|
||||||
cleanup_empty_dir_fn(src_dir)
|
cleanup_empty_dir_fn(src_dir)
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Prune empty *destination* siblings — e.g. when a previous
|
# Prune empty *destination* siblings — e.g. when a previous
|
||||||
|
|
|
||||||
|
|
@ -490,8 +490,8 @@ class LidarrDownloadClient(DownloadSourcePlugin):
|
||||||
try:
|
try:
|
||||||
self._api_delete(f'album/{lidarr_album_id}', params={'deleteFiles': 'false'})
|
self._api_delete(f'album/{lidarr_album_id}', params={'deleteFiles': 'false'})
|
||||||
logger.debug(f"Cleaned up album {lidarr_album_id} from Lidarr")
|
logger.debug(f"Cleaned up album {lidarr_album_id} from Lidarr")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Lidarr album cleanup failed: %s", e)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Lidarr download thread failed: {e}")
|
logger.error(f"Lidarr download thread failed: {e}")
|
||||||
|
|
|
||||||
|
|
@ -302,8 +302,8 @@ class ListenBrainzManager:
|
||||||
# Fallback to first image
|
# Fallback to first image
|
||||||
if images:
|
if images:
|
||||||
return images[0].get('thumbnails', {}).get('small') or images[0].get('image')
|
return images[0].get('thumbnails', {}).get('small') or images[0].get('image')
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cover-art fetch: %s", e)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -507,7 +507,7 @@ class ListeningStatsWorker:
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _resolve_db_track_id(self, title, artist):
|
def _resolve_db_track_id(self, title, artist):
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ def lookup(normalized_album_key: str, artist_key: str) -> Optional[str]:
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
@ -142,7 +142,7 @@ def record(normalized_album_key: str, artist_key: str, release_mbid: str) -> boo
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,7 +168,7 @@ def clear_all() -> bool:
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -320,8 +320,8 @@ def download_cover_art(album_info: dict, target_dir: str, context: dict = None):
|
||||||
from core.spotify_client import _upgrade_spotify_image_url
|
from core.spotify_client import _upgrade_spotify_image_url
|
||||||
|
|
||||||
art_url = _upgrade_spotify_image_url(art_url)
|
art_url = _upgrade_spotify_image_url(art_url)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("upgrade spotify image url failed: %s", e)
|
||||||
elif art_url and "mzstatic.com" in art_url:
|
elif art_url and "mzstatic.com" in art_url:
|
||||||
art_url = re.sub(r"\d+x\d+bb", "3000x3000bb", art_url)
|
art_url = re.sub(r"\d+x\d+bb", "3000x3000bb", art_url)
|
||||||
if not art_url:
|
if not art_url:
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ def get_metadata_cache():
|
||||||
try:
|
try:
|
||||||
import threading
|
import threading
|
||||||
threading.Thread(target=_cache_instance.backfill_deezer_album_genres, daemon=True).start()
|
threading.Thread(target=_cache_instance.backfill_deezer_album_genres, daemon=True).start()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("start deezer genres backfill failed: %s", e)
|
||||||
return _cache_instance
|
return _cache_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,8 +171,8 @@ def get_image_dimensions(data: bytes):
|
||||||
return w, h
|
return w, h
|
||||||
length = struct.unpack(">H", data[i + 2 : i + 4])[0]
|
length = struct.unpack(">H", data[i + 2 : i + 4])[0]
|
||||||
i += 2 + length
|
i += 2 + length
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("parse JPEG dimensions failed: %s", e)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -278,8 +278,8 @@ def get_hydrabase_client(allow_fallback: bool = True, require_enabled: bool = Tr
|
||||||
if client and client.is_connected():
|
if client and client.is_connected():
|
||||||
if not require_enabled or bool(_dev_mode_enabled_provider()):
|
if not require_enabled or bool(_dev_mode_enabled_provider()):
|
||||||
return client
|
return client
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hydrabase client lookup: %s", e)
|
||||||
|
|
||||||
if allow_fallback:
|
if allow_fallback:
|
||||||
return get_itunes_client()
|
return get_itunes_client()
|
||||||
|
|
@ -394,8 +394,8 @@ def get_client_for_source(
|
||||||
client = get_spotify_client(client_factory=spotify_client_factory)
|
client = get_spotify_client(client_factory=spotify_client_factory)
|
||||||
if client and client.is_spotify_authenticated():
|
if client and client.is_spotify_authenticated():
|
||||||
return client
|
return client
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify client get_for_source: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if source == "deezer":
|
if source == "deezer":
|
||||||
|
|
|
||||||
|
|
@ -296,8 +296,8 @@ def _process_musicbrainz_source(pp: dict, metadata: dict, cfg, runtime, track_ti
|
||||||
try:
|
try:
|
||||||
from core.metadata import album_mbid_cache as _persisted_cache
|
from core.metadata import album_mbid_cache as _persisted_cache
|
||||||
_persisted_cache.record(normalized_album_key, artist_key, release_mbid)
|
_persisted_cache.record(normalized_album_key, artist_key, release_mbid)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("MBID cache persist failed: %s", e)
|
||||||
pp["release_mbid"] = release_mbid or ""
|
pp["release_mbid"] = release_mbid or ""
|
||||||
if pp["release_mbid"]:
|
if pp["release_mbid"]:
|
||||||
pp["id_tags"]["MUSICBRAINZ_RELEASE_ID"] = pp["release_mbid"]
|
pp["id_tags"]["MUSICBRAINZ_RELEASE_ID"] = pp["release_mbid"]
|
||||||
|
|
@ -959,8 +959,8 @@ def extract_source_metadata(context: dict, artist: dict, album_info: dict) -> di
|
||||||
resolved = itunes_client.resolve_primary_artist(artist_id)
|
resolved = itunes_client.resolve_primary_artist(artist_id)
|
||||||
if resolved and resolved != raw_album_artist:
|
if resolved and resolved != raw_album_artist:
|
||||||
raw_album_artist = resolved
|
raw_album_artist = resolved
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("itunes primary artist resolve failed: %s", e)
|
||||||
metadata["album_artist"] = raw_album_artist
|
metadata["album_artist"] = raw_album_artist
|
||||||
|
|
||||||
if album_info.get("is_album"):
|
if album_info.get("is_album"):
|
||||||
|
|
@ -1136,8 +1136,8 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
)
|
)
|
||||||
if isrc_value:
|
if isrc_value:
|
||||||
context["_isrc"] = str(isrc_value)
|
context["_isrc"] = str(isrc_value)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("context isrc copy failed: %s", e)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Error embedding source IDs (non-fatal): %s", exc)
|
logger.error("Error embedding source IDs (non-fatal): %s", exc)
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,8 @@ class MusicBrainzWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null id table resolve failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -922,8 +922,8 @@ class NavidromeClient(MediaServerClient):
|
||||||
if target_playlist:
|
if target_playlist:
|
||||||
self._make_request('deletePlaylist', {'id': target_playlist.id})
|
self._make_request('deletePlaylist', {'id': target_playlist.id})
|
||||||
logger.info(f"Deleted existing backup playlist '{target_name}'")
|
logger.info(f"Deleted existing backup playlist '{target_name}'")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Target doesn't exist, which is fine
|
logger.debug("backup playlist precheck: %s", e)
|
||||||
|
|
||||||
# Create new playlist with copied tracks
|
# Create new playlist with copied tracks
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,8 @@ def playlist_explorer_build_tree(deps: PlaylistExplorerDeps):
|
||||||
artist_image = images[0].get('url') if images else None
|
artist_image = images[0].get('url') if images else None
|
||||||
elif hasattr(artist_info, 'image_url'):
|
elif hasattr(artist_info, 'image_url'):
|
||||||
artist_image = artist_info.image_url
|
artist_image = artist_info.image_url
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist image resolve: %s", e)
|
||||||
else:
|
else:
|
||||||
# No pre-resolved ID — search by name
|
# No pre-resolved ID — search by name
|
||||||
try:
|
try:
|
||||||
|
|
@ -224,8 +224,8 @@ def playlist_explorer_build_tree(deps: PlaylistExplorerDeps):
|
||||||
cursor.execute("SELECT title FROM albums WHERE artist_id = ?", (ar['id'],))
|
cursor.execute("SELECT title FROM albums WHERE artist_id = ?", (ar['id'],))
|
||||||
for alb_row in cursor.fetchall():
|
for alb_row in cursor.fetchall():
|
||||||
owned_titles.add((alb_row['title'] or '').strip().lower())
|
owned_titles.add((alb_row['title'] or '').strip().lower())
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Non-critical — owned badges just won't show
|
logger.debug("owned-titles lookup: %s", e)
|
||||||
|
|
||||||
# Build release list
|
# Build release list
|
||||||
releases = []
|
releases = []
|
||||||
|
|
@ -256,8 +256,8 @@ def playlist_explorer_build_tree(deps: PlaylistExplorerDeps):
|
||||||
if cache and releases:
|
if cache and releases:
|
||||||
try:
|
try:
|
||||||
cache.store_entity(source_name, 'artist_discography', cache_key, result)
|
cache.store_entity(source_name, 'artist_discography', cache_key, result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cache discography write: %s", e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -405,8 +405,8 @@ class PlexClient(MediaServerClient):
|
||||||
new_count = getattr(a, 'leafCount', 0) or 0
|
new_count = getattr(a, 'leafCount', 0) or 0
|
||||||
if new_count > existing_count:
|
if new_count > existing_count:
|
||||||
by_name[name_key] = a
|
by_name[name_key] = a
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist leafCount compare failed: %s", e)
|
||||||
return list(by_name.values())
|
return list(by_name.values())
|
||||||
|
|
||||||
def _dedupe_albums(self, albums: List[PlexAlbum]) -> List[PlexAlbum]:
|
def _dedupe_albums(self, albums: List[PlexAlbum]) -> List[PlexAlbum]:
|
||||||
|
|
@ -424,8 +424,8 @@ class PlexClient(MediaServerClient):
|
||||||
artist = ''
|
artist = ''
|
||||||
try:
|
try:
|
||||||
artist = (getattr(alb, 'parentTitle', '') or '').strip().lower()
|
artist = (getattr(alb, 'parentTitle', '') or '').strip().lower()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album parentTitle read failed: %s", e)
|
||||||
key = (artist, title)
|
key = (artist, title)
|
||||||
existing = by_key.get(key)
|
existing = by_key.get(key)
|
||||||
if existing is None:
|
if existing is None:
|
||||||
|
|
@ -436,8 +436,8 @@ class PlexClient(MediaServerClient):
|
||||||
new_count = getattr(alb, 'leafCount', 0) or 0
|
new_count = getattr(alb, 'leafCount', 0) or 0
|
||||||
if new_count > existing_count:
|
if new_count > existing_count:
|
||||||
by_key[key] = alb
|
by_key[key] = alb
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album leafCount compare failed: %s", e)
|
||||||
return list(by_key.values())
|
return list(by_key.values())
|
||||||
|
|
||||||
def get_all_playlists(self) -> List[PlaylistInfo]:
|
def get_all_playlists(self) -> List[PlaylistInfo]:
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,8 @@ class QobuzWorker:
|
||||||
try:
|
try:
|
||||||
if self.client:
|
if self.client:
|
||||||
authenticated = self.client.is_authenticated()
|
authenticated = self.client.is_authenticated()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("qobuz auth status check: %s", e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
|
|
@ -163,8 +163,8 @@ class QobuzWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null-id item type lookup: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,15 +95,15 @@ def build_runner(
|
||||||
def _cleanup_empty(src_dir):
|
def _cleanup_empty(src_dir):
|
||||||
try:
|
try:
|
||||||
cleanup_empty_directories_fn(transfer_dir, os.path.join(src_dir, '_'))
|
cleanup_empty_directories_fn(transfer_dir, os.path.join(src_dir, '_'))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cleanup empty dirs failed: %s", e)
|
||||||
|
|
||||||
def _on_progress(updates):
|
def _on_progress(updates):
|
||||||
try:
|
try:
|
||||||
get_queue().update_active_progress(queue_id=item.queue_id, **updates)
|
get_queue().update_active_progress(queue_id=item.queue_id, **updates)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# Progress fan-out failures must never break a run.
|
# Progress fan-out failures must never break a run.
|
||||||
pass
|
logger.debug("reorganize progress fan-out: %s", e)
|
||||||
|
|
||||||
return reorganize_album(
|
return reorganize_album(
|
||||||
album_id=item.album_id,
|
album_id=item.album_id,
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ def _read_tag(audio, tag_name):
|
||||||
if vals:
|
if vals:
|
||||||
return vals[0].decode('utf-8') if isinstance(vals[0], bytes) else str(vals[0])
|
return vals[0].decode('utf-8') if isinstance(vals[0], bytes) else str(vals[0])
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read tag value failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -299,8 +299,8 @@ class DiscographyBackfillJob(RepairJob):
|
||||||
track_id = track_item.get('id', '')
|
track_id = track_item.get('id', '')
|
||||||
if track_id and self._is_in_wishlist(context.db, track_id):
|
if track_id and self._is_in_wishlist(context.db, track_id):
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("wishlist membership check failed: %s", e)
|
||||||
|
|
||||||
# Build wishlist-ready track data. album is a dict (required by
|
# Build wishlist-ready track data. album is a dict (required by
|
||||||
# add_to_wishlist and by the download pipeline's cover-art
|
# add_to_wishlist and by the download pipeline's cover-art
|
||||||
|
|
|
||||||
|
|
@ -337,8 +337,8 @@ class LibraryReorganizeJob(RepairJob):
|
||||||
if context.config_manager:
|
if context.config_manager:
|
||||||
try:
|
try:
|
||||||
active_server = context.config_manager.get_active_media_server()
|
active_server = context.config_manager.get_active_media_server()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("active media server lookup: %s", e)
|
||||||
try:
|
try:
|
||||||
conn = context.db._get_connection()
|
conn = context.db._get_connection()
|
||||||
try:
|
try:
|
||||||
|
|
@ -399,7 +399,7 @@ class LibraryReorganizeJob(RepairJob):
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _get_setting(self, context: JobContext, key: str, default):
|
def _get_setting(self, context: JobContext, key: str, default):
|
||||||
|
|
|
||||||
|
|
@ -366,8 +366,8 @@ class MbidMismatchDetectorJob(RepairJob):
|
||||||
try:
|
try:
|
||||||
from core.musicbrainz_client import MusicBrainzClient
|
from core.musicbrainz_client import MusicBrainzClient
|
||||||
mb_client = MusicBrainzClient()
|
mb_client = MusicBrainzClient()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("MusicBrainz client init failed: %s", e)
|
||||||
|
|
||||||
if not mb_client:
|
if not mb_client:
|
||||||
logger.warning("MusicBrainz client not available, skipping MBID mismatch scan")
|
logger.warning("MusicBrainz client not available, skipping MBID mismatch scan")
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,8 @@ class OrphanFileDetectorJob(RepairJob):
|
||||||
(first_artist and (clean_title, first_artist) in known_titles_clean)
|
(first_artist and (clean_title, first_artist) in known_titles_clean)
|
||||||
):
|
):
|
||||||
is_known = True
|
is_known = True
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("tag-based orphan check: %s", e)
|
||||||
|
|
||||||
# Last resort: parse title from filename pattern "NN - Title [Quality].ext"
|
# Last resort: parse title from filename pattern "NN - Title [Quality].ext"
|
||||||
# and match against known titles. Catches files with unreadable tags.
|
# and match against known titles. Catches files with unreadable tags.
|
||||||
|
|
@ -186,8 +186,8 @@ class OrphanFileDetectorJob(RepairJob):
|
||||||
if clean_fn and (clean_fn, clean_fa) in known_titles_clean:
|
if clean_fn and (clean_fn, clean_fa) in known_titles_clean:
|
||||||
is_known = True
|
is_known = True
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("filename-pattern orphan check: %s", e)
|
||||||
|
|
||||||
if not is_known:
|
if not is_known:
|
||||||
orphan_files.append(fpath)
|
orphan_files.append(fpath)
|
||||||
|
|
|
||||||
|
|
@ -496,8 +496,8 @@ class UnknownArtistFixerJob(RepairJob):
|
||||||
if not os.path.exists(sidecar_dst):
|
if not os.path.exists(sidecar_dst):
|
||||||
try:
|
try:
|
||||||
shutil.move(sidecar_src, sidecar_dst)
|
shutil.move(sidecar_src, sidecar_dst)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("move sidecar file: %s", e)
|
||||||
|
|
||||||
# Also move cover.jpg from old album folder
|
# Also move cover.jpg from old album folder
|
||||||
cover_src = os.path.join(src_dir, 'cover.jpg')
|
cover_src = os.path.join(src_dir, 'cover.jpg')
|
||||||
|
|
@ -505,8 +505,8 @@ class UnknownArtistFixerJob(RepairJob):
|
||||||
if os.path.isfile(cover_src) and not os.path.exists(cover_dst):
|
if os.path.isfile(cover_src) and not os.path.exists(cover_dst):
|
||||||
try:
|
try:
|
||||||
shutil.copy2(cover_src, cover_dst)
|
shutil.copy2(cover_src, cover_dst)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("copy cover.jpg: %s", e)
|
||||||
|
|
||||||
# Clean up empty directories
|
# Clean up empty directories
|
||||||
parent = os.path.dirname(current_norm)
|
parent = os.path.dirname(current_norm)
|
||||||
|
|
|
||||||
|
|
@ -579,8 +579,8 @@ class RepairWorker:
|
||||||
if self._on_job_start:
|
if self._on_job_start:
|
||||||
try:
|
try:
|
||||||
self._on_job_start(job_id, job.display_name)
|
self._on_job_start(job_id, job.display_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("on_job_start callback failed: %s", e)
|
||||||
|
|
||||||
# Record job start
|
# Record job start
|
||||||
run_id = self._record_job_start(job_id)
|
run_id = self._record_job_start(job_id)
|
||||||
|
|
@ -590,8 +590,8 @@ class RepairWorker:
|
||||||
if self._on_job_progress:
|
if self._on_job_progress:
|
||||||
try:
|
try:
|
||||||
self._on_job_progress(job_id, **kwargs)
|
self._on_job_progress(job_id, **kwargs)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("on_job_progress callback failed: %s", e)
|
||||||
|
|
||||||
# Build context
|
# Build context
|
||||||
context = JobContext(
|
context = JobContext(
|
||||||
|
|
@ -636,8 +636,8 @@ class RepairWorker:
|
||||||
try:
|
try:
|
||||||
status = 'error' if result.errors > 0 and result.auto_fixed == 0 else 'finished'
|
status = 'error' if result.errors > 0 and result.auto_fixed == 0 else 'finished'
|
||||||
self._on_job_finish(job_id, status, result)
|
self._on_job_finish(job_id, status, result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("on_job_finish callback failed: %s", e)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Job %s complete: scanned=%d fixed=%d findings=%d errors=%d (%.1fs)",
|
"Job %s complete: scanned=%d fixed=%d findings=%d errors=%d (%.1fs)",
|
||||||
|
|
@ -1164,8 +1164,8 @@ class RepairWorker:
|
||||||
audio = MutagenFile(resolved)
|
audio = MutagenFile(resolved)
|
||||||
if audio:
|
if audio:
|
||||||
_, total_tracks = _read_track_number_tag(audio)
|
_, total_tracks = _read_track_number_tag(audio)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to read total_tracks tag from file: %s", e)
|
||||||
total_tracks = int(total_tracks or 0)
|
total_tracks = int(total_tracks or 0)
|
||||||
_fix_track_number_tag(resolved, int(correct_num), total_tracks)
|
_fix_track_number_tag(resolved, int(correct_num), total_tracks)
|
||||||
|
|
||||||
|
|
@ -1185,8 +1185,8 @@ class RepairWorker:
|
||||||
cursor.execute("UPDATE tracks SET file_path = ? WHERE file_path = ?",
|
cursor.execute("UPDATE tracks SET file_path = ? WHERE file_path = ?",
|
||||||
(new_path, resolved))
|
(new_path, resolved))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to update DB file_path after rename: %s", e)
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
@ -1544,8 +1544,8 @@ class RepairWorker:
|
||||||
if not os.path.exists(d):
|
if not os.path.exists(d):
|
||||||
try:
|
try:
|
||||||
shutil.move(s, d)
|
shutil.move(s, d)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to move sidecar %s: %s", s, e)
|
||||||
|
|
||||||
# Clean up empty dirs
|
# Clean up empty dirs
|
||||||
self._cleanup_empty_parents(resolved)
|
self._cleanup_empty_parents(resolved)
|
||||||
|
|
@ -1652,8 +1652,8 @@ class RepairWorker:
|
||||||
if resolved and os.path.exists(resolved):
|
if resolved and os.path.exists(resolved):
|
||||||
try:
|
try:
|
||||||
os.remove(resolved)
|
os.remove(resolved)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to remove wrong file %s: %s", resolved, e)
|
||||||
if track_id:
|
if track_id:
|
||||||
try:
|
try:
|
||||||
conn = self.db._get_connection()
|
conn = self.db._get_connection()
|
||||||
|
|
@ -1661,8 +1661,8 @@ class RepairWorker:
|
||||||
cursor.execute("DELETE FROM tracks WHERE id = ?", (track_id,))
|
cursor.execute("DELETE FROM tracks WHERE id = ?", (track_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to delete wrong track row from DB: %s", e)
|
||||||
return {'success': True, 'action': 'redownload',
|
return {'success': True, 'action': 'redownload',
|
||||||
'message': f'Added "{expected_title}" to wishlist, removed wrong file'}
|
'message': f'Added "{expected_title}" to wishlist, removed wrong file'}
|
||||||
|
|
||||||
|
|
@ -2389,8 +2389,8 @@ class RepairWorker:
|
||||||
album_thumb = album_row[2]
|
album_thumb = album_row[2]
|
||||||
album_track_count = album_row[3]
|
album_track_count = album_row[3]
|
||||||
spotify_album_id = album_row[4] if len(album_row) > 4 else None
|
spotify_album_id = album_row[4] if len(album_row) > 4 else None
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to load album metadata for retag: %s", e)
|
||||||
finally:
|
finally:
|
||||||
if conn_meta:
|
if conn_meta:
|
||||||
conn_meta.close()
|
conn_meta.close()
|
||||||
|
|
@ -2529,8 +2529,8 @@ class RepairWorker:
|
||||||
if not os.path.exists(sidecar_dst):
|
if not os.path.exists(sidecar_dst):
|
||||||
try:
|
try:
|
||||||
shutil.move(sidecar_src, sidecar_dst)
|
shutil.move(sidecar_src, sidecar_dst)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to move sidecar %s: %s", sidecar_src, e)
|
||||||
|
|
||||||
# Update DB file path
|
# Update DB file path
|
||||||
conn = None
|
conn = None
|
||||||
|
|
@ -2550,8 +2550,8 @@ class RepairWorker:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE tracks SET file_path = ? WHERE file_path LIKE ? ESCAPE '^'",
|
"UPDATE tracks SET file_path = ? WHERE file_path LIKE ? ESCAPE '^'",
|
||||||
(dst, '%/' + escaped))
|
(dst, '%/' + escaped))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Suffix-match DB path update failed: %s", e)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("DB path update failed for %s: %s", src, e)
|
logger.debug("DB path update failed for %s: %s", src, e)
|
||||||
|
|
@ -2646,8 +2646,8 @@ class RepairWorker:
|
||||||
if os.path.exists(out_path):
|
if os.path.exists(out_path):
|
||||||
try:
|
try:
|
||||||
os.remove(out_path)
|
os.remove(out_path)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to remove out_path after ffmpeg failure: %s", e)
|
||||||
return {'success': False, 'error': f'ffmpeg conversion failed: {proc.stderr[:200] if proc.stderr else "unknown error"}'}
|
return {'success': False, 'error': f'ffmpeg conversion failed: {proc.stderr[:200] if proc.stderr else "unknown error"}'}
|
||||||
|
|
||||||
# Update QUALITY tag
|
# Update QUALITY tag
|
||||||
|
|
@ -2664,8 +2664,8 @@ class RepairWorker:
|
||||||
from mutagen.mp4 import MP4FreeForm
|
from mutagen.mp4 import MP4FreeForm
|
||||||
audio['----:com.apple.iTunes:QUALITY'] = [MP4FreeForm(quality_label.encode('utf-8'))]
|
audio['----:com.apple.iTunes:QUALITY'] = [MP4FreeForm(quality_label.encode('utf-8'))]
|
||||||
audio.save()
|
audio.save()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to write QUALITY tag on lossy copy: %s", e)
|
||||||
|
|
||||||
# Embed cover art from source FLAC
|
# Embed cover art from source FLAC
|
||||||
if codec in ('opus', 'aac'):
|
if codec in ('opus', 'aac'):
|
||||||
|
|
@ -2697,8 +2697,8 @@ class RepairWorker:
|
||||||
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG
|
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG
|
||||||
dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)]
|
dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)]
|
||||||
dest_audio.save()
|
dest_audio.save()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to embed cover art in lossy copy: %s", e)
|
||||||
|
|
||||||
# Blasphemy Mode — uses the job's own setting, not the global lossy_copy one
|
# Blasphemy Mode — uses the job's own setting, not the global lossy_copy one
|
||||||
delete_original = False
|
delete_original = False
|
||||||
|
|
@ -2724,8 +2724,8 @@ class RepairWorker:
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to update DB path after lossy conversion: %s", e)
|
||||||
return {'success': True, 'action': 'converted_and_deleted',
|
return {'success': True, 'action': 'converted_and_deleted',
|
||||||
'message': f'Converted to {quality_label} and deleted original'}
|
'message': f'Converted to {quality_label} and deleted original'}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -2738,8 +2738,8 @@ class RepairWorker:
|
||||||
if os.path.exists(out_path):
|
if os.path.exists(out_path):
|
||||||
try:
|
try:
|
||||||
os.remove(out_path)
|
os.remove(out_path)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to remove out_path after timeout: %s", e)
|
||||||
return {'success': False, 'error': 'Conversion timed out (120s)'}
|
return {'success': False, 'error': 'Conversion timed out (120s)'}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'success': False, 'error': f'Conversion error: {e}'}
|
return {'success': False, 'error': f'Conversion error: {e}'}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,13 @@ Tag writing uses mutagen directly to stay consistent with the rest of the codeba
|
||||||
Supported formats: MP3, FLAC, OGG Vorbis, Opus, M4A/MP4
|
Supported formats: MP3, FLAC, OGG Vorbis, Opus, M4A/MP4
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Tuple, Dict
|
from typing import Optional, Tuple, Dict
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ReplayGain 2.0 reference level (EBU R128)
|
# ReplayGain 2.0 reference level (EBU R128)
|
||||||
RG_REFERENCE_LUFS = -18.0
|
RG_REFERENCE_LUFS = -18.0
|
||||||
|
|
||||||
|
|
@ -189,8 +192,8 @@ def read_replaygain_tags(file_path: str) -> Dict[str, Optional[str]]:
|
||||||
result['track_peak'] = _mp4_rg(audio, _TAG_TRACK_PEAK)
|
result['track_peak'] = _mp4_rg(audio, _TAG_TRACK_PEAK)
|
||||||
result['album_gain'] = _mp4_rg(audio, _TAG_ALBUM_GAIN)
|
result['album_gain'] = _mp4_rg(audio, _TAG_ALBUM_GAIN)
|
||||||
result['album_peak'] = _mp4_rg(audio, _TAG_ALBUM_PEAK)
|
result['album_peak'] = _mp4_rg(audio, _TAG_ALBUM_PEAK)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read replaygain tags failed: %s", e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -207,8 +210,8 @@ def _read_id3_txxx(audio, description: str) -> Optional[str]:
|
||||||
if frame_key.upper() == key.upper():
|
if frame_key.upper() == key.upper():
|
||||||
frame = audio.tags[frame_key]
|
frame = audio.tags[frame_key]
|
||||||
return str(frame.text[0]) if frame.text else None
|
return str(frame.text[0]) if frame.text else None
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read id3 txxx frame failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -218,8 +221,8 @@ def _vorbis_first(audio, key: str) -> Optional[str]:
|
||||||
vals = audio.get(key) or audio.get(key.upper())
|
vals = audio.get(key) or audio.get(key.upper())
|
||||||
if vals:
|
if vals:
|
||||||
return str(vals[0])
|
return str(vals[0])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read vorbis comment failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -234,8 +237,8 @@ def _mp4_rg(audio, tag_name: str) -> Optional[str]:
|
||||||
if hasattr(val, 'decode'):
|
if hasattr(val, 'decode'):
|
||||||
return val.decode('utf-8')
|
return val.decode('utf-8')
|
||||||
return str(val)
|
return str(val)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read mp4 replaygain atom failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
matched_context_lock = threading.Lock()
|
matched_context_lock = threading.Lock()
|
||||||
matched_downloads_context: Dict[str, Dict[str, Any]] = {}
|
matched_downloads_context: Dict[str, Dict[str, Any]] = {}
|
||||||
tasks_lock = threading.Lock()
|
tasks_lock = threading.Lock()
|
||||||
|
|
@ -57,8 +60,8 @@ def add_activity_item(icon, title, subtitle, time_ago="Now", show_toast=True):
|
||||||
if show_toast and _activity_toast_emitter is not None:
|
if show_toast and _activity_toast_emitter is not None:
|
||||||
try:
|
try:
|
||||||
_activity_toast_emitter("dashboard:toast", activity_item)
|
_activity_toast_emitter("dashboard:toast", activity_item)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("emit activity toast failed: %s", e)
|
||||||
|
|
||||||
return activity_item
|
return activity_item
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,8 @@ class SeasonalDiscoveryService:
|
||||||
try:
|
try:
|
||||||
cursor.execute(f"ALTER TABLE {table} ADD COLUMN source TEXT NOT NULL DEFAULT 'spotify'")
|
cursor.execute(f"ALTER TABLE {table} ADD COLUMN source TEXT NOT NULL DEFAULT 'spotify'")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Column already exists
|
logger.debug("source column migration %s: %s", table, e)
|
||||||
|
|
||||||
logger.info("Seasonal discovery database schema initialized")
|
logger.info("Seasonal discovery database schema initialized")
|
||||||
|
|
||||||
|
|
@ -201,8 +201,8 @@ class SeasonalDiscoveryService:
|
||||||
val = row[0] if isinstance(row, tuple) else row['value']
|
val = row[0] if isinstance(row, tuple) else row['value']
|
||||||
if val in ('northern', 'southern'):
|
if val in ('northern', 'southern'):
|
||||||
return val
|
return val
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read hemisphere metadata failed: %s", e)
|
||||||
return 'northern'
|
return 'northern'
|
||||||
|
|
||||||
def get_current_season(self) -> Optional[str]:
|
def get_current_season(self) -> Optional[str]:
|
||||||
|
|
|
||||||
|
|
@ -280,8 +280,8 @@ class SoulIDWorker:
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("rollback failed: %s", _e)
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
|
|
@ -533,8 +533,8 @@ class SoulIDWorker:
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("rollback failed: %s", _e)
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
|
|
@ -595,8 +595,8 @@ class SoulIDWorker:
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("rollback failed: %s", _e)
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
|
|
@ -634,8 +634,8 @@ class SoulIDWorker:
|
||||||
if conn:
|
if conn:
|
||||||
try:
|
try:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("rollback failed: %s", _e)
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,8 @@ class SoulseekClient(DownloadSourcePlugin):
|
||||||
if session:
|
if session:
|
||||||
try:
|
try:
|
||||||
await session.close()
|
await session.close()
|
||||||
except:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("aiohttp session close: %s", _e)
|
||||||
|
|
||||||
async def _make_direct_request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict[str, Any]]:
|
async def _make_direct_request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict[str, Any]]:
|
||||||
"""Make a direct request to slskd without /api/v0/ prefix (for endpoints that work directly)"""
|
"""Make a direct request to slskd without /api/v0/ prefix (for endpoints that work directly)"""
|
||||||
|
|
@ -273,9 +273,9 @@ class SoulseekClient(DownloadSourcePlugin):
|
||||||
if session:
|
if session:
|
||||||
try:
|
try:
|
||||||
await session.close()
|
await session.close()
|
||||||
except:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("aiohttp direct session close: %s", _e)
|
||||||
|
|
||||||
def _process_search_responses(self, responses_data: List[Dict[str, Any]]) -> tuple[List[TrackResult], List[AlbumResult]]:
|
def _process_search_responses(self, responses_data: List[Dict[str, Any]]) -> tuple[List[TrackResult], List[AlbumResult]]:
|
||||||
"""Process search response data into TrackResult and AlbumResult objects"""
|
"""Process search response data into TrackResult and AlbumResult objects"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
@ -1621,8 +1621,8 @@ class SoulseekClient(DownloadSourcePlugin):
|
||||||
if resp.status in [200, 405]: # 405 means endpoint exists but wrong method
|
if resp.status in [200, 405]: # 405 means endpoint exists but wrong method
|
||||||
available_endpoints[f"direct_{endpoint}"] = f"Status: {resp.status}"
|
available_endpoints[f"direct_{endpoint}"] = f"Status: {resp.status}"
|
||||||
logger.info(f"[OK] Direct endpoint available: {simple_url} (Status: {resp.status})")
|
logger.info(f"[OK] Direct endpoint available: {simple_url} (Status: {resp.status})")
|
||||||
except:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("direct endpoint probe %s: %s", endpoint, _e)
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,8 +253,8 @@ class SoulSyncClient(MediaServerClient):
|
||||||
if self._progress_callback:
|
if self._progress_callback:
|
||||||
try:
|
try:
|
||||||
self._progress_callback(msg)
|
self._progress_callback(msg)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("progress callback failed: %s", e)
|
||||||
|
|
||||||
# ── Core Scanning ──
|
# ── Core Scanning ──
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ def _get_min_api_interval():
|
||||||
val = config_manager.get('spotify.min_api_interval', None)
|
val = config_manager.get('spotify.min_api_interval', None)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return max(0.1, float(val)) # Floor at 100ms to prevent abuse
|
return max(0.1, float(val)) # Floor at 100ms to prevent abuse
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("get min_api_interval setting: %s", e)
|
||||||
return MIN_API_INTERVAL
|
return MIN_API_INTERVAL
|
||||||
|
|
||||||
# Request queuing for burst handling
|
# Request queuing for burst handling
|
||||||
|
|
@ -136,8 +136,8 @@ def _set_global_rate_limit(retry_after_seconds, endpoint_name, has_real_header=F
|
||||||
detail=f'{"escalation #" + str(_rate_limit_hit_count) if escalated else "initial"}'
|
detail=f'{"escalation #" + str(_rate_limit_hit_count) if escalated else "initial"}'
|
||||||
f'{", real Retry-After" if has_real_header else ", estimated"}'
|
f'{", real Retry-After" if has_real_header else ", estimated"}'
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("api_call_tracker record rate_limit_ban: %s", e)
|
||||||
try:
|
try:
|
||||||
from core.metadata.status import publish_spotify_status
|
from core.metadata.status import publish_spotify_status
|
||||||
|
|
||||||
|
|
@ -148,8 +148,8 @@ def _set_global_rate_limit(retry_after_seconds, endpoint_name, has_real_header=F
|
||||||
rate_limit=_get_rate_limit_info(),
|
rate_limit=_get_rate_limit_info(),
|
||||||
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
|
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status set rate limit: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _is_globally_rate_limited():
|
def _is_globally_rate_limited():
|
||||||
|
|
@ -230,8 +230,8 @@ def _clear_rate_limit():
|
||||||
rate_limit=None,
|
rate_limit=None,
|
||||||
post_ban_cooldown=None,
|
post_ban_cooldown=None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status clear rate limit: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _detect_and_set_rate_limit(exception, endpoint_name="unknown"):
|
def _detect_and_set_rate_limit(exception, endpoint_name="unknown"):
|
||||||
|
|
@ -637,8 +637,8 @@ class SpotifyClient:
|
||||||
rate_limit=_get_rate_limit_info(),
|
rate_limit=_get_rate_limit_info(),
|
||||||
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
|
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status no-client: %s", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If globally rate limited, report as NOT authenticated so callers
|
# If globally rate limited, report as NOT authenticated so callers
|
||||||
|
|
@ -655,8 +655,8 @@ class SpotifyClient:
|
||||||
rate_limit=_get_rate_limit_info(),
|
rate_limit=_get_rate_limit_info(),
|
||||||
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
|
post_ban_cooldown=_get_post_ban_cooldown_remaining() or None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status rate-limited: %s", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Post-ban cooldown: after a ban expires, don't probe Spotify immediately.
|
# Post-ban cooldown: after a ban expires, don't probe Spotify immediately.
|
||||||
|
|
@ -675,8 +675,8 @@ class SpotifyClient:
|
||||||
rate_limit=None,
|
rate_limit=None,
|
||||||
post_ban_cooldown=remaining or None,
|
post_ban_cooldown=remaining or None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status post-ban cooldown: %s", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check cache first (lock only for brief read)
|
# Check cache first (lock only for brief read)
|
||||||
|
|
@ -692,8 +692,8 @@ class SpotifyClient:
|
||||||
rate_limit=None,
|
rate_limit=None,
|
||||||
post_ban_cooldown=None,
|
post_ban_cooldown=None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status cache hit: %s", e)
|
||||||
return self._auth_cached_result
|
return self._auth_cached_result
|
||||||
|
|
||||||
# Cache miss — make API call outside the lock.
|
# Cache miss — make API call outside the lock.
|
||||||
|
|
@ -718,11 +718,11 @@ class SpotifyClient:
|
||||||
rate_limit=None,
|
rate_limit=None,
|
||||||
post_ban_cooldown=None,
|
post_ban_cooldown=None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status no-token: %s", e)
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("cached token probe: %s", e)
|
||||||
|
|
||||||
# Use a dedicated probe client (retries=0) so a 429 here propagates
|
# Use a dedicated probe client (retries=0) so a 429 here propagates
|
||||||
# immediately and we can detect long Retry-After bans.
|
# immediately and we can detect long Retry-After bans.
|
||||||
|
|
@ -771,8 +771,8 @@ class SpotifyClient:
|
||||||
rate_limit=_get_rate_limit_info() if rate_limited_state else None,
|
rate_limit=_get_rate_limit_info() if rate_limited_state else None,
|
||||||
post_ban_cooldown=None,
|
post_ban_cooldown=None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status auth probe: %s", e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -793,8 +793,8 @@ class SpotifyClient:
|
||||||
rate_limit=None,
|
rate_limit=None,
|
||||||
post_ban_cooldown=None,
|
post_ban_cooldown=None,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("publish_spotify_status disconnect: %s", e)
|
||||||
|
|
||||||
cache_path = 'config/.spotify_cache'
|
cache_path = 'config/.spotify_cache'
|
||||||
try:
|
try:
|
||||||
|
|
@ -1245,8 +1245,8 @@ class SpotifyClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
tracks.append(Track.from_spotify_track(raw))
|
tracks.append(Track.from_spotify_track(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Track.from_spotify_track cache parse: %s", e)
|
||||||
if tracks:
|
if tracks:
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
@ -1298,8 +1298,8 @@ class SpotifyClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
artists.append(Artist.from_spotify_artist(raw))
|
artists.append(Artist.from_spotify_artist(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Artist.from_spotify_artist cache parse: %s", e)
|
||||||
if artists:
|
if artists:
|
||||||
query_lower = query.lower().strip()
|
query_lower = query.lower().strip()
|
||||||
artists.sort(key=lambda a: (0 if a.name.lower().strip() == query_lower else 1))
|
artists.sort(key=lambda a: (0 if a.name.lower().strip() == query_lower else 1))
|
||||||
|
|
@ -1361,8 +1361,8 @@ class SpotifyClient:
|
||||||
for raw in cached_results:
|
for raw in cached_results:
|
||||||
try:
|
try:
|
||||||
albums.append(Album.from_spotify_album(raw))
|
albums.append(Album.from_spotify_album(raw))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Album.from_spotify_album cache parse: %s", e)
|
||||||
if albums:
|
if albums:
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
|
|
@ -1636,8 +1636,8 @@ class SpotifyClient:
|
||||||
try:
|
try:
|
||||||
albums_list = cached.get('_albums', cached) if isinstance(cached, dict) else cached
|
albums_list = cached.get('_albums', cached) if isinstance(cached, dict) else cached
|
||||||
return [Album.from_spotify_album(ad) for ad in albums_list]
|
return [Album.from_spotify_album(ad) for ad in albums_list]
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Cache data incompatible, re-fetch
|
logger.debug("artist albums cache reuse: %s", e)
|
||||||
|
|
||||||
if self.is_spotify_authenticated():
|
if self.is_spotify_authenticated():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -234,8 +234,8 @@ class SpotifyWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null id table resolve failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._process_item(item)
|
self._process_item(item)
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ def get_top_artists(database, image_url_fixer: ImageUrlFixer, time_range: str, l
|
||||||
artist['soul_id'] = row[4]
|
artist['soul_id'] = row[4]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("top artists enrich failed: %s", e)
|
||||||
|
|
||||||
return artists
|
return artists
|
||||||
|
|
||||||
|
|
@ -114,8 +114,8 @@ def get_top_albums(database, image_url_fixer: ImageUrlFixer, time_range: str, li
|
||||||
album['artist_id'] = row[2]
|
album['artist_id'] = row[2]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("top albums enrich failed: %s", e)
|
||||||
|
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
|
|
@ -146,8 +146,8 @@ def get_top_tracks(database, image_url_fixer: ImageUrlFixer, time_range: str, li
|
||||||
track['artist_id'] = row[2]
|
track['artist_id'] = row[2]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("top tracks enrich failed: %s", e)
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,8 +131,8 @@ def read_file_tags(file_path: str) -> Dict[str, Any]:
|
||||||
result['replaygain_track_peak'] = rg.get('track_peak')
|
result['replaygain_track_peak'] = rg.get('track_peak')
|
||||||
result['replaygain_album_gain'] = rg.get('album_gain')
|
result['replaygain_album_gain'] = rg.get('album_gain')
|
||||||
result['replaygain_album_peak'] = rg.get('album_peak')
|
result['replaygain_album_peak'] = rg.get('album_peak')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("read replaygain tags failed: %s", e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -640,8 +640,8 @@ class TidalClient:
|
||||||
image_id = image_rel.get('id', '')
|
image_id = image_rel.get('id', '')
|
||||||
if image_id:
|
if image_id:
|
||||||
image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg"
|
image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg"
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("tidal v2 playlists image_url extract: %s", _e)
|
||||||
|
|
||||||
new_playlist = Playlist(
|
new_playlist = Playlist(
|
||||||
id=str(playlist_id),
|
id=str(playlist_id),
|
||||||
|
|
@ -1188,8 +1188,8 @@ class TidalClient:
|
||||||
image_id = image_rel.get('id', '')
|
image_id = image_rel.get('id', '')
|
||||||
if image_id:
|
if image_id:
|
||||||
playlist.image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg"
|
playlist.image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg"
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("tidal playlist image_url extract: %s", _e)
|
||||||
|
|
||||||
logger.info(f"Retrieved Tidal playlist '{playlist.name}' with {len(tracks)} tracks")
|
logger.info(f"Retrieved Tidal playlist '{playlist.name}' with {len(tracks)} tracks")
|
||||||
return playlist
|
return playlist
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ class TidalWorker:
|
||||||
authenticated = False
|
authenticated = False
|
||||||
try:
|
try:
|
||||||
authenticated = self.client.is_authenticated()
|
authenticated = self.client.is_authenticated()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("tidal auth status check: %s", e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
|
|
@ -176,8 +176,8 @@ class TidalWorker:
|
||||||
itype = item.get('type', '')
|
itype = item.get('type', '')
|
||||||
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
|
||||||
# Can't mark status without an ID — just skip
|
# Can't mark status without an ID — just skip
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("null-id item type lookup: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -440,8 +440,8 @@ def process_watchlist_scan_automatically(automation_id=None, profile_id=None, de
|
||||||
'new_tracks_found': str(total_new_tracks),
|
'new_tracks_found': str(total_new_tracks),
|
||||||
'tracks_added': str(total_added_to_wishlist),
|
'tracks_added': str(total_added_to_wishlist),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist_scan_completed emit failed: %s", e)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in automatic watchlist scan: {e}")
|
logger.error(f"Error in automatic watchlist scan: {e}")
|
||||||
|
|
@ -460,7 +460,7 @@ def process_watchlist_scan_automatically(automation_id=None, profile_id=None, de
|
||||||
# Clear one-time rescan cutoff after full scan cycle
|
# Clear one-time rescan cutoff after full scan cycle
|
||||||
try:
|
try:
|
||||||
scanner._clear_rescan_cutoff()
|
scanner._clear_rescan_cutoff()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — finally-block cleanup, logger may be torn down
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Always reset flag
|
# Always reset flag
|
||||||
|
|
|
||||||
|
|
@ -2745,8 +2745,8 @@ class WatchlistScanner:
|
||||||
if release_date_str and len(release_date_str) >= 10:
|
if release_date_str and len(release_date_str) >= 10:
|
||||||
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
||||||
is_new = (datetime.now() - release_date).days <= 30
|
is_new = (datetime.now() - release_date).days <= 30
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album release_date parse failed: %s", e)
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
try:
|
try:
|
||||||
|
|
@ -2778,8 +2778,8 @@ class WatchlistScanner:
|
||||||
synth_pop += 15
|
synth_pop += 15
|
||||||
elif age_days <= 365:
|
elif age_days <= 365:
|
||||||
synth_pop += 5
|
synth_pop += 5
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("synthetic popularity age calc failed: %s", e)
|
||||||
if similar_artist.occurrence_count >= 3:
|
if similar_artist.occurrence_count >= 3:
|
||||||
synth_pop += 10
|
synth_pop += 10
|
||||||
elif similar_artist.occurrence_count >= 2:
|
elif similar_artist.occurrence_count >= 2:
|
||||||
|
|
@ -2902,8 +2902,8 @@ class WatchlistScanner:
|
||||||
if release_date_str and len(release_date_str) >= 10:
|
if release_date_str and len(release_date_str) >= 10:
|
||||||
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
||||||
is_new = (datetime.now() - release_date).days <= 30
|
is_new = (datetime.now() - release_date).days <= 30
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album release_date parse failed: %s", e)
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
try:
|
try:
|
||||||
|
|
@ -3107,8 +3107,8 @@ class WatchlistScanner:
|
||||||
release_date = datetime.strptime(release_date_str, "%Y-%m-%d")
|
release_date = datetime.strptime(release_date_str, "%Y-%m-%d")
|
||||||
days_old = (datetime.now() - release_date).days
|
days_old = (datetime.now() - release_date).days
|
||||||
is_new = days_old <= 30
|
is_new = days_old <= 30
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("new-release date parse: %s", e)
|
||||||
|
|
||||||
# Add each track to discovery pool
|
# Add each track to discovery pool
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
|
|
@ -3214,8 +3214,8 @@ class WatchlistScanner:
|
||||||
elif profile['avg_daily_plays'] > 20:
|
elif profile['avg_daily_plays'] > 20:
|
||||||
days_lookback = 21 # Heavy listener — keep it fresh
|
days_lookback = 21 # Heavy listener — keep it fresh
|
||||||
logger.info(f"Recent albums window: {days_lookback} days (avg {profile['avg_daily_plays']:.1f} plays/day)")
|
logger.info(f"Recent albums window: {days_lookback} days (avg {profile['avg_daily_plays']:.1f} plays/day)")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("listening profile lookback adjust failed: %s", e)
|
||||||
cutoff_date = datetime.now() - timedelta(days=days_lookback)
|
cutoff_date = datetime.now() - timedelta(days=days_lookback)
|
||||||
discovery_sources = self._discovery_source_priority()
|
discovery_sources = self._discovery_source_priority()
|
||||||
if not discovery_sources:
|
if not discovery_sources:
|
||||||
|
|
@ -3498,8 +3498,8 @@ class WatchlistScanner:
|
||||||
_artist_genre_cache[_row[0].lower()] = {g.strip().lower() for g in _row[1].split(',') if g.strip()}
|
_artist_genre_cache[_row[0].lower()] = {g.strip().lower() for g in _row[1].split(',') if g.strip()}
|
||||||
_conn.close()
|
_conn.close()
|
||||||
logger.debug(f"Built genre cache for {len(_artist_genre_cache)} artists")
|
logger.debug(f"Built genre cache for {len(_artist_genre_cache)} artists")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist genre cache build failed: %s", e)
|
||||||
|
|
||||||
logger.info(f"Curating playlists for sources: {sources_to_process}")
|
logger.info(f"Curating playlists for sources: {sources_to_process}")
|
||||||
|
|
||||||
|
|
@ -3552,8 +3552,8 @@ class WatchlistScanner:
|
||||||
if release_date_str and len(release_date_str) >= 10:
|
if release_date_str and len(release_date_str) >= 10:
|
||||||
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
||||||
days_old = (datetime.now() - release_date).days
|
days_old = (datetime.now() - release_date).days
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("release-date parse: %s", e)
|
||||||
|
|
||||||
for track in album_data['tracks'].get('items', []):
|
for track in album_data['tracks'].get('items', []):
|
||||||
track_id = track.get('id')
|
track_id = track.get('id')
|
||||||
|
|
@ -3760,8 +3760,8 @@ class WatchlistScanner:
|
||||||
_wa_list = self.database.get_watchlist_artists(profile_id=profile_id)
|
_wa_list = self.database.get_watchlist_artists(profile_id=profile_id)
|
||||||
for _wa in _wa_list:
|
for _wa in _wa_list:
|
||||||
_wa_id_to_name[str(_wa.id)] = (_wa.artist_name or '').lower()
|
_wa_id_to_name[str(_wa.id)] = (_wa.artist_name or '').lower()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist artist id-to-name map failed: %s", e)
|
||||||
|
|
||||||
all_similar = self.database.get_top_similar_artists(limit=200, profile_id=profile_id)
|
all_similar = self.database.get_top_similar_artists(limit=200, profile_id=profile_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def load_wishlist_keys(cursor, profile_id: int) -> set[str]:
|
def load_wishlist_keys(cursor, profile_id: int) -> set[str]:
|
||||||
|
|
@ -27,21 +30,21 @@ def load_wishlist_keys(cursor, profile_id: int) -> set[str]:
|
||||||
wa = ""
|
wa = ""
|
||||||
if wname:
|
if wname:
|
||||||
keys.add(wname + "|||" + wa.lower().strip())
|
keys.add(wname + "|||" + wa.lower().strip())
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("parse wishlist row failed: %s", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute("SELECT spotify_data FROM wishlist_tracks WHERE profile_id = ?", (profile_id,))
|
cursor.execute("SELECT spotify_data FROM wishlist_tracks WHERE profile_id = ?", (profile_id,))
|
||||||
_absorb(cursor.fetchall())
|
_absorb(cursor.fetchall())
|
||||||
return keys
|
return keys
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("profile-aware wishlist query failed: %s", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute("SELECT spotify_data FROM wishlist_tracks")
|
cursor.execute("SELECT spotify_data FROM wishlist_tracks")
|
||||||
_absorb(cursor.fetchall())
|
_absorb(cursor.fetchall())
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("legacy wishlist query failed: %s", e)
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,8 +215,8 @@ def finalize_auto_wishlist_completion(
|
||||||
'tracks_found': str(tracks_added),
|
'tracks_found': str(tracks_added),
|
||||||
'tracks_failed': str(total_failed - tracks_added),
|
'tracks_failed': str(total_failed - tracks_added),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("emit wishlist_processing_completed failed: %s", e)
|
||||||
|
|
||||||
return completion_summary
|
return completion_summary
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,8 @@ class WebMetadataUpdateWorker:
|
||||||
raw = self._db.api_get_artist(best.id)
|
raw = self._db.api_get_artist(best.id)
|
||||||
if raw:
|
if raw:
|
||||||
spotify_artist_id = raw.get('spotify_artist_id')
|
spotify_artist_id = raw.get('spotify_artist_id')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("get spotify_artist_id failed: %s", e)
|
||||||
return best, has_genres, spotify_artist_id
|
return best, has_genres, spotify_artist_id
|
||||||
except Exception:
|
except Exception:
|
||||||
return None, False, None
|
return None, False, None
|
||||||
|
|
@ -419,10 +419,10 @@ class WebMetadataUpdateWorker:
|
||||||
try:
|
try:
|
||||||
for album in artist.albums():
|
for album in artist.albums():
|
||||||
if hasattr(album, 'genres') and album.genres:
|
if hasattr(album, 'genres') and album.genres:
|
||||||
album_genres.update(genre.tag if hasattr(genre, 'tag') else str(genre)
|
album_genres.update(genre.tag if hasattr(genre, 'tag') else str(genre)
|
||||||
for genre in album.genres)
|
for genre in album.genres)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Albums might not be accessible
|
logger.debug("artist album genre walk: %s", e)
|
||||||
|
|
||||||
# Combine all genres (prioritize Spotify genres)
|
# Combine all genres (prioritize Spotify genres)
|
||||||
all_genres = spotify_genres.union(album_genres)
|
all_genres = spotify_genres.union(album_genres)
|
||||||
|
|
|
||||||
|
|
@ -1190,8 +1190,8 @@ class YouTubeClient(DownloadSourcePlugin):
|
||||||
album_data = track_details.get('album', {})
|
album_data = track_details.get('album', {})
|
||||||
if album_data.get('artists'):
|
if album_data.get('artists'):
|
||||||
album_artist = album_data['artists'][0]
|
album_artist = album_data['artists'][0]
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify album artist lookup: %s", e)
|
||||||
|
|
||||||
logger.debug(" Setting metadata tags...")
|
logger.debug(" Setting metadata tags...")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -650,8 +650,8 @@ class MusicDatabase:
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE sync_history ADD COLUMN track_results TEXT")
|
cursor.execute("ALTER TABLE sync_history ADD COLUMN track_results TEXT")
|
||||||
logger.info("Added track_results column to sync_history table")
|
logger.info("Added track_results column to sync_history table")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to add track_results column: %s", e)
|
||||||
|
|
||||||
# Migration: add source_page column to sync_history (UI origin context for batch panel)
|
# Migration: add source_page column to sync_history (UI origin context for batch panel)
|
||||||
try:
|
try:
|
||||||
|
|
@ -660,8 +660,8 @@ class MusicDatabase:
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE sync_history ADD COLUMN source_page TEXT")
|
cursor.execute("ALTER TABLE sync_history ADD COLUMN source_page TEXT")
|
||||||
logger.info("Added source_page column to sync_history table")
|
logger.info("Added source_page column to sync_history table")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to add source_page column: %s", e)
|
||||||
|
|
||||||
# Migration: add track_artist column for per-track artist on compilations/DJ mixes
|
# Migration: add track_artist column for per-track artist on compilations/DJ mixes
|
||||||
try:
|
try:
|
||||||
|
|
@ -670,8 +670,8 @@ class MusicDatabase:
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE tracks ADD COLUMN track_artist TEXT")
|
cursor.execute("ALTER TABLE tracks ADD COLUMN track_artist TEXT")
|
||||||
logger.info("Added track_artist column to tracks table")
|
logger.info("Added track_artist column to tracks table")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to add track_artist column: %s", e)
|
||||||
|
|
||||||
# Migration: add file_size column so the Stats page can show
|
# Migration: add file_size column so the Stats page can show
|
||||||
# total library size on disk without having to walk the
|
# total library size on disk without having to walk the
|
||||||
|
|
@ -687,8 +687,8 @@ class MusicDatabase:
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE tracks ADD COLUMN file_size INTEGER")
|
cursor.execute("ALTER TABLE tracks ADD COLUMN file_size INTEGER")
|
||||||
logger.info("Added file_size column to tracks table")
|
logger.info("Added file_size column to tracks table")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to add file_size column: %s", e)
|
||||||
|
|
||||||
# One-time migration: purge discovery cache entries that lack track_number.
|
# One-time migration: purge discovery cache entries that lack track_number.
|
||||||
# Prior versions cached discovery results without track_number/disc_number/release_date,
|
# Prior versions cached discovery results without track_number/disc_number/release_date,
|
||||||
|
|
@ -704,8 +704,8 @@ class MusicDatabase:
|
||||||
cursor.execute("CREATE TABLE _discovery_cache_v2_migrated (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
cursor.execute("CREATE TABLE _discovery_cache_v2_migrated (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
||||||
if purged > 0:
|
if purged > 0:
|
||||||
logger.info(f"Purged {purged} stale discovery cache entries (missing track_number)")
|
logger.info(f"Purged {purged} stale discovery cache entries (missing track_number)")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to purge stale discovery cache entries: %s", e)
|
||||||
|
|
||||||
# One-time migration: purge Deezer album/track cache entries with missing data.
|
# One-time migration: purge Deezer album/track cache entries with missing data.
|
||||||
# Deezer's /artist/{id}/albums returns albums without artist info, and search
|
# Deezer's /artist/{id}/albums returns albums without artist info, and search
|
||||||
|
|
@ -721,8 +721,8 @@ class MusicDatabase:
|
||||||
cursor.execute("CREATE TABLE _deezer_cache_v2_migrated (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
cursor.execute("CREATE TABLE _deezer_cache_v2_migrated (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
||||||
if purged > 0:
|
if purged > 0:
|
||||||
logger.info(f"Purged {purged} stale Deezer cache entries (missing artist/track_position)")
|
logger.info(f"Purged {purged} stale Deezer cache entries (missing artist/track_position)")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to purge stale Deezer cache entries: %s", e)
|
||||||
|
|
||||||
# One-time migration: purge cached tracks/albums with junk artist names.
|
# One-time migration: purge cached tracks/albums with junk artist names.
|
||||||
# The cache gate now rejects these, but existing entries need cleaning.
|
# The cache gate now rejects these, but existing entries need cleaning.
|
||||||
|
|
@ -738,8 +738,8 @@ class MusicDatabase:
|
||||||
cursor.execute("CREATE TABLE _cache_junk_artist_purged (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
cursor.execute("CREATE TABLE _cache_junk_artist_purged (applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
||||||
if purged > 0:
|
if purged > 0:
|
||||||
logger.info(f"Purged {purged} cached tracks/albums with junk artist names")
|
logger.info(f"Purged {purged} cached tracks/albums with junk artist names")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to purge cached tracks/albums with junk artist names: %s", e)
|
||||||
|
|
||||||
# HiFi API instances table
|
# HiFi API instances table
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
|
|
@ -819,8 +819,8 @@ class MusicDatabase:
|
||||||
config = json.loads(row[2]) if row[2] else {}
|
config = json.loads(row[2]) if row[2] else {}
|
||||||
then_actions = json.dumps([{'type': row[1], 'config': config}])
|
then_actions = json.dumps([{'type': row[1], 'config': config}])
|
||||||
cursor.execute("UPDATE automations SET then_actions = ? WHERE id = ?", (then_actions, row[0]))
|
cursor.execute("UPDATE automations SET then_actions = ? WHERE id = ?", (then_actions, row[0]))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to migrate notify data for automation row: %s", e)
|
||||||
logger.info("Migrated existing notify data to then_actions")
|
logger.info("Migrated existing notify data to then_actions")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding automation then_actions column: {e}")
|
logger.error(f"Error adding automation then_actions column: {e}")
|
||||||
|
|
@ -1527,8 +1527,8 @@ class MusicDatabase:
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE liked_albums_pool ADD COLUMN discogs_release_id TEXT")
|
cursor.execute("ALTER TABLE liked_albums_pool ADD COLUMN discogs_release_id TEXT")
|
||||||
logger.info("Added discogs_release_id column to liked_albums_pool")
|
logger.info("Added discogs_release_id column to liked_albums_pool")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to add discogs_release_id column: %s", e)
|
||||||
|
|
||||||
logger.info("Discovery tables added/verified successfully")
|
logger.info("Discovery tables added/verified successfully")
|
||||||
|
|
||||||
|
|
@ -2468,8 +2468,8 @@ class MusicDatabase:
|
||||||
if 'profile_id' not in columns:
|
if 'profile_id' not in columns:
|
||||||
cursor.execute(f"ALTER TABLE {table} ADD COLUMN profile_id INTEGER DEFAULT 1")
|
cursor.execute(f"ALTER TABLE {table} ADD COLUMN profile_id INTEGER DEFAULT 1")
|
||||||
logger.info(f"Repaired missing profile_id column on {table}")
|
logger.info(f"Repaired missing profile_id column on {table}")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to repair profile_id column on %s: %s", table, e)
|
||||||
|
|
||||||
if already_migrated:
|
if already_migrated:
|
||||||
return # Rest of migration already done
|
return # Rest of migration already done
|
||||||
|
|
@ -2666,8 +2666,8 @@ class MusicDatabase:
|
||||||
for idx_name, table in index_pairs:
|
for idx_name, table in index_pairs:
|
||||||
try:
|
try:
|
||||||
cursor.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table} (profile_id)")
|
cursor.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table} (profile_id)")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to create index %s on %s: %s", idx_name, table, e)
|
||||||
|
|
||||||
# Set migration marker
|
# Set migration marker
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
|
|
@ -3326,8 +3326,8 @@ class MusicDatabase:
|
||||||
))
|
))
|
||||||
if cursor.rowcount > 0:
|
if cursor.rowcount > 0:
|
||||||
inserted += 1
|
inserted += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to insert listening event: %s", e)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return inserted
|
return inserted
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -3635,8 +3635,8 @@ class MusicDatabase:
|
||||||
total_size = 0
|
total_size = 0
|
||||||
try:
|
try:
|
||||||
total_size = os.path.getsize(str(self.database_path))
|
total_size = os.path.getsize(str(self.database_path))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to stat database file size: %s", e)
|
||||||
|
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
@ -3663,8 +3663,8 @@ class MusicDatabase:
|
||||||
cursor.execute(f"SELECT COUNT(*) FROM [{tbl}]")
|
cursor.execute(f"SELECT COUNT(*) FROM [{tbl}]")
|
||||||
count = cursor.fetchone()[0]
|
count = cursor.fetchone()[0]
|
||||||
tables.append({'name': tbl, 'size': count})
|
tables.append({'name': tbl, 'size': count})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to get row count for table %s: %s", tbl, e)
|
||||||
tables.sort(key=lambda x: x['size'], reverse=True)
|
tables.sort(key=lambda x: x['size'], reverse=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -4189,8 +4189,8 @@ class MusicDatabase:
|
||||||
'bubble_snapshots', 'recent_releases']:
|
'bubble_snapshots', 'recent_releases']:
|
||||||
try:
|
try:
|
||||||
cursor.execute(f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,))
|
cursor.execute(f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to delete from %s for profile: %s", table, e)
|
||||||
cursor.execute("DELETE FROM profiles WHERE id = ?", (profile_id,))
|
cursor.execute("DELETE FROM profiles WHERE id = ?", (profile_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
@ -5152,8 +5152,8 @@ class MusicDatabase:
|
||||||
album_artist_name = artist_row[0] if artist_row else ''
|
album_artist_name = artist_row[0] if artist_row else ''
|
||||||
if nav_artist and nav_artist.lower() != album_artist_name.lower():
|
if nav_artist and nav_artist.lower() != album_artist_name.lower():
|
||||||
track_artist = nav_artist
|
track_artist = nav_artist
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to load album artist for track_artist comparison: %s", e)
|
||||||
|
|
||||||
# Extract MusicBrainz recording ID from server if available (Navidrome provides this)
|
# Extract MusicBrainz recording ID from server if available (Navidrome provides this)
|
||||||
mbid = getattr(track_obj, 'musicBrainzId', None) or None
|
mbid = getattr(track_obj, 'musicBrainzId', None) or None
|
||||||
|
|
@ -5219,8 +5219,8 @@ class MusicDatabase:
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
thumb_url=album_row[1] if album_row and len(album_row) > 1 else None
|
thumb_url=album_row[1] if album_row and len(album_row) > 1 else None
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Non-critical history logging
|
logger.debug("history logging: %s", e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -10901,8 +10901,8 @@ class MusicDatabase:
|
||||||
""")
|
""")
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
source_counts[row['download_source']] = row['cnt']
|
source_counts[row['download_source']] = row['cnt']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to load library history source counts: %s", e)
|
||||||
stats['source_counts'] = source_counts
|
stats['source_counts'] = source_counts
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
@ -11193,8 +11193,8 @@ class MusicDatabase:
|
||||||
WHERE playlist_id = ? AND source_track_id IS NOT NULL AND extra_data IS NOT NULL
|
WHERE playlist_id = ? AND source_track_id IS NOT NULL AND extra_data IS NOT NULL
|
||||||
""", (playlist_id,))
|
""", (playlist_id,))
|
||||||
old_extra_map = {row['source_track_id']: row['extra_data'] for row in cursor.fetchall()}
|
old_extra_map = {row['source_track_id']: row['extra_data'] for row in cursor.fetchall()}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("Failed to preserve mirrored playlist extra_data: %s", e)
|
||||||
|
|
||||||
# Replace all tracks
|
# Replace all tracks
|
||||||
cursor.execute("DELETE FROM mirrored_playlist_tracks WHERE playlist_id=?", (playlist_id,))
|
cursor.execute("DELETE FROM mirrored_playlist_tracks WHERE playlist_id=?", (playlist_id,))
|
||||||
|
|
@ -12137,5 +12137,5 @@ def close_database():
|
||||||
db_instance.close()
|
db_instance.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Ignore threading errors during shutdown
|
# Ignore threading errors during shutdown
|
||||||
pass
|
logger.debug("db instance close: %s", e)
|
||||||
_database_instances.clear()
|
_database_instances.clear()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ line-length = 160
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# Start lenient — catch real bugs, not style nits
|
# Start lenient — catch real bugs, not style nits
|
||||||
# E: pycodestyle errors, F: pyflakes, UP: pyupgrade, B: bugbear
|
# E: pycodestyle errors, F: pyflakes, UP: pyupgrade, B: bugbear
|
||||||
select = ["F", "E9", "W6", "B"]
|
select = ["F", "E9", "W6", "B", "S110"]
|
||||||
# Ignore rules that will flag too much in an existing codebase
|
# Ignore rules that will flag too much in an existing codebase
|
||||||
ignore = [
|
ignore = [
|
||||||
"F401", # unused imports (too noisy initially)
|
"F401", # unused imports (too noisy initially)
|
||||||
|
|
@ -15,7 +15,7 @@ ignore = [
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
# Tests can use assert, magic values, etc.
|
# Tests can use assert, magic values, etc.
|
||||||
"tests/**" = ["B", "F"]
|
"tests/**" = ["B", "F", "S110"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
markers = [
|
markers = [
|
||||||
|
|
|
||||||
|
|
@ -545,8 +545,8 @@ class PlaylistSyncService:
|
||||||
spotify_id, me.clean_title(original_title), me.clean_artist(artist_name),
|
spotify_id, me.clean_title(original_title), me.clean_artist(artist_name),
|
||||||
active_server, db_track.id, db_track.title, confidence
|
active_server, db_track.id, db_track.title, confidence
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("save sync match cache failed: %s", e)
|
||||||
|
|
||||||
# Fetch the actual track object from active media server using the database track ID
|
# Fetch the actual track object from active media server using the database track ID
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
384
web_server.py
384
web_server.py
|
|
@ -51,8 +51,8 @@ def _build_version_string():
|
||||||
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
|
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
sha = result.stdout.strip()
|
sha = result.stdout.strip()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("git rev-parse failed: %s", e)
|
||||||
if sha:
|
if sha:
|
||||||
return f"{_SOULSYNC_BASE_VERSION}+{sha[:7]}"
|
return f"{_SOULSYNC_BASE_VERSION}+{sha[:7]}"
|
||||||
return _SOULSYNC_BASE_VERSION
|
return _SOULSYNC_BASE_VERSION
|
||||||
|
|
@ -387,8 +387,8 @@ def _set_profile_context():
|
||||||
session.pop('profile_id', None)
|
session.pop('profile_id', None)
|
||||||
from flask import jsonify as _jsonify
|
from flask import jsonify as _jsonify
|
||||||
return _jsonify({"error": "profile_required", "message": "Profile no longer exists"}), 401
|
return _jsonify({"error": "profile_required", "message": "Profile no longer exists"}), 401
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # DB error — don't block requests, use the session value
|
logger.debug("profile session validate: %s", e)
|
||||||
|
|
||||||
g.profile_id = pid
|
g.profile_id = pid
|
||||||
|
|
||||||
|
|
@ -415,8 +415,8 @@ def _log_slow_request(response):
|
||||||
response.status_code,
|
response.status_code,
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("slow request log failed: %s", e)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -515,8 +515,8 @@ def check_download_permission():
|
||||||
profile = get_database().get_profile(pid)
|
profile = get_database().get_profile(pid)
|
||||||
if profile and not profile.get('can_download', True):
|
if profile and not profile.get('can_download', True):
|
||||||
return jsonify({'success': False, 'error': 'Downloads are disabled for this profile.'}), 403
|
return jsonify({'success': False, 'error': 'Downloads are disabled for this profile.'}), 403
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # DB error — don't block
|
logger.debug("download permission check: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- Docker Helper Functions ---
|
# --- Docker Helper Functions ---
|
||||||
|
|
@ -1249,8 +1249,8 @@ def _register_automation_handlers():
|
||||||
'added': str(added_count),
|
'added': str(added_count),
|
||||||
'removed': str(removed_count),
|
'removed': str(removed_count),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("playlist_synced automation emit failed: %s", e)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[AUTOMATION] No changes: '{pl.get('name', '')}' (tracks={len(old_ids)})")
|
logger.warning(f"[AUTOMATION] No changes: '{pl.get('name', '')}' (tracks={len(old_ids)})")
|
||||||
_update_automation_progress(auto_id,
|
_update_automation_progress(auto_id,
|
||||||
|
|
@ -1397,8 +1397,8 @@ def _register_automation_handlers():
|
||||||
'status': 'skipped',
|
'status': 'skipped',
|
||||||
'reason': f'All {len(tracks_json)} tracks unchanged since last sync',
|
'reason': f'All {len(tracks_json)} tracks unchanged since last sync',
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # If we can't read last status, just run the sync
|
logger.debug("mirror sync last-status read: %s", e)
|
||||||
|
|
||||||
_update_automation_progress(auto_id, progress=50,
|
_update_automation_progress(auto_id, progress=50,
|
||||||
phase=f'Syncing "{pl["name"]}"',
|
phase=f'Syncing "{pl["name"]}"',
|
||||||
|
|
@ -1855,8 +1855,8 @@ def _register_automation_handlers():
|
||||||
elif os.path.isdir(fp):
|
elif os.path.isdir(fp):
|
||||||
_shutil.rmtree(fp)
|
_shutil.rmtree(fp)
|
||||||
removed += 1
|
removed += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("quarantine entry purge failed: %s", e)
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
log_line=f'Removed {removed} quarantined items', log_type='success' if removed > 0 else 'info')
|
log_line=f'Removed {removed} quarantined items', log_type='success' if removed > 0 else 'info')
|
||||||
return {'status': 'completed', 'removed': str(removed)}
|
return {'status': 'completed', 'removed': str(removed)}
|
||||||
|
|
@ -1961,8 +1961,8 @@ def _register_automation_handlers():
|
||||||
while len(existing) > max_backups:
|
while len(existing) > max_backups:
|
||||||
try:
|
try:
|
||||||
os.remove(existing.pop(0))
|
os.remove(existing.pop(0))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("rolling backup cleanup failed: %s", e)
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
log_line=f'Backup created: {size_mb}MB ({os.path.basename(backup_path)})', log_type='success')
|
log_line=f'Backup created: {size_mb}MB ({os.path.basename(backup_path)})', log_type='success')
|
||||||
return {'status': 'completed', 'backup_path': backup_path, 'size_mb': str(size_mb)}
|
return {'status': 'completed', 'backup_path': backup_path, 'size_mb': str(size_mb)}
|
||||||
|
|
@ -2101,8 +2101,8 @@ def _register_automation_handlers():
|
||||||
elif os.path.isdir(fp):
|
elif os.path.isdir(fp):
|
||||||
_shutil.rmtree(fp)
|
_shutil.rmtree(fp)
|
||||||
q_removed += 1
|
q_removed += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("quarantine entry purge failed: %s", e)
|
||||||
steps.append(f'Quarantine: removed {q_removed} items')
|
steps.append(f'Quarantine: removed {q_removed} items')
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
log_line=f'Quarantine: removed {q_removed} items', log_type='success' if q_removed else 'info')
|
log_line=f'Quarantine: removed {q_removed} items', log_type='success' if q_removed else 'info')
|
||||||
|
|
@ -2166,8 +2166,8 @@ def _register_automation_handlers():
|
||||||
for hidden in entries:
|
for hidden in entries:
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(dirpath, hidden))
|
os.remove(os.path.join(dirpath, hidden))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hidden file cleanup failed: %s", e)
|
||||||
try:
|
try:
|
||||||
os.rmdir(dirpath)
|
os.rmdir(dirpath)
|
||||||
s_removed += 1
|
s_removed += 1
|
||||||
|
|
@ -2515,8 +2515,8 @@ def get_cached_transfer_data():
|
||||||
all_downloads = run_async(
|
all_downloads = run_async(
|
||||||
download_orchestrator.engine.get_all_downloads(exclude=('soulseek',))
|
download_orchestrator.engine.get_all_downloads(exclude=('soulseek',))
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("get_all_downloads failed: %s", e)
|
||||||
for download in all_downloads:
|
for download in all_downloads:
|
||||||
key = _make_context_key(download.username, download.filename)
|
key = _make_context_key(download.username, download.filename)
|
||||||
# Convert DownloadStatus to transfer dict format
|
# Convert DownloadStatus to transfer dict format
|
||||||
|
|
@ -2974,13 +2974,13 @@ def _atexit_save_history():
|
||||||
try:
|
try:
|
||||||
from core.api_call_tracker import api_call_tracker
|
from core.api_call_tracker import api_call_tracker
|
||||||
api_call_tracker.save()
|
api_call_tracker.save()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — atexit handler, log handles may be closed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _atexit_shutdown():
|
def _atexit_shutdown():
|
||||||
try:
|
try:
|
||||||
_shutdown_runtime_components()
|
_shutdown_runtime_components()
|
||||||
except Exception:
|
except Exception: # noqa: S110 — atexit handler, log handles may be closed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3446,8 +3446,8 @@ def _get_enrichment_status():
|
||||||
if key == 'spotify_enrichment':
|
if key == 'spotify_enrichment':
|
||||||
try:
|
try:
|
||||||
svc_data['daily_budget'] = worker._get_daily_budget_info()
|
svc_data['daily_budget'] = worker._get_daily_budget_info()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify daily budget read failed: %s", e)
|
||||||
|
|
||||||
services[key] = svc_data
|
services[key] = svc_data
|
||||||
else:
|
else:
|
||||||
|
|
@ -4176,8 +4176,8 @@ def handle_settings():
|
||||||
# FIX: Re-instantiate the global tidal_client to pick up new settings
|
# FIX: Re-instantiate the global tidal_client to pick up new settings
|
||||||
try:
|
try:
|
||||||
tidal_client = TidalClient()
|
tidal_client = TidalClient()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Keep existing tidal_client if re-init fails
|
logger.debug("tidal client re-init: %s", e)
|
||||||
# Reload enrichment worker clients for key-based services
|
# Reload enrichment worker clients for key-based services
|
||||||
if lastfm_worker:
|
if lastfm_worker:
|
||||||
lastfm_worker._init_client()
|
lastfm_worker._init_client()
|
||||||
|
|
@ -4205,8 +4205,8 @@ def handle_settings():
|
||||||
# Include which download sources are configured so the UI can auto-disable unconfigured ones
|
# Include which download sources are configured so the UI can auto-disable unconfigured ones
|
||||||
try:
|
try:
|
||||||
data['_source_status'] = download_orchestrator.get_source_status()
|
data['_source_status'] = download_orchestrator.get_source_status()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("download source status read failed: %s", e)
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
@ -4330,8 +4330,8 @@ def hydrabase_connect():
|
||||||
if _hydrabase_ws:
|
if _hydrabase_ws:
|
||||||
try:
|
try:
|
||||||
_hydrabase_ws.close()
|
_hydrabase_ws.close()
|
||||||
except:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("hydrabase connect-existing close: %s", _e)
|
||||||
ws = websocket.create_connection(
|
ws = websocket.create_connection(
|
||||||
url,
|
url,
|
||||||
header={"x-api-key": api_key},
|
header={"x-api-key": api_key},
|
||||||
|
|
@ -4356,8 +4356,8 @@ def hydrabase_disconnect():
|
||||||
if _hydrabase_ws:
|
if _hydrabase_ws:
|
||||||
try:
|
try:
|
||||||
_hydrabase_ws.close()
|
_hydrabase_ws.close()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hydrabase disconnect close: %s", e)
|
||||||
_hydrabase_ws = None
|
_hydrabase_ws = None
|
||||||
config_manager.set('hydrabase.auto_connect', False)
|
config_manager.set('hydrabase.auto_connect', False)
|
||||||
# Only disable dev mode if not using Hydrabase as a regular fallback source
|
# Only disable dev mode if not using Hydrabase as a regular fallback source
|
||||||
|
|
@ -4428,8 +4428,8 @@ def hydrabase_send():
|
||||||
with _hydrabase_lock:
|
with _hydrabase_lock:
|
||||||
try:
|
try:
|
||||||
_hydrabase_ws.close()
|
_hydrabase_ws.close()
|
||||||
except:
|
except Exception as _e:
|
||||||
pass
|
logger.debug("hydrabase send close: %s", _e)
|
||||||
_hydrabase_ws = None
|
_hydrabase_ws = None
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
@ -6118,8 +6118,8 @@ def deezer_callback():
|
||||||
try:
|
try:
|
||||||
json_data = resp.json()
|
json_data = resp.json()
|
||||||
access_token = json_data.get('access_token')
|
access_token = json_data.get('access_token')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("deezer token json parse failed: %s", e)
|
||||||
|
|
||||||
if not access_token:
|
if not access_token:
|
||||||
return f"<h1>No Access Token</h1><p>Deezer response: {resp.text[:200]}</p>", 400
|
return f"<h1>No Access Token</h1><p>Deezer response: {resp.text[:200]}</p>", 400
|
||||||
|
|
@ -7451,8 +7451,8 @@ def get_download_status():
|
||||||
if transfer_id:
|
if transfer_id:
|
||||||
try:
|
try:
|
||||||
run_async(download_orchestrator.cancel_download(str(transfer_id), username, remove=True))
|
run_async(download_orchestrator.cancel_download(str(transfer_id), username, remove=True))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("orphan transfer cancel failed: %s", e)
|
||||||
_orphaned_download_keys.discard(context_key)
|
_orphaned_download_keys.discard(context_key)
|
||||||
continue # Skip normal post-processing either way
|
continue # Skip normal post-processing either way
|
||||||
|
|
||||||
|
|
@ -8475,8 +8475,8 @@ def get_artist_detail(artist_id):
|
||||||
except Exception:
|
except Exception:
|
||||||
enrichment_coverage[svc] = 0
|
enrichment_coverage[svc] = 0
|
||||||
enrichment_coverage['total_tracks'] = total
|
enrichment_coverage['total_tracks'] = total
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("enrichment coverage build failed: %s", e)
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
@ -8780,8 +8780,8 @@ def get_artist_discography(artist_id):
|
||||||
if not artist_info.get('genres') and lib['genres']:
|
if not artist_info.get('genres') and lib['genres']:
|
||||||
try:
|
try:
|
||||||
artist_info['genres'] = json.loads(lib['genres'])
|
artist_info['genres'] = json.loads(lib['genres'])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("genres json parse failed: %s", e)
|
||||||
# Last.fm enrichment
|
# Last.fm enrichment
|
||||||
if lib.get('lastfm_bio'):
|
if lib.get('lastfm_bio'):
|
||||||
artist_info['lastfm_bio'] = lib['lastfm_bio']
|
artist_info['lastfm_bio'] = lib['lastfm_bio']
|
||||||
|
|
@ -8792,8 +8792,8 @@ def get_artist_discography(artist_id):
|
||||||
if lib.get('lastfm_tags'):
|
if lib.get('lastfm_tags'):
|
||||||
try:
|
try:
|
||||||
artist_info['lastfm_tags'] = json.loads(lib['lastfm_tags']) if isinstance(lib['lastfm_tags'], str) else lib['lastfm_tags']
|
artist_info['lastfm_tags'] = json.loads(lib['lastfm_tags']) if isinstance(lib['lastfm_tags'], str) else lib['lastfm_tags']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("lastfm_tags json parse failed: %s", e)
|
||||||
if lib.get('lastfm_url'):
|
if lib.get('lastfm_url'):
|
||||||
artist_info['lastfm_url'] = lib['lastfm_url']
|
artist_info['lastfm_url'] = lib['lastfm_url']
|
||||||
if lib.get('genius_url'):
|
if lib.get('genius_url'):
|
||||||
|
|
@ -9475,25 +9475,25 @@ def _build_artist_quality_deps():
|
||||||
sources.append(('spotify', spotify_client))
|
sources.append(('spotify', spotify_client))
|
||||||
try:
|
try:
|
||||||
sources.append(('itunes', _get_itunes_client()))
|
sources.append(('itunes', _get_itunes_client()))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("itunes client init failed: %s", e)
|
||||||
try:
|
try:
|
||||||
sources.append(('deezer', _get_deezer_client()))
|
sources.append(('deezer', _get_deezer_client()))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("deezer client init failed: %s", e)
|
||||||
# Discogs needs an explicit token; only include when configured.
|
# Discogs needs an explicit token; only include when configured.
|
||||||
try:
|
try:
|
||||||
_discogs_token = config_manager.get('discogs.token', '')
|
_discogs_token = config_manager.get('discogs.token', '')
|
||||||
if _discogs_token:
|
if _discogs_token:
|
||||||
sources.append(('discogs', _get_discogs_client(_discogs_token)))
|
sources.append(('discogs', _get_discogs_client(_discogs_token)))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discogs client init failed: %s", e)
|
||||||
# Hydrabase only when connected (dev-mode + active client).
|
# Hydrabase only when connected (dev-mode + active client).
|
||||||
try:
|
try:
|
||||||
if hydrabase_client and hydrabase_client.is_connected():
|
if hydrabase_client and hydrabase_client.is_connected():
|
||||||
sources.append(('hydrabase', hydrabase_client))
|
sources.append(('hydrabase', hydrabase_client))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hydrabase client check failed: %s", e)
|
||||||
return sources
|
return sources
|
||||||
|
|
||||||
return _artists_quality.ArtistQualityDeps(
|
return _artists_quality.ArtistQualityDeps(
|
||||||
|
|
@ -10958,8 +10958,8 @@ def _resolve_library_file_path(file_path):
|
||||||
if plex and plex.server:
|
if plex and plex.server:
|
||||||
for loc in plex.get_music_library_locations():
|
for loc in plex.get_music_library_locations():
|
||||||
library_dirs.add(loc)
|
library_dirs.add(loc)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex library locations lookup failed: %s", e)
|
||||||
|
|
||||||
# Check user-configured music library paths (Settings > Library)
|
# Check user-configured music library paths (Settings > Library)
|
||||||
try:
|
try:
|
||||||
|
|
@ -10970,8 +10970,8 @@ def _resolve_library_file_path(file_path):
|
||||||
resolved_p = docker_resolve_path(p.strip())
|
resolved_p = docker_resolve_path(p.strip())
|
||||||
if resolved_p:
|
if resolved_p:
|
||||||
library_dirs.add(resolved_p)
|
library_dirs.add(resolved_p)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("library music paths read failed: %s", e)
|
||||||
|
|
||||||
path_parts = file_path.replace('\\', '/').split('/')
|
path_parts = file_path.replace('\\', '/').split('/')
|
||||||
|
|
||||||
|
|
@ -11462,8 +11462,8 @@ def library_delete_track(track_id):
|
||||||
try:
|
try:
|
||||||
os.remove(sidecar)
|
os.remove(sidecar)
|
||||||
logger.info(f"Deleted sidecar file: {sidecar}")
|
logger.info(f"Deleted sidecar file: {sidecar}")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("sidecar removal failed: %s", e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete file: {e}")
|
logger.warning(f"Failed to delete file: {e}")
|
||||||
file_error = str(e)
|
file_error = str(e)
|
||||||
|
|
@ -12023,8 +12023,8 @@ def library_delete_album(album_id):
|
||||||
if os.path.exists(sidecar):
|
if os.path.exists(sidecar):
|
||||||
try:
|
try:
|
||||||
os.remove(sidecar)
|
os.remove(sidecar)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("sidecar removal failed: %s", e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete track file: {e}")
|
logger.warning(f"Failed to delete track file: {e}")
|
||||||
files_failed += 1
|
files_failed += 1
|
||||||
|
|
@ -12042,8 +12042,8 @@ def library_delete_album(album_id):
|
||||||
if os.path.isdir(album_dir) and not os.listdir(album_dir):
|
if os.path.isdir(album_dir) and not os.listdir(album_dir):
|
||||||
os.rmdir(album_dir)
|
os.rmdir(album_dir)
|
||||||
logger.info(f"Removed empty album directory: {album_dir}")
|
logger.info(f"Removed empty album directory: {album_dir}")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("empty album dir cleanup failed: %s", e)
|
||||||
|
|
||||||
# Delete all tracks belonging to this album
|
# Delete all tracks belonging to this album
|
||||||
cursor.execute("DELETE FROM tracks WHERE album_id = ?", (album_id,))
|
cursor.execute("DELETE FROM tracks WHERE album_id = ?", (album_id,))
|
||||||
|
|
@ -13197,8 +13197,8 @@ def _sweep_empty_download_directories():
|
||||||
for hidden in entries:
|
for hidden in entries:
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(dirpath, hidden))
|
os.remove(os.path.join(dirpath, hidden))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hidden file cleanup failed: %s", e)
|
||||||
os.rmdir(dirpath)
|
os.rmdir(dirpath)
|
||||||
removed += 1
|
removed += 1
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|
@ -14010,8 +14010,8 @@ def _apply_path_template(template: str, context: dict) -> str:
|
||||||
resolved = _get_itunes_client().resolve_primary_artist(itunes_artist_id)
|
resolved = _get_itunes_client().resolve_primary_artist(itunes_artist_id)
|
||||||
if resolved and resolved != album_artist_value:
|
if resolved and resolved != album_artist_value:
|
||||||
album_artist_value = resolved
|
album_artist_value = resolved
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("itunes primary artist resolve failed: %s", e)
|
||||||
# $cdnum — smart CD label for multi-disc filenames. Produces "CD01" /
|
# $cdnum — smart CD label for multi-disc filenames. Produces "CD01" /
|
||||||
# "CD02" etc. when the album has 2+ discs, empty string otherwise.
|
# "CD02" etc. when the album has 2+ discs, empty string otherwise.
|
||||||
# Empty output collapses gracefully via the trailing double-dash cleanup
|
# Empty output collapses gracefully via the trailing double-dash cleanup
|
||||||
|
|
@ -14491,8 +14491,8 @@ def _get_current_commit_sha():
|
||||||
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
|
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("git rev-parse failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_current_commit_sha = _get_current_commit_sha()
|
_current_commit_sha = _get_current_commit_sha()
|
||||||
|
|
@ -14809,8 +14809,8 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
|
||||||
'total_albums': str(total_albums),
|
'total_albums': str(total_albums),
|
||||||
'total_tracks': str(total_tracks),
|
'total_tracks': str(total_tracks),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("library_updated automation emit failed: %s", e)
|
||||||
|
|
||||||
# Invalidate sync match cache (track IDs may have changed)
|
# Invalidate sync match cache (track IDs may have changed)
|
||||||
try:
|
try:
|
||||||
|
|
@ -14818,8 +14818,8 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
|
||||||
cleared = inv_db.invalidate_sync_match_cache()
|
cleared = inv_db.invalidate_sync_match_cache()
|
||||||
if cleared:
|
if cleared:
|
||||||
logger.info(f"Cleared {cleared} sync match cache entries after database update")
|
logger.info(f"Cleared {cleared} sync match cache entries after database update")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("sync match cache invalidation failed: %s", e)
|
||||||
|
|
||||||
# WISHLIST CLEANUP: Automatically clean up wishlist after database update
|
# WISHLIST CLEANUP: Automatically clean up wishlist after database update
|
||||||
try:
|
try:
|
||||||
|
|
@ -14950,8 +14950,8 @@ def _run_soulsync_full_refresh():
|
||||||
INSERT OR IGNORE INTO artists (id, name, server_source, created_at, updated_at)
|
INSERT OR IGNORE INTO artists (id, name, server_source, created_at, updated_at)
|
||||||
VALUES (?, ?, 'soulsync', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, 'soulsync', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
""", (artist_id, artist_name))
|
""", (artist_id, artist_name))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("soulsync artist insert failed: %s", e)
|
||||||
|
|
||||||
for album_name, tracks in albums.items():
|
for album_name, tracks in albums.items():
|
||||||
album_key = f"{artist_name.lower()}::{album_name.lower()}"
|
album_key = f"{artist_name.lower()}::{album_name.lower()}"
|
||||||
|
|
@ -14970,8 +14970,8 @@ def _run_soulsync_full_refresh():
|
||||||
INSERT OR IGNORE INTO albums (id, artist_id, title, year, track_count, server_source, created_at, updated_at)
|
INSERT OR IGNORE INTO albums (id, artist_id, title, year, track_count, server_source, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, 'soulsync', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, 'soulsync', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
""", (album_id, artist_id, album_name, year, len(tracks)))
|
""", (album_id, artist_id, album_name, year, len(tracks)))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("soulsync album insert failed: %s", e)
|
||||||
|
|
||||||
# Insert tracks
|
# Insert tracks
|
||||||
for file_path, tags in tracks:
|
for file_path, tags in tracks:
|
||||||
|
|
@ -15761,8 +15761,8 @@ def backup_database_endpoint():
|
||||||
try:
|
try:
|
||||||
with open(meta_path, 'w') as mf:
|
with open(meta_path, 'w') as mf:
|
||||||
json.dump({"version": SOULSYNC_VERSION, "created": timestamp}, mf)
|
json.dump({"version": SOULSYNC_VERSION, "created": timestamp}, mf)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Non-critical — backup still works without metadata
|
logger.debug("backup meta sidecar write: %s", e)
|
||||||
# Rolling cleanup
|
# Rolling cleanup
|
||||||
existing = sorted(_glob.glob(f"{db_path}.backup_*"), key=os.path.getmtime)
|
existing = sorted(_glob.glob(f"{db_path}.backup_*"), key=os.path.getmtime)
|
||||||
# Filter out .meta.json files from the backup list
|
# Filter out .meta.json files from the backup list
|
||||||
|
|
@ -15774,8 +15774,8 @@ def backup_database_endpoint():
|
||||||
# Also remove sidecar if present
|
# Also remove sidecar if present
|
||||||
if os.path.exists(removed + '.meta.json'):
|
if os.path.exists(removed + '.meta.json'):
|
||||||
os.remove(removed + '.meta.json')
|
os.remove(removed + '.meta.json')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("rolling backup cleanup failed: %s", e)
|
||||||
return jsonify({"success": True, "backup_path": backup_path, "size_mb": size_mb, "version": SOULSYNC_VERSION})
|
return jsonify({"success": True, "backup_path": backup_path, "size_mb": size_mb, "version": SOULSYNC_VERSION})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
@ -15809,8 +15809,8 @@ def list_backups_endpoint():
|
||||||
with open(meta_path, 'r') as mf:
|
with open(meta_path, 'r') as mf:
|
||||||
meta = json.load(mf)
|
meta = json.load(mf)
|
||||||
entry['version'] = meta.get('version')
|
entry['version'] = meta.get('version')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("backup metadata read failed: %s", e)
|
||||||
backups.append(entry)
|
backups.append(entry)
|
||||||
db_size_mb = round(os.path.getsize(db_path) / (1024 * 1024), 2) if os.path.exists(db_path) else 0
|
db_size_mb = round(os.path.getsize(db_path) / (1024 * 1024), 2) if os.path.exists(db_path) else 0
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
@ -15838,8 +15838,8 @@ def delete_backup_endpoint(filename):
|
||||||
if os.path.exists(meta_path):
|
if os.path.exists(meta_path):
|
||||||
try:
|
try:
|
||||||
os.remove(meta_path)
|
os.remove(meta_path)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("backup sidecar removal failed: %s", e)
|
||||||
return jsonify({"success": True, "deleted": filename})
|
return jsonify({"success": True, "deleted": filename})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
@ -15865,8 +15865,8 @@ def restore_backup_endpoint(filename):
|
||||||
with open(meta_path, 'r') as mf:
|
with open(meta_path, 'r') as mf:
|
||||||
meta = json.load(mf)
|
meta = json.load(mf)
|
||||||
backup_version = meta.get('version')
|
backup_version = meta.get('version')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("backup version metadata read failed: %s", e)
|
||||||
|
|
||||||
version_warning = None
|
version_warning = None
|
||||||
# Compare base versions only (strip +commit suffix) to avoid false mismatches
|
# Compare base versions only (strip +commit suffix) to avoid false mismatches
|
||||||
|
|
@ -15898,8 +15898,8 @@ def restore_backup_endpoint(filename):
|
||||||
try:
|
try:
|
||||||
with open(safety_path + '.meta.json', 'w') as mf:
|
with open(safety_path + '.meta.json', 'w') as mf:
|
||||||
json.dump({"version": SOULSYNC_VERSION, "created": safety_ts}, mf)
|
json.dump({"version": SOULSYNC_VERSION, "created": safety_ts}, mf)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("safety backup metadata write failed: %s", e)
|
||||||
|
|
||||||
# Restore using SQLite backup API (handles concurrent access safely)
|
# Restore using SQLite backup API (handles concurrent access safely)
|
||||||
from database.music_database import close_database, get_database
|
from database.music_database import close_database, get_database
|
||||||
|
|
@ -18140,13 +18140,13 @@ def get_server_playlist_tracks(playlist_id):
|
||||||
raw_playlist = None
|
raw_playlist = None
|
||||||
try:
|
try:
|
||||||
raw_playlist = media_server_engine.client('plex').server.fetchItem(int(playlist_id))
|
raw_playlist = media_server_engine.client('plex').server.fetchItem(int(playlist_id))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist fetchItem failed: %s", e)
|
||||||
if not raw_playlist and playlist_name:
|
if not raw_playlist and playlist_name:
|
||||||
try:
|
try:
|
||||||
raw_playlist = media_server_engine.client('plex').server.playlist(playlist_name)
|
raw_playlist = media_server_engine.client('plex').server.playlist(playlist_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist by-name lookup failed: %s", e)
|
||||||
if not raw_playlist:
|
if not raw_playlist:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[ServerPlaylistTracks] Plex playlist not found by "
|
f"[ServerPlaylistTracks] Plex playlist not found by "
|
||||||
|
|
@ -18417,13 +18417,13 @@ def server_playlist_replace_track(playlist_id):
|
||||||
raw_playlist = None
|
raw_playlist = None
|
||||||
try:
|
try:
|
||||||
raw_playlist = plex_server.fetchItem(int(playlist_id))
|
raw_playlist = plex_server.fetchItem(int(playlist_id))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist fetchItem failed: %s", e)
|
||||||
if not raw_playlist and playlist_name:
|
if not raw_playlist and playlist_name:
|
||||||
try:
|
try:
|
||||||
raw_playlist = plex_server.playlist(playlist_name)
|
raw_playlist = plex_server.playlist(playlist_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist by-name lookup failed: %s", e)
|
||||||
if not raw_playlist:
|
if not raw_playlist:
|
||||||
logger.warning(f"[ServerPlaylist] replace-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
|
logger.warning(f"[ServerPlaylist] replace-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
|
||||||
return jsonify({"success": False, "error": "Playlist not found on server"}), 404
|
return jsonify({"success": False, "error": "Playlist not found on server"}), 404
|
||||||
|
|
@ -18517,13 +18517,13 @@ def server_playlist_add_track(playlist_id):
|
||||||
raw_playlist = None
|
raw_playlist = None
|
||||||
try:
|
try:
|
||||||
raw_playlist = plex_server.fetchItem(int(playlist_id))
|
raw_playlist = plex_server.fetchItem(int(playlist_id))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist fetchItem failed: %s", e)
|
||||||
if not raw_playlist and playlist_name:
|
if not raw_playlist and playlist_name:
|
||||||
try:
|
try:
|
||||||
raw_playlist = plex_server.playlist(playlist_name)
|
raw_playlist = plex_server.playlist(playlist_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist by-name lookup failed: %s", e)
|
||||||
if not raw_playlist:
|
if not raw_playlist:
|
||||||
logger.warning(f"[ServerPlaylist] add-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
|
logger.warning(f"[ServerPlaylist] add-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
|
||||||
return jsonify({"success": False, "error": "Playlist not found"}), 404
|
return jsonify({"success": False, "error": "Playlist not found"}), 404
|
||||||
|
|
@ -18601,13 +18601,13 @@ def server_playlist_remove_track(playlist_id):
|
||||||
raw_playlist = None
|
raw_playlist = None
|
||||||
try:
|
try:
|
||||||
raw_playlist = plex_server.fetchItem(int(playlist_id))
|
raw_playlist = plex_server.fetchItem(int(playlist_id))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist fetchItem failed: %s", e)
|
||||||
if not raw_playlist and playlist_name:
|
if not raw_playlist and playlist_name:
|
||||||
try:
|
try:
|
||||||
raw_playlist = plex_server.playlist(playlist_name)
|
raw_playlist = plex_server.playlist(playlist_name)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("plex playlist by-name lookup failed: %s", e)
|
||||||
if not raw_playlist:
|
if not raw_playlist:
|
||||||
logger.warning(f"[ServerPlaylist] remove-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
|
logger.warning(f"[ServerPlaylist] remove-track: playlist not found by id={playlist_id} or name='{playlist_name}'")
|
||||||
return jsonify({"success": False, "error": "Playlist not found"}), 404
|
return jsonify({"success": False, "error": "Playlist not found"}), 404
|
||||||
|
|
@ -20747,8 +20747,8 @@ def _pause_enrichment_workers(label='discovery'):
|
||||||
worker.pause()
|
worker.pause()
|
||||||
was_running[name] = True
|
was_running[name] = True
|
||||||
logger.warning(f"Paused {name} enrichment worker during {label}")
|
logger.warning(f"Paused {name} enrichment worker during {label}")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("enrichment worker pause failed: %s", e)
|
||||||
return was_running
|
return was_running
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20765,8 +20765,8 @@ def _resume_enrichment_workers(was_running, label='discovery'):
|
||||||
if was_running.get(name) and worker:
|
if was_running.get(name) and worker:
|
||||||
worker.resume()
|
worker.resume()
|
||||||
logger.info(f"Resumed {name} enrichment worker after {label}")
|
logger.info(f"Resumed {name} enrichment worker after {label}")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("enrichment worker resume failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discovery_results, discovery_source, profile_id=1):
|
def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discovery_results, discovery_source, profile_id=1):
|
||||||
|
|
@ -24764,8 +24764,8 @@ def add_to_watchlist():
|
||||||
elif row['discogs_id']:
|
elif row['discogs_id']:
|
||||||
artist_id = row['discogs_id']
|
artist_id = row['discogs_id']
|
||||||
source = 'discogs'
|
source = 'discogs'
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist artist source lookup failed: %s", e)
|
||||||
if not source:
|
if not source:
|
||||||
fallback_source = _get_metadata_fallback_source()
|
fallback_source = _get_metadata_fallback_source()
|
||||||
source = fallback_source if is_numeric_id else 'spotify'
|
source = fallback_source if is_numeric_id else 'spotify'
|
||||||
|
|
@ -24845,16 +24845,16 @@ def add_to_watchlist():
|
||||||
try:
|
try:
|
||||||
pid = get_current_profile_id()
|
pid = get_current_profile_id()
|
||||||
socketio.emit('watchlist:count', _build_watchlist_count_payload(profile_id=pid), room=f'profile:{pid}')
|
socketio.emit('watchlist:count', _build_watchlist_count_payload(profile_id=pid), room=f'profile:{pid}')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist count emit failed: %s", e)
|
||||||
try:
|
try:
|
||||||
if automation_engine:
|
if automation_engine:
|
||||||
automation_engine.emit('watchlist_artist_added', {
|
automation_engine.emit('watchlist_artist_added', {
|
||||||
'artist': artist_name,
|
'artist': artist_name,
|
||||||
'artist_id': str(artist_id),
|
'artist_id': str(artist_id),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist_artist_added emit failed: %s", e)
|
||||||
_artmap_cache_invalidate(get_current_profile_id())
|
_artmap_cache_invalidate(get_current_profile_id())
|
||||||
return jsonify({"success": True, "message": f"Added {artist_name} to watchlist"})
|
return jsonify({"success": True, "message": f"Added {artist_name} to watchlist"})
|
||||||
else:
|
else:
|
||||||
|
|
@ -24882,16 +24882,16 @@ def remove_from_watchlist():
|
||||||
try:
|
try:
|
||||||
pid = get_current_profile_id()
|
pid = get_current_profile_id()
|
||||||
socketio.emit('watchlist:count', _build_watchlist_count_payload(profile_id=pid), room=f'profile:{pid}')
|
socketio.emit('watchlist:count', _build_watchlist_count_payload(profile_id=pid), room=f'profile:{pid}')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist count emit failed: %s", e)
|
||||||
try:
|
try:
|
||||||
if automation_engine:
|
if automation_engine:
|
||||||
automation_engine.emit('watchlist_artist_removed', {
|
automation_engine.emit('watchlist_artist_removed', {
|
||||||
'artist': data.get('artist_name', str(artist_id)),
|
'artist': data.get('artist_name', str(artist_id)),
|
||||||
'artist_id': str(artist_id),
|
'artist_id': str(artist_id),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist_artist_removed emit failed: %s", e)
|
||||||
_artmap_cache_invalidate(get_current_profile_id())
|
_artmap_cache_invalidate(get_current_profile_id())
|
||||||
return jsonify({"success": True, "message": "Removed artist from watchlist"})
|
return jsonify({"success": True, "message": "Removed artist from watchlist"})
|
||||||
else:
|
else:
|
||||||
|
|
@ -25033,8 +25033,8 @@ def watchlist_all_unwatched_library_artists():
|
||||||
if artist.get('image_url'):
|
if artist.get('image_url'):
|
||||||
try:
|
try:
|
||||||
database.update_watchlist_artist_image(artist_id, artist['image_url'])
|
database.update_watchlist_artist_image(artist_id, artist['image_url'])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("watchlist artist image update failed: %s", e)
|
||||||
|
|
||||||
total_unwatched = len(unwatched_artists)
|
total_unwatched = len(unwatched_artists)
|
||||||
message_parts = [f"Added {added} artist{'s' if added != 1 else ''} to watchlist"]
|
message_parts = [f"Added {added} artist{'s' if added != 1 else ''} to watchlist"]
|
||||||
|
|
@ -25193,8 +25193,8 @@ def start_watchlist_scan():
|
||||||
try:
|
try:
|
||||||
if config_manager.get('discogs.token', ''):
|
if config_manager.get('discogs.token', ''):
|
||||||
providers_to_backfill.append('discogs')
|
providers_to_backfill.append('discogs')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discogs token backfill check failed: %s", e)
|
||||||
for _bf_provider in providers_to_backfill:
|
for _bf_provider in providers_to_backfill:
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Checking for missing {_bf_provider} IDs in watchlist...")
|
logger.debug(f"Checking for missing {_bf_provider} IDs in watchlist...")
|
||||||
|
|
@ -25357,8 +25357,8 @@ def start_watchlist_scan():
|
||||||
# Clear one-time rescan cutoff after full scan cycle
|
# Clear one-time rescan cutoff after full scan cycle
|
||||||
try:
|
try:
|
||||||
scanner._clear_rescan_cutoff()
|
scanner._clear_rescan_cutoff()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("scanner rescan cutoff clear failed: %s", e)
|
||||||
|
|
||||||
# Always reset flag when scan completes (success or error)
|
# Always reset flag when scan completes (success or error)
|
||||||
with watchlist_timer_lock:
|
with watchlist_timer_lock:
|
||||||
|
|
@ -26195,8 +26195,8 @@ def enrich_similar_artists():
|
||||||
aid, fallback_source,
|
aid, fallback_source,
|
||||||
image_url=img_url, genres=genres, popularity=0
|
image_url=img_url, genres=genres, popularity=0
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("similar artist enrichment failed: %s", e)
|
||||||
|
|
||||||
cached_count = len(enriched) - len([aid for aid in uncached_ids if aid in enriched])
|
cached_count = len(enriched) - len([aid for aid in uncached_ids if aid in enriched])
|
||||||
api_count = len([aid for aid in uncached_ids if aid in enriched])
|
api_count = len([aid for aid in uncached_ids if aid in enriched])
|
||||||
|
|
@ -26363,10 +26363,10 @@ def get_discover_recent_releases():
|
||||||
if album_id:
|
if album_id:
|
||||||
try:
|
try:
|
||||||
database.update_discovery_recent_album_cover(album_id, cover)
|
database.update_discovery_recent_album_cover(album_id, cover)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("recent album cover update failed: %s", e)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("recent album cover fetch failed: %s", e)
|
||||||
|
|
||||||
# Filter out blacklisted artists
|
# Filter out blacklisted artists
|
||||||
blacklisted = database.get_discovery_blacklist_names()
|
blacklisted = database.get_discovery_blacklist_names()
|
||||||
|
|
@ -26500,8 +26500,8 @@ def get_discover_because_you_listen_to():
|
||||||
if row and row[0]:
|
if row and row[0]:
|
||||||
artist_image = fix_artist_image_url(row[0])
|
artist_image = fix_artist_image_url(row[0])
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("artist image lookup failed: %s", e)
|
||||||
|
|
||||||
sections.append({
|
sections.append({
|
||||||
'artist_name': artist_name,
|
'artist_name': artist_name,
|
||||||
|
|
@ -26686,8 +26686,8 @@ def resolve_cache_album():
|
||||||
if results:
|
if results:
|
||||||
r = results[0]
|
r = results[0]
|
||||||
return jsonify({'success': True, 'entity_id': str(r.id), 'source': _get_metadata_fallback_source()})
|
return jsonify({'success': True, 'entity_id': str(r.id), 'source': _get_metadata_fallback_source()})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("fallback album search failed: %s", e)
|
||||||
|
|
||||||
return jsonify({'success': False, 'error': 'Album not found in cache'})
|
return jsonify({'success': False, 'error': 'Album not found in cache'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -27002,8 +27002,8 @@ def get_seasonal_playlist(season_key):
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("track_data_json parse: %s", e)
|
||||||
tracks.append(track_dict)
|
tracks.append(track_dict)
|
||||||
else:
|
else:
|
||||||
# Try discovery_pool as fallback (filtered by source)
|
# Try discovery_pool as fallback (filtered by source)
|
||||||
|
|
@ -27029,8 +27029,8 @@ def get_seasonal_playlist(season_key):
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discovery track_data_json parse: %s", e)
|
||||||
tracks.append(track_dict)
|
tracks.append(track_dict)
|
||||||
|
|
||||||
config = SEASONAL_CONFIG[season_key]
|
config = SEASONAL_CONFIG[season_key]
|
||||||
|
|
@ -27435,8 +27435,8 @@ def get_your_artists_sources():
|
||||||
try:
|
try:
|
||||||
if tidal_client and hasattr(tidal_client, '_ensure_valid_token') and tidal_client._ensure_valid_token():
|
if tidal_client and hasattr(tidal_client, '_ensure_valid_token') and tidal_client._ensure_valid_token():
|
||||||
connected.append('tidal')
|
connected.append('tidal')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("tidal auth check failed: %s", e)
|
||||||
# Last.fm
|
# Last.fm
|
||||||
if config_manager.get('lastfm.api_key', '') and config_manager.get('lastfm.session_key', ''):
|
if config_manager.get('lastfm.api_key', '') and config_manager.get('lastfm.session_key', ''):
|
||||||
connected.append('lastfm')
|
connected.append('lastfm')
|
||||||
|
|
@ -27448,8 +27448,8 @@ def get_your_artists_sources():
|
||||||
and download_orchestrator.client("deezer_dl").is_authenticated())
|
and download_orchestrator.client("deezer_dl").is_authenticated())
|
||||||
if deezer_oauth or deezer_arl:
|
if deezer_oauth or deezer_arl:
|
||||||
connected.append('deezer')
|
connected.append('deezer')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("deezer auth check failed: %s", e)
|
||||||
|
|
||||||
return jsonify({"success": True, "enabled": enabled, "connected": connected})
|
return jsonify({"success": True, "enabled": enabled, "connected": connected})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -27711,8 +27711,8 @@ def get_your_albums_sources():
|
||||||
try:
|
try:
|
||||||
if tidal_client and hasattr(tidal_client, '_ensure_valid_token') and tidal_client._ensure_valid_token():
|
if tidal_client and hasattr(tidal_client, '_ensure_valid_token') and tidal_client._ensure_valid_token():
|
||||||
connected.append('tidal')
|
connected.append('tidal')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("tidal auth check failed: %s", e)
|
||||||
try:
|
try:
|
||||||
deezer_cl = _get_deezer_client()
|
deezer_cl = _get_deezer_client()
|
||||||
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
|
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
|
||||||
|
|
@ -27720,8 +27720,8 @@ def get_your_albums_sources():
|
||||||
and download_orchestrator.client("deezer_dl").is_authenticated())
|
and download_orchestrator.client("deezer_dl").is_authenticated())
|
||||||
if deezer_oauth or deezer_arl:
|
if deezer_oauth or deezer_arl:
|
||||||
connected.append('deezer')
|
connected.append('deezer')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("deezer auth check failed: %s", e)
|
||||||
|
|
||||||
# Discogs: counts as "connected" when a personal access token is
|
# Discogs: counts as "connected" when a personal access token is
|
||||||
# configured. Username comes from /oauth/identity at fetch time;
|
# configured. Username comes from /oauth/identity at fetch time;
|
||||||
|
|
@ -27729,8 +27729,8 @@ def get_your_albums_sources():
|
||||||
try:
|
try:
|
||||||
if config_manager.get('discogs.token', ''):
|
if config_manager.get('discogs.token', ''):
|
||||||
connected.append('discogs')
|
connected.append('discogs')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discogs token check failed: %s", e)
|
||||||
|
|
||||||
return jsonify({"success": True, "enabled": enabled, "connected": connected})
|
return jsonify({"success": True, "enabled": enabled, "connected": connected})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -27913,8 +27913,8 @@ def get_your_artist_info(artist_id):
|
||||||
'lastfm_playcount': r.get('lastfm_playcount', 0),
|
'lastfm_playcount': r.get('lastfm_playcount', 0),
|
||||||
})
|
})
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("library artist lookup failed: %s", e)
|
||||||
|
|
||||||
# 2. Try metadata cache
|
# 2. Try metadata cache
|
||||||
try:
|
try:
|
||||||
|
|
@ -27935,8 +27935,8 @@ def get_your_artist_info(artist_id):
|
||||||
'followers': cached.get('followers', {}).get('total', 0) if isinstance(cached.get('followers'), dict) else cached.get('followers', 0),
|
'followers': cached.get('followers', {}).get('total', 0) if isinstance(cached.get('followers'), dict) else cached.get('followers', 0),
|
||||||
})
|
})
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("metadata cache lookup failed: %s", e)
|
||||||
|
|
||||||
# 3. Try Spotify API directly (genres, image, followers)
|
# 3. Try Spotify API directly (genres, image, followers)
|
||||||
try:
|
try:
|
||||||
|
|
@ -31402,8 +31402,8 @@ def mirror_playlist_endpoint():
|
||||||
'source': source,
|
'source': source,
|
||||||
'track_count': str(len(tracks)),
|
'track_count': str(len(tracks)),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("mirrored_playlist_created emit failed: %s", e)
|
||||||
|
|
||||||
return jsonify({"success": True, "playlist_id": playlist_id})
|
return jsonify({"success": True, "playlist_id": playlist_id})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -31577,8 +31577,8 @@ def fix_discovery_pool_track():
|
||||||
cache_key[0], cache_key[1], _get_active_discovery_source(), 1.0, matched_data,
|
cache_key[0], cache_key[1], _get_active_discovery_source(), 1.0, matched_data,
|
||||||
row['track_name'], row['artist_name']
|
row['track_name'], row['artist_name']
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discovery cache match save failed: %s", e)
|
||||||
|
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -32002,8 +32002,8 @@ def playlist_explorer_album_tracks(album_id):
|
||||||
if cache and tracks:
|
if cache and tracks:
|
||||||
try:
|
try:
|
||||||
cache.store_entity(source_name, 'album_tracks', cache_key, result)
|
cache.store_entity(source_name, 'album_tracks', cache_key, result)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album_tracks cache store failed: %s", e)
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -32212,8 +32212,8 @@ def start_oauth_callback_servers():
|
||||||
self.send_header('Content-type', 'text/html')
|
self.send_header('Content-type', 'text/html')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(f'<h1>Internal Server Error</h1><p>{str(e)}</p>'.encode())
|
self.wfile.write(f'<h1>Internal Server Error</h1><p>{str(e)}</p>'.encode())
|
||||||
except Exception:
|
except Exception as _e:
|
||||||
pass # Connection already broken, nothing more we can do
|
_oauth_logger.debug("oauth response write: %s", _e)
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
pass # Suppress BaseHTTPRequestHandler access logs (we use our own logger)
|
pass # Suppress BaseHTTPRequestHandler access logs (we use our own logger)
|
||||||
|
|
@ -33153,8 +33153,8 @@ try:
|
||||||
state['log'].append({'type': 'success' if status == 'finished' else 'error', 'text': summary})
|
state['log'].append({'type': 'success' if status == 'finished' else 'error', 'text': summary})
|
||||||
try:
|
try:
|
||||||
socketio.emit('repair:progress', {job_id: dict(state)})
|
socketio.emit('repair:progress', {job_id: dict(state)})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("repair progress emit failed: %s", e)
|
||||||
|
|
||||||
repair_worker.register_progress_callbacks(_repair_job_start, _repair_job_progress, _repair_job_finish)
|
repair_worker.register_progress_callbacks(_repair_job_start, _repair_job_progress, _repair_job_finish)
|
||||||
# Store refs for WebSocket push loop
|
# Store refs for WebSocket push loop
|
||||||
|
|
@ -33639,8 +33639,8 @@ def import_staging_hints():
|
||||||
if album:
|
if album:
|
||||||
key = (album.strip(), (artist or '').strip())
|
key = (album.strip(), (artist or '').strip())
|
||||||
tag_albums[key] = tag_albums.get(key, 0) + 1
|
tag_albums[key] = tag_albums.get(key, 0) + 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("tag read failed: %s", e)
|
||||||
|
|
||||||
# Build search queries, prioritizing tag-based hints (more specific)
|
# Build search queries, prioritizing tag-based hints (more specific)
|
||||||
queries = []
|
queries = []
|
||||||
|
|
@ -33796,8 +33796,8 @@ def import_album_process():
|
||||||
'completed_tracks': str(processed),
|
'completed_tracks': str(processed),
|
||||||
'failed_tracks': str(len(errors)),
|
'failed_tracks': str(len(errors)),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("album import automation emit failed: %s", e)
|
||||||
|
|
||||||
# Rebuild suggestions cache since staging contents changed
|
# Rebuild suggestions cache since staging contents changed
|
||||||
if processed > 0:
|
if processed > 0:
|
||||||
|
|
@ -33960,8 +33960,8 @@ def import_singles_process():
|
||||||
'completed_tracks': str(processed),
|
'completed_tracks': str(processed),
|
||||||
'failed_tracks': str(len(errors)),
|
'failed_tracks': str(len(errors)),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("singles import automation emit failed: %s", e)
|
||||||
|
|
||||||
# Rebuild suggestions cache since staging contents changed
|
# Rebuild suggestions cache since staging contents changed
|
||||||
if processed > 0:
|
if processed > 0:
|
||||||
|
|
@ -34147,8 +34147,8 @@ def _build_status_payload():
|
||||||
for t in download_tasks.values():
|
for t in download_tasks.values():
|
||||||
if t.get('status') in ('downloading', 'searching', 'post_processing', 'queued', 'pending'):
|
if t.get('status') in ('downloading', 'searching', 'post_processing', 'queued', 'pending'):
|
||||||
active_dl_count += 1
|
active_dl_count += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("active download count failed: %s", e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'metadata_source': metadata_status['metadata_source'],
|
'metadata_source': metadata_status['metadata_source'],
|
||||||
|
|
@ -34193,8 +34193,8 @@ def _hydrabase_reconnect_loop():
|
||||||
if _hydrabase_ws is not None and _hydrabase_ws.connected:
|
if _hydrabase_ws is not None and _hydrabase_ws.connected:
|
||||||
_consecutive_failures = 0
|
_consecutive_failures = 0
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Socket in bad state — treat as disconnected
|
logger.debug("hydrabase socket check: %s", e)
|
||||||
|
|
||||||
# Disconnected with auto_connect enabled — try to reconnect
|
# Disconnected with auto_connect enabled — try to reconnect
|
||||||
# Back off: 30s, 60s, 120s, max 300s between attempts
|
# Back off: 30s, 60s, 120s, max 300s between attempts
|
||||||
|
|
@ -34208,8 +34208,8 @@ def _hydrabase_reconnect_loop():
|
||||||
if _hydrabase_ws:
|
if _hydrabase_ws:
|
||||||
try:
|
try:
|
||||||
_hydrabase_ws.close()
|
_hydrabase_ws.close()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug("hydrabase reconnect close: %s", e)
|
||||||
ws = websocket.create_connection(
|
ws = websocket.create_connection(
|
||||||
hydra_cfg['url'],
|
hydra_cfg['url'],
|
||||||
header={"x-api-key": hydra_cfg['api_key']},
|
header={"x-api-key": hydra_cfg['api_key']},
|
||||||
|
|
@ -34224,8 +34224,8 @@ def _hydrabase_reconnect_loop():
|
||||||
logger.error(f"[Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}")
|
logger.error(f"[Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}")
|
||||||
elif _consecutive_failures == 4:
|
elif _consecutive_failures == 4:
|
||||||
logger.error("[Hydrabase] Reconnect failing repeatedly — suppressing further logs until success")
|
logger.error("[Hydrabase] Reconnect failing repeatedly — suppressing further logs until success")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass # Don't crash the monitor loop
|
logger.debug("hydrabase monitor loop: %s", e)
|
||||||
|
|
||||||
def _emit_service_status_loop():
|
def _emit_service_status_loop():
|
||||||
"""Background thread that pushes service status every 5 seconds."""
|
"""Background thread that pushes service status every 5 seconds."""
|
||||||
|
|
@ -34368,8 +34368,8 @@ def _has_active_downloads():
|
||||||
for batch_data in download_batches.values():
|
for batch_data in download_batches.values():
|
||||||
if batch_data.get('phase') not in ('complete', 'error', 'cancelled', None):
|
if batch_data.get('phase') not in ('complete', 'error', 'cancelled', None):
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("active downloads check failed: %s", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34403,8 +34403,8 @@ def _spotify_resume_pre_check():
|
||||||
try:
|
try:
|
||||||
if _spotify_rate_limited():
|
if _spotify_rate_limited():
|
||||||
return (429, 'Cannot resume while Spotify is rate limited')
|
return (429, 'Cannot resume while Spotify is rate limited')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify rate-limit pre-check failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34511,8 +34511,8 @@ def _emit_rate_monitor_loop():
|
||||||
}
|
}
|
||||||
if svc_key == 'spotify' and enr.get('daily_budget'):
|
if svc_key == 'spotify' and enr.get('daily_budget'):
|
||||||
entry['worker']['daily_budget'] = enr['daily_budget']
|
entry['worker']['daily_budget'] = enr['daily_budget']
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("enrichment worker status build failed: %s", e)
|
||||||
|
|
||||||
# Add Spotify rate limit state
|
# Add Spotify rate limit state
|
||||||
try:
|
try:
|
||||||
|
|
@ -34522,8 +34522,8 @@ def _emit_rate_monitor_loop():
|
||||||
payload['spotify']['rate_limited'] = True
|
payload['spotify']['rate_limited'] = True
|
||||||
payload['spotify']['rl_remaining'] = rl_info.get('remaining_seconds', 0)
|
payload['spotify']['rl_remaining'] = rl_info.get('remaining_seconds', 0)
|
||||||
payload['spotify']['rl_endpoint'] = rl_info.get('endpoint', '')
|
payload['spotify']['rl_endpoint'] = rl_info.get('endpoint', '')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("spotify rate-limit status read failed: %s", e)
|
||||||
|
|
||||||
socketio.emit('rate-monitor:update', payload)
|
socketio.emit('rate-monitor:update', payload)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -34763,8 +34763,8 @@ def _emit_sync_progress_loop():
|
||||||
socketio.emit('sync:progress', {
|
socketio.emit('sync:progress', {
|
||||||
'playlist_id': pid, **state
|
'playlist_id': pid, **state
|
||||||
}, room=f'sync:{pid}')
|
}, room=f'sync:{pid}')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("sync progress emit failed: %s", e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error in sync progress loop: {e}")
|
logger.debug(f"Error in sync progress loop: {e}")
|
||||||
|
|
||||||
|
|
@ -34801,8 +34801,8 @@ def _emit_discovery_progress_loop():
|
||||||
'complete': state.get('phase') == 'discovered',
|
'complete': state.get('phase') == 'discovered',
|
||||||
}
|
}
|
||||||
socketio.emit('discovery:progress', payload, room=f'discovery:{pid}')
|
socketio.emit('discovery:progress', payload, room=f'discovery:{pid}')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("discovery progress emit failed: %s", e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error in {platform_name} discovery loop: {e}")
|
logger.debug(f"Error in {platform_name} discovery loop: {e}")
|
||||||
|
|
||||||
|
|
@ -34945,8 +34945,8 @@ def start_runtime_services():
|
||||||
logger.info("Automation engine started")
|
logger.info("Automation engine started")
|
||||||
try:
|
try:
|
||||||
automation_engine.emit('app_started', {})
|
automation_engine.emit('app_started', {})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug("app_started emit failed: %s", e)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.error(f"Automation engine failed to start: {e}")
|
logger.error(f"Automation engine failed to start: {e}")
|
||||||
logger.info(" If using Docker, check that your volume mount is /app/data (not /app/database)")
|
logger.info(" If using Docker, check that your volume mount is /app/data (not /app/database)")
|
||||||
|
|
|
||||||
|
|
@ -3432,6 +3432,7 @@ const WHATS_NEW = {
|
||||||
'2.4.2': [
|
'2.4.2': [
|
||||||
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
|
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
|
||||||
{ date: 'Unreleased — 2.4.2 dev cycle' },
|
{ date: 'Unreleased — 2.4.2 dev cycle' },
|
||||||
|
{ title: 'Internal: Stop Swallowing Exceptions Silently', desc: 'github issue #369 (johnbaumb): the codebase had ~300 `except Exception: pass` blocks — and another ~30 bare `except: pass` ones — across web_server.py, every metadata client, every download/import worker, the repair jobs, and most service modules. when one of those paths failed at runtime, the failure was completely invisible: no log line, no telemetry, nothing. you\'d see "downloads stopped working after a few hours" or "enrichment never finishes" and there was nothing to grep for in app.log because the exception had been thrown straight into the void. swept all of them. converted to `except Exception as e: logger.debug("<context>: %s", e)` so failures land in the log with enough context to grep. bare `except:` cases (which also swallow KeyboardInterrupt and SystemExit — actively wrong) got upgraded to `except Exception:` first so ctrl-c works correctly. ~14 cleanup-path sites (atexit handlers, finally-block conn.close calls) were intentionally left silent with explicit `# noqa: S110` comments — logging during shutdown can itself crash because file handles get torn down before the handler fires. and added ruff S110 to the lint config so this pattern fails CI going forward — drift fails at PR review instead of at runtime against a wedged worker thread. zero behavior change to any happy path; just made the failure paths inspectable. test suite (2188 tests) green throughout the sweep.' },
|
||||||
{ title: 'Fix: Repair Job Card "X Findings" Badge Was Misleading After Bulk-Fix', desc: 'discord report: duplicate detector card said "372 findings" and cover art filler said "60 findings", but clicking the findings tab pending filter showed 0 — read like a bug ("findings aren\'t being created"). actual cause: job-card badge displayed `last_run.findings_created` (historical "found in last scan") which doesn\'t reflect current state when those findings have since been bulk-fixed and moved to status="resolved". fix: api response now includes `pending_findings_count` per job (current pending count from a single sql aggregation). badge now shows "X pending" when pending count > 0 (urgent red color), or "X found in last scan" with a muted grey color when pending = 0 but the last scan did surface something. user can tell at a glance whether something needs review vs whether it\'s a historical reminder. 3 new tests pin the per-job pending count helper.', page: 'stats' },
|
{ title: 'Fix: Repair Job Card "X Findings" Badge Was Misleading After Bulk-Fix', desc: 'discord report: duplicate detector card said "372 findings" and cover art filler said "60 findings", but clicking the findings tab pending filter showed 0 — read like a bug ("findings aren\'t being created"). actual cause: job-card badge displayed `last_run.findings_created` (historical "found in last scan") which doesn\'t reflect current state when those findings have since been bulk-fixed and moved to status="resolved". fix: api response now includes `pending_findings_count` per job (current pending count from a single sql aggregation). badge now shows "X pending" when pending count > 0 (urgent red color), or "X found in last scan" with a muted grey color when pending = 0 but the last scan did surface something. user can tell at a glance whether something needs review vs whether it\'s a historical reminder. 3 new tests pin the per-job pending count helper.', page: 'stats' },
|
||||||
{ title: 'Fix: Downloads Stop After 2-3 Hours (slskd HTTP Timeout)', desc: 'github issue #499 (bafoed): big initial sync of spotify playlists worked for 2-3 hours then downloads silently stopped. 3 active tasks stuck in "searching" state, replaced every ~10 min by different ones, but slskd ui showed no actual searches happening. only fix was restarting the soulsync container — which would buy another 2-3 hours. root cause: `core/soulseek_client.py` constructed `aiohttp.ClientSession()` with no timeout at four sites. when slskd hung on a request (overloaded, transient network blip, internal stall), the http call blocked indefinitely — and the worker thread blocked with it. download executor only has `max_workers=3` for download workers. once 3 worker threads were wedged on hung calls, no further downloads could start. batch-level "stuck detection" (10-min) was correctly marking tasks `not_found` and trying to start replacements, but the executor pool was exhausted — replacements queued forever. fix: bounded `aiohttp.ClientTimeout` (total 120s, connect 15s, sock_read 60s) on every slskd `ClientSession` construction. legitimate metadata calls (search submission, status polls, download enqueue) finish in seconds — slskd doesn\'t stream files through these requests, so the timeout can\'t kill a real operation. when timeout fires, the request raises `asyncio.TimeoutError` which is now explicitly caught + logged + returns None to the caller (treats as a normal failure, same code path as a 5xx response). worker thread unblocks → executor pool stays healthy → downloads keep flowing. 3 new tests pin the timeout config + the `asyncio.TimeoutError` handler so future drift fails at the test boundary instead of at runtime against a wedged executor.', page: 'downloads' },
|
{ title: 'Fix: Downloads Stop After 2-3 Hours (slskd HTTP Timeout)', desc: 'github issue #499 (bafoed): big initial sync of spotify playlists worked for 2-3 hours then downloads silently stopped. 3 active tasks stuck in "searching" state, replaced every ~10 min by different ones, but slskd ui showed no actual searches happening. only fix was restarting the soulsync container — which would buy another 2-3 hours. root cause: `core/soulseek_client.py` constructed `aiohttp.ClientSession()` with no timeout at four sites. when slskd hung on a request (overloaded, transient network blip, internal stall), the http call blocked indefinitely — and the worker thread blocked with it. download executor only has `max_workers=3` for download workers. once 3 worker threads were wedged on hung calls, no further downloads could start. batch-level "stuck detection" (10-min) was correctly marking tasks `not_found` and trying to start replacements, but the executor pool was exhausted — replacements queued forever. fix: bounded `aiohttp.ClientTimeout` (total 120s, connect 15s, sock_read 60s) on every slskd `ClientSession` construction. legitimate metadata calls (search submission, status polls, download enqueue) finish in seconds — slskd doesn\'t stream files through these requests, so the timeout can\'t kill a real operation. when timeout fires, the request raises `asyncio.TimeoutError` which is now explicitly caught + logged + returns None to the caller (treats as a normal failure, same code path as a 5xx response). worker thread unblocks → executor pool stays healthy → downloads keep flowing. 3 new tests pin the timeout config + the `asyncio.TimeoutError` handler so future drift fails at the test boundary instead of at runtime against a wedged executor.', page: 'downloads' },
|
||||||
{ title: 'Fix: Library Reorganize Job Misclassified Album Tracks As Singles', desc: 'github issue #500 (bafoed): library reorganize repair job moved tracks like `Surf Curse/Surf Curse - Nothing Yet (2017)/01 - Christine F.flac` to single-template paths like `Surf Curse/Surf Curse - Christine F/Surf Curse - Christine F.flac`. root cause: the job used `is_album = (group_size > 1)` where `group_size` was the count of tracks for the same album currently sitting in the transfer folder being scanned — when only one track of an album was in transfer (rest already moved to library, or album tags varied across tracks like "Buds" vs "Buds (Bonus)"), each track became a 1-element group → all routed through single template. fix: rewrote the job to delegate to the per-album planner (`core.library_reorganize.preview_album_reorganize` / `reorganize_queue`) — the same planner the artist-detail "reorganize" modal uses. db-driven: the planner knows the album has multiple tracks regardless of how many sit in the transfer folder, so the album-vs-single classification is structurally correct. apply mode delegates to the existing reorganize queue → file move + post-processing + db update + sidecar handling all flow through one code path. only iterates albums for the ACTIVE media server (matches the artist-detail modal\'s scope) — multi-server users (plex + jellyfin etc) won\'t accidentally have the job touch the inactive server\'s files. albums missing a metadata source id get a single "needs enrichment first" finding instead of n per-track "no source" findings. dropped ~500 loc of tag-reading + transfer-walk + template logic that was duplicated against the per-album path. files in transfer with no db entry are now exclusively the orphan_file_detector\'s domain (clean separation). 12 tests pin the delegation contract.', page: 'library' },
|
{ title: 'Fix: Library Reorganize Job Misclassified Album Tracks As Singles', desc: 'github issue #500 (bafoed): library reorganize repair job moved tracks like `Surf Curse/Surf Curse - Nothing Yet (2017)/01 - Christine F.flac` to single-template paths like `Surf Curse/Surf Curse - Christine F/Surf Curse - Christine F.flac`. root cause: the job used `is_album = (group_size > 1)` where `group_size` was the count of tracks for the same album currently sitting in the transfer folder being scanned — when only one track of an album was in transfer (rest already moved to library, or album tags varied across tracks like "Buds" vs "Buds (Bonus)"), each track became a 1-element group → all routed through single template. fix: rewrote the job to delegate to the per-album planner (`core.library_reorganize.preview_album_reorganize` / `reorganize_queue`) — the same planner the artist-detail "reorganize" modal uses. db-driven: the planner knows the album has multiple tracks regardless of how many sit in the transfer folder, so the album-vs-single classification is structurally correct. apply mode delegates to the existing reorganize queue → file move + post-processing + db update + sidecar handling all flow through one code path. only iterates albums for the ACTIVE media server (matches the artist-detail modal\'s scope) — multi-server users (plex + jellyfin etc) won\'t accidentally have the job touch the inactive server\'s files. albums missing a metadata source id get a single "needs enrichment first" finding instead of n per-track "no source" findings. dropped ~500 loc of tag-reading + transfer-walk + template logic that was duplicated against the per-album path. files in transfer with no db entry are now exclusively the orphan_file_detector\'s domain (clean separation). 12 tests pin the delegation contract.', page: 'library' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue