@@ -6643,7 +6643,7 @@ def auth_spotify():
-
Using a reverse proxy? Your redirect URI is set to
{configured_uri}
@@ -6683,26 +6683,26 @@ def auth_spotify():
'''
else:
# Local access - simple message
- return f'
🔐 Spotify Authentication
Click the link below to authenticate:
{auth_url}
After authentication, return to the app.
'
+ return f'
Spotify Authentication
Click the link below to authenticate:
{auth_url}
After authentication, return to the app.
'
else:
- return "
❌ Spotify Authentication Failed
Could not initialize Spotify client. Check your credentials.
", 400
+ return "
Spotify Authentication Failed
Could not initialize Spotify client. Check your credentials.
", 400
except Exception as e:
- print(f"🔴 Error starting Spotify auth: {e}")
- return f"
❌ Spotify Authentication Error
{str(e)}
", 500
+ print(f"Error starting Spotify auth: {e}")
+ return f"
Spotify Authentication Error
{str(e)}
", 500
@app.route('/auth/tidal')
def auth_tidal():
"""
Initiates Tidal OAuth authentication flow
"""
- print("🔐🔐🔐 TIDAL AUTH ROUTE CALLED 🔐🔐🔐")
+ print("TIDAL AUTH ROUTE CALLED ")
try:
# Create a fresh tidal client to get OAuth URL
from core.tidal_client import TidalClient
temp_tidal_client = TidalClient()
if not temp_tidal_client.client_id:
- return "
❌ Tidal Authentication Failed
Tidal client ID not configured. Check your credentials.
", 400
+ return "
Tidal Authentication Failed
Tidal client ID not configured. Check your credentials.
", 400
# Generate PKCE challenge and store globally
temp_tidal_client._generate_pkce_challenge()
@@ -6718,20 +6718,20 @@ def auth_tidal():
configured_redirect = config_manager.get('tidal.redirect_uri', '')
if configured_redirect:
temp_tidal_client.redirect_uri = configured_redirect
- print(f"🔗 Using configured Tidal redirect_uri: {configured_redirect}")
+ print(f"Using configured Tidal redirect_uri: {configured_redirect}")
else:
# Fallback: dynamically set based on request host (non-Docker local access)
request_host = request.host.split(':')[0]
if request_host not in ('127.0.0.1', 'localhost'):
dynamic_redirect = f"http://{request_host}:8889/tidal/callback"
temp_tidal_client.redirect_uri = dynamic_redirect
- print(f"🔗 Tidal redirect_uri set from request host: {dynamic_redirect}")
+ print(f"Tidal redirect_uri set from request host: {dynamic_redirect}")
# Store PKCE + redirect_uri for callback to use the same values
with tidal_oauth_lock:
tidal_oauth_state["redirect_uri"] = temp_tidal_client.redirect_uri
- print(f"🔐 Stored PKCE - verifier: {temp_tidal_client.code_verifier[:20]}... challenge: {temp_tidal_client.code_challenge[:20]}...")
+ print(f"Stored PKCE - verifier: {temp_tidal_client.code_verifier[:20]}... challenge: {temp_tidal_client.code_challenge[:20]}...")
# Store profile_id for per-profile auth
profile_id = request.args.get('profile_id', '')
@@ -6751,10 +6751,10 @@ def auth_tidal():
auth_url = f"{temp_tidal_client.auth_url}?" + urllib.parse.urlencode(params)
- print(f"🔗 Generated Tidal OAuth URL: {auth_url}")
- print(f"🔗 Redirect URI in URL: {params['redirect_uri']}")
+ print(f"Generated Tidal OAuth URL: {auth_url}")
+ print(f"Redirect URI in URL: {params['redirect_uri']}")
- add_activity_item("🔐", "Tidal Auth Started", "Please complete OAuth in browser", "Now")
+ add_activity_item("", "Tidal Auth Started", "Please complete OAuth in browser", "Now")
# Detect if accessing remotely (copied from Spotify auth logic)
host = request.host.split(':')[0]
@@ -6767,7 +6767,7 @@ def auth_tidal():
if is_remote or is_docker:
# Show instructions for remote/docker access
- page_title = "🔐 Tidal Authentication (Remote/Docker)"
+ page_title = "Tidal Authentication (Remote/Docker)"
step_1_text = "Click the link below to authenticate with Tidal"
return f'''
@@ -6808,7 +6808,7 @@ def auth_tidal():
function copyIP() {{
navigator.clipboard.writeText('{host}').then(() => {{
const btn = event.target;
- btn.textContent = '✓ Copied!';
+ btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {{
btn.textContent = 'Copy IP';
@@ -6821,13 +6821,13 @@ def auth_tidal():
'''
else:
- return f'
🔐 Tidal Authentication
Please visit this URL to authenticate:
{auth_url}
After authentication, return to the app.
'
+ return f'
Tidal Authentication
Please visit this URL to authenticate:
{auth_url}
After authentication, return to the app.
'
except Exception as e:
- print(f"🔴 Error starting Tidal auth: {e}")
+ print(f"Error starting Tidal auth: {e}")
import traceback
- print(f"🔴 Full traceback: {traceback.format_exc()}")
- return f"
❌ Tidal Authentication Error
{str(e)}
", 500
+ print(f"Full traceback: {traceback.format_exc()}")
+ return f"
Tidal Authentication Error
{str(e)}
", 500
@app.route('/callback')
@@ -6843,19 +6843,19 @@ def spotify_callback():
if not auth_code:
error = request.args.get('error')
if error:
- print(f"🔴 Spotify OAuth error on port 8008: Spotify returned error: {error}")
- add_activity_item("❌", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now")
+ print(f"Spotify OAuth error on port 8008: Spotify returned error: {error}")
+ add_activity_item("", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now")
return f"
Spotify Authentication Failed
Spotify returned error: {error}
", 400
# No code AND no error — check if query params were stripped
if request.args:
- print(f"🔴 Spotify callback on port 8008 received unexpected params: {dict(request.args)}")
+ print(f"Spotify callback on port 8008 received unexpected params: {dict(request.args)}")
else:
# Completely empty — likely a healthcheck or spurious request
pass
return '', 204
- print(f"🎵 Spotify callback received on port 8008 with authorization code")
+ print(f"Spotify callback received on port 8008 with authorization code")
# Check for per-profile state parameter
state = request.args.get('state', '')
@@ -6863,7 +6863,7 @@ def spotify_callback():
if state and state.startswith('profile_'):
try:
profile_id_from_state = int(state.replace('profile_', ''))
- print(f"🎵 Per-profile callback detected for profile {profile_id_from_state}")
+ print(f"Per-profile callback detected for profile {profile_id_from_state}")
except ValueError:
pass
@@ -6891,7 +6891,7 @@ def spotify_callback():
# Invalidate cached profile client so it gets recreated with new tokens
with _profile_spotify_lock:
_profile_spotify_clients.pop(profile_id_from_state, None)
- add_activity_item("✅", "Spotify Auth Complete", f"Profile {profile_id_from_state} authenticated with Spotify", "Now")
+ add_activity_item("", "Spotify Auth Complete", f"Profile {profile_id_from_state} authenticated with Spotify", "Now")
return "
Spotify Authentication Successful!
Your personal Spotify account is now connected. You can close this window.
"
else:
raise Exception("Failed to exchange authorization code for access token")
@@ -6899,7 +6899,7 @@ def spotify_callback():
# Global callback (admin)
config = config_manager.get_spotify_config()
configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback")
- print(f"🎵 Using redirect_uri for token exchange: {configured_uri}")
+ print(f"Using redirect_uri for token exchange: {configured_uri}")
auth_manager = SpotifyOAuth(
client_id=config['client_id'],
@@ -6927,15 +6927,15 @@ def spotify_callback():
if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'):
spotify_enrichment_worker.client.reload_config()
spotify_enrichment_worker.client._invalidate_auth_cache()
- add_activity_item("✅", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now")
+ add_activity_item("", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now")
return "
Spotify Authentication Successful!
You can close this window.
"
else:
raise Exception("Token exchange succeeded but authentication validation failed")
else:
raise Exception("Failed to exchange authorization code for access token")
except Exception as e:
- print(f"🔴 Spotify OAuth callback error on port 8008: {e}")
- add_activity_item("❌", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now")
+ print(f"Spotify OAuth callback error on port 8008: {e}")
+ add_activity_item("", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now")
return f"
Spotify Authentication Failed
{str(e)}
", 400
@@ -6959,7 +6959,7 @@ def spotify_disconnect():
}
_status_cache_timestamps['spotify'] = time.time()
fallback_label = 'Deezer' if fallback_src == 'deezer' else 'Discogs' if fallback_src == 'discogs' else 'iTunes'
- add_activity_item("🔌", "Spotify Disconnected", f"Switched to {fallback_label} metadata source", "Now")
+ add_activity_item("", "Spotify Disconnected", f"Switched to {fallback_label} metadata source", "Now")
return jsonify({'success': True, 'message': f'Spotify disconnected. Now using {fallback_label}.'})
except Exception as e:
logger.error(f"Error disconnecting Spotify: {e}")
@@ -7034,21 +7034,21 @@ def tidal_callback():
WHERE id = ?
""", (enc_access, enc_refresh, profile_id_int))
conn.commit()
- add_activity_item("✅", "Tidal Auth Complete", f"Profile {profile_id_int} authenticated with Tidal", "Now")
- return "
✅ Tidal Authentication Successful!
Your personal Tidal account is now connected. You can close this window.
"
+ add_activity_item("", "Tidal Auth Complete", f"Profile {profile_id_int} authenticated with Tidal", "Now")
+ return "
Tidal Authentication Successful!
Your personal Tidal account is now connected. You can close this window.
"
except Exception as profile_err:
- print(f"⚠️ Per-profile Tidal auth failed, falling back to global: {profile_err}")
+ print(f"Per-profile Tidal auth failed, falling back to global: {profile_err}")
# Global: Re-initialize the main global tidal_client instance with the new token
tidal_client = TidalClient()
if tidal_enrichment_worker:
tidal_enrichment_worker.client = tidal_client
- return "
✅ Tidal Authentication Successful!
You can now close this window and return to the SoulSync application.
"
+ return "
Tidal Authentication Successful!
You can now close this window and return to the SoulSync application.
"
else:
- return "
❌ Tidal Authentication Failed
Could not exchange authorization code for a token. Please try again.
", 400
+ return "
Tidal Authentication Failed
Could not exchange authorization code for a token. Please try again.
", 400
except Exception as e:
- print(f"🔴 Error during Tidal token exchange: {e}")
- return f"
❌ An Error Occurred
An unexpected error occurred during the authentication process: {e}
", 500
+ print(f"Error during Tidal token exchange: {e}")
+ return f"
An Error Occurred
An unexpected error occurred during the authentication process: {e}
", 500
# --- Deezer OAuth ---
@@ -7070,7 +7070,7 @@ def auth_deezer():
host = request.host.split(':')[0]
return f"""
-
🎵 Deezer Authorization
+
Deezer Authorization
Click the link below to authorize SoulSync with your Deezer account:
Authorize on Deezer →
@@ -7127,12 +7127,12 @@ def deezer_callback():
deezer_client = _get_deezer_client()
deezer_client.reload_config()
- add_activity_item("✅", "Deezer Auth Complete", "Deezer account connected via OAuth", "Now")
+ add_activity_item("", "Deezer Auth Complete", "Deezer account connected via OAuth", "Now")
logger.info("Deezer OAuth authentication successful")
return """
-
✅ Deezer Authentication Successful!
+
Deezer Authentication Successful!
Your Deezer account is now connected. You can close this window.
"""
@@ -7147,17 +7147,17 @@ def deezer_callback():
def get_beatport_hero_tracks():
"""Get fresh tracks from Beatport hero slideshow for the rebuild slider"""
try:
- logger.info("🎯 Fetching Beatport hero tracks...")
+ logger.info("Fetching Beatport hero tracks...")
# Check cache first
cached_data = get_cached_beatport_data('homepage', 'hero_tracks')
if cached_data:
- logger.info("🎯 Returning cached hero tracks data")
+ logger.info("Returning cached hero tracks data")
response = jsonify(cached_data)
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh hero tracks data...")
+ logger.info("Cache miss - scraping fresh hero tracks data...")
# Initialize scraper
scraper = BeatportUnifiedScraper()
@@ -7169,7 +7169,7 @@ def get_beatport_hero_tracks():
valid_tracks = []
seen_urls = set()
- logger.info(f"🔍 Processing {len(tracks)} raw tracks from scraper (SMART FILTERING)...")
+ logger.info(f"Processing {len(tracks)} raw tracks from scraper (SMART FILTERING)...")
for i, track in enumerate(tracks):
logger.info(f" Track {i+1}: {track.get('title', 'NO_TITLE')} - {track.get('artist', 'NO_ARTIST')}")
@@ -7220,7 +7220,7 @@ def get_beatport_hero_tracks():
skip_reasons.append("Duplicate URL")
if not is_valid:
- logger.info(f" ❌ Track {i+1} filtered out: {', '.join(skip_reasons)}")
+ logger.info(f" Track {i+1} filtered out: {', '.join(skip_reasons)}")
continue
# Mark URL as seen for deduplication
@@ -7263,9 +7263,9 @@ def get_beatport_hero_tracks():
break
valid_tracks.append(track_data)
- logger.info(f" ✅ Track {i+1} added: {title} - {artist}")
+ logger.info(f" Track {i+1} added: {title} - {artist}")
- logger.info(f"✅ Retrieved {len(valid_tracks)} valid unique Beatport tracks (SMART FILTERING)")
+ logger.info(f"Retrieved {len(valid_tracks)} valid unique Beatport tracks (SMART FILTERING)")
# Prepare response data
response_data = {
@@ -7282,7 +7282,7 @@ def get_beatport_hero_tracks():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error fetching Beatport tracks: {str(e)}")
+ logger.error(f"Error fetching Beatport tracks: {str(e)}")
return jsonify({
'success': False,
'error': str(e),
@@ -7303,7 +7303,7 @@ def get_beatport_new_releases():
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh new releases data...")
+ logger.info("Cache miss - scraping fresh new releases data...")
# Initialize scraper
scraper = BeatportUnifiedScraper()
@@ -7331,12 +7331,12 @@ def get_beatport_new_releases():
if releases_container:
release_cards = releases_container.select('[class*="ReleaseCard-style__Wrapper"]')
else:
- logger.warning("⚠️ No New Releases GridSlider found, trying page-wide ReleaseCard search")
+ logger.warning("No New Releases GridSlider found, trying page-wide ReleaseCard search")
release_cards = soup.select('[class*="ReleaseCard-style__Wrapper"]')
releases = []
- logger.info(f"🔍 Found {len(release_cards)} release cards")
+ logger.info(f"Found {len(release_cards)} release cards")
for i, card in enumerate(release_cards[:100]): # Limit to 100 for 10 slides
release_data = {}
@@ -7395,7 +7395,7 @@ def get_beatport_new_releases():
releases.append(release_data)
- logger.info(f"✅ Successfully extracted {len(releases)} new releases")
+ logger.info(f"Successfully extracted {len(releases)} new releases")
# Prepare response data
response_data = {
@@ -7413,7 +7413,7 @@ def get_beatport_new_releases():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error fetching new releases: {str(e)}")
+ logger.error(f"Error fetching new releases: {str(e)}")
return jsonify({
'success': False,
'error': str(e),
@@ -7425,17 +7425,17 @@ def get_beatport_new_releases():
def get_beatport_featured_charts():
"""Get featured charts from Beatport for the charts slider grid using GridSlider approach"""
try:
- logger.info("🔥 Fetching Beatport featured charts...")
+ logger.info("Fetching Beatport featured charts...")
# Check cache first
cached_data = get_cached_beatport_data('homepage', 'featured_charts')
if cached_data:
- logger.info("🔥 Returning cached featured charts data")
+ logger.info("Returning cached featured charts data")
response = jsonify(cached_data)
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh featured charts data...")
+ logger.info("Cache miss - scraping fresh featured charts data...")
# Initialize scraper
scraper = BeatportUnifiedScraper()
@@ -7449,21 +7449,21 @@ def get_beatport_featured_charts():
gridsliders = soup.select('[class*="GridSlider-style__Wrapper"]')
featured_container = None
- logger.info(f"🔍 Checking {len(gridsliders)} GridSlider containers for featured charts...")
+ logger.info(f"Checking {len(gridsliders)} GridSlider containers for featured charts...")
for container in gridsliders:
h2 = container.select_one('h2')
if h2:
title = h2.get_text(strip=True).lower()
- logger.info(f"📋 Found section: '{h2.get_text(strip=True)}'")
+ logger.info(f"Found section: '{h2.get_text(strip=True)}'")
if 'featured' in title and 'chart' in title:
featured_container = container
- logger.info(f"🔥 FOUND FEATURED CHARTS: '{h2.get_text(strip=True)}'")
+ logger.info(f"FOUND FEATURED CHARTS: '{h2.get_text(strip=True)}'")
break
if not featured_container:
- logger.warning("❌ No Featured Charts GridSlider container found")
+ logger.warning("No Featured Charts GridSlider container found")
return jsonify({
'success': False,
'error': 'Featured Charts section not found',
@@ -7474,7 +7474,7 @@ def get_beatport_featured_charts():
charts = []
chart_links = featured_container.select('a[href*="/chart/"]')
- logger.info(f"📊 Found {len(chart_links)} chart links in Featured Charts section")
+ logger.info(f"Found {len(chart_links)} chart links in Featured Charts section")
for i, link in enumerate(chart_links[:100]): # Limit to 100 for 10 slides
chart_data = {}
@@ -7542,9 +7542,9 @@ def get_beatport_featured_charts():
# Only add if we have meaningful data
if 'name' in chart_data and 'url' in chart_data:
charts.append(chart_data)
- logger.info(f"✅ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}")
+ logger.info(f"Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}")
- logger.info(f"📊 Successfully extracted {len(charts)} featured charts")
+ logger.info(f"Successfully extracted {len(charts)} featured charts")
# Prepare response data
response_data = {
@@ -7562,7 +7562,7 @@ def get_beatport_featured_charts():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error fetching featured charts: {str(e)}")
+ logger.error(f"Error fetching featured charts: {str(e)}")
return jsonify({
'success': False,
'error': str(e),
@@ -7574,17 +7574,17 @@ def get_beatport_featured_charts():
def get_beatport_dj_charts():
"""Get DJ charts from Beatport for the DJ charts slider using Carousel approach"""
try:
- logger.info("🎧 Fetching Beatport DJ charts...")
+ logger.info("Fetching Beatport DJ charts...")
# Check cache first
cached_data = get_cached_beatport_data('homepage', 'dj_charts')
if cached_data:
- logger.info("🎧 Returning cached DJ charts data")
+ logger.info("Returning cached DJ charts data")
response = jsonify(cached_data)
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh DJ charts data...")
+ logger.info("Cache miss - scraping fresh DJ charts data...")
# Initialize scraper
scraper = BeatportUnifiedScraper()
@@ -7598,21 +7598,21 @@ def get_beatport_dj_charts():
carousels = soup.select('[class*="Carousel-style__Wrapper"]')
dj_container = None
- logger.info(f"🔍 Checking {len(carousels)} Carousel containers for DJ charts...")
+ logger.info(f"Checking {len(carousels)} Carousel containers for DJ charts...")
# Based on test results, DJ charts are in the second carousel (index 1) with ~9 chart links
for i, container in enumerate(carousels):
chart_links = container.select('a[href*="/chart/"]')
- logger.info(f"📋 Carousel {i+1}: {len(chart_links)} chart links")
+ logger.info(f"Carousel {i+1}: {len(chart_links)} chart links")
# DJ charts container typically has 8-12 chart links (not 99+ like featured charts)
if 5 <= len(chart_links) <= 15:
dj_container = container
- logger.info(f"🔥 FOUND DJ CHARTS: Carousel {i+1} with {len(chart_links)} charts")
+ logger.info(f"FOUND DJ CHARTS: Carousel {i+1} with {len(chart_links)} charts")
break
if not dj_container:
- logger.warning("❌ No DJ Charts Carousel container found")
+ logger.warning("No DJ Charts Carousel container found")
return jsonify({
'success': False,
'error': 'DJ Charts section not found',
@@ -7623,7 +7623,7 @@ def get_beatport_dj_charts():
charts = []
chart_links = dj_container.select('a[href*="/chart/"]')
- logger.info(f"📊 Found {len(chart_links)} DJ chart links")
+ logger.info(f"Found {len(chart_links)} DJ chart links")
for i, link in enumerate(chart_links):
chart_data = {}
@@ -7682,9 +7682,9 @@ def get_beatport_dj_charts():
# Only add if we have meaningful data
if 'name' in chart_data and 'url' in chart_data:
charts.append(chart_data)
- logger.info(f"✅ DJ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}")
+ logger.info(f"DJ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}")
- logger.info(f"📊 Successfully extracted {len(charts)} DJ charts")
+ logger.info(f"Successfully extracted {len(charts)} DJ charts")
# Prepare response data
response_data = {
@@ -7702,7 +7702,7 @@ def get_beatport_dj_charts():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error fetching DJ charts: {str(e)}")
+ logger.error(f"Error fetching DJ charts: {str(e)}")
return jsonify({
'success': False,
'error': str(e),
@@ -7749,7 +7749,7 @@ def search_music():
logger.info(f"Web UI Search initiated for: '{query}'")
# Add activity for search start
- add_activity_item("🔍", "Search Started", f"'{query}'", "Now")
+ add_activity_item("", "Search Started", f"'{query}'", "Now")
try:
tracks, albums = run_async(soulseek_client.search(query))
@@ -7773,7 +7773,7 @@ def search_music():
# Add activity for search completion
total_results = len(all_results)
- add_activity_item("✅", "Search Complete", f"'{query}' - {total_results} results", "Now")
+ add_activity_item("", "Search Complete", f"'{query}' - {total_results} results", "Now")
return jsonify({"results": all_results})
@@ -8210,7 +8210,7 @@ def stream_enhanced_search_track():
if not track_name or not artist_name:
return jsonify({"error": "Track name and artist name are required"}), 400
- logger.info(f"▶️ Enhanced search stream request: '{track_name}' by '{artist_name}'")
+ logger.info(f"Enhanced search stream request: '{track_name}' by '{artist_name}'")
try:
# Create a temporary SpotifyTrack-like object for the matching engine
@@ -8235,13 +8235,13 @@ def stream_enhanced_search_track():
_hybrid_first = _hybrid_order[0] if _hybrid_order else config_manager.get('download_source.hybrid_primary', 'hifi')
if download_mode == 'soulseek' or (download_mode == 'hybrid' and _hybrid_first == 'soulseek'):
effective_mode = 'youtube' # Soulseek is too slow for streaming preview
- logger.info("▶️ Stream source is 'active' but primary is Soulseek — falling back to YouTube")
+ logger.info("Stream source is 'active' but primary is Soulseek — falling back to YouTube")
elif download_mode == 'hybrid':
effective_mode = _hybrid_first
else:
effective_mode = download_mode
- logger.info(f"▶️ Stream source: {stream_source} → effective: {effective_mode}")
+ logger.info(f"Stream source: {stream_source} → effective: {effective_mode}")
# Generate search queries based on effective stream mode
search_queries = []
@@ -8258,7 +8258,7 @@ def stream_enhanced_search_track():
if cleaned_name and cleaned_name.lower() != track_name.lower():
search_queries.append(f"{artist_name} {cleaned_name}".strip())
- logger.info(f"🔍 {effective_mode.title()} stream: Searching with artist + track name: {search_queries}")
+ logger.info(f"{effective_mode.title()} stream: Searching with artist + track name: {search_queries}")
else:
# Soulseek mode: Track name only to avoid keyword filtering
if track_name.strip():
@@ -8270,7 +8270,7 @@ def stream_enhanced_search_track():
if cleaned_name and cleaned_name.lower() != track_name.lower():
search_queries.append(cleaned_name.strip())
- logger.info(f"🔍 Soulseek mode: Searching by track name only (will match with artist): {search_queries}")
+ logger.info(f"Soulseek mode: Searching by track name only (will match with artist): {search_queries}")
# Remove duplicates while preserving order
unique_queries = []
@@ -8296,7 +8296,7 @@ def stream_enhanced_search_track():
# Try queries sequentially until we find a good match
for query_index, query in enumerate(search_queries):
- logger.info(f"🔍 Query {query_index + 1}/{len(search_queries)}: '{query}'")
+ logger.info(f"Query {query_index + 1}/{len(search_queries)}: '{query}'")
try:
# Search using the stream source client (not the download source)
@@ -8306,7 +8306,7 @@ def stream_enhanced_search_track():
tracks_result, _ = run_async(soulseek_client.search(query, timeout=15))
if tracks_result:
- logger.info(f"✅ Found {len(tracks_result)} results for query: '{query}'")
+ logger.info(f"Found {len(tracks_result)} results for query: '{query}'")
# Use matching engine to find best match
_max_q = config_manager.get('soulseek.max_peer_queue', 0) or 0
@@ -8330,30 +8330,30 @@ def stream_enhanced_search_track():
"result_type": "track"
}
- logger.info(f"✅ Returning best match from query '{query}': {best_result.filename} ({best_result.quality})")
+ logger.info(f"Returning best match from query '{query}': {best_result.filename} ({best_result.quality})")
return jsonify({
"success": True,
"result": result_dict
})
else:
- logger.info(f"⏭️ No suitable matches for query '{query}', trying next query...")
+ logger.info(f"No suitable matches for query '{query}', trying next query...")
else:
- logger.info(f"⏭️ No results for query '{query}', trying next query...")
+ logger.info(f"No results for query '{query}', trying next query...")
except Exception as search_error:
- logger.warning(f"⚠️ Error searching with query '{query}': {search_error}")
+ logger.warning(f"Error searching with query '{query}': {search_error}")
continue
# If we get here, none of the queries found a suitable match
- logger.warning(f"❌ No suitable matches found after trying {len(search_queries)} queries")
+ logger.warning(f"No suitable matches found after trying {len(search_queries)} queries")
return jsonify({
"success": False,
"error": "No suitable track found after trying multiple search strategies"
}), 404
except Exception as e:
- logger.error(f"❌ Error streaming enhanced search track: {e}", exc_info=True)
+ logger.error(f"Error streaming enhanced search track: {e}", exc_info=True)
return jsonify({"error": str(e)}), 500
# =============================================================================
@@ -8429,16 +8429,16 @@ def download_music_video():
if best and best_score >= 0.5:
artist_name = best.artists[0] if best.artists else raw_channel
track_title = best.name
- print(f"🎬 [Music Video] Matched to: {artist_name} - {track_title} (confidence: {best_score:.2f})")
+ print(f"[Music Video] Matched to: {artist_name} - {track_title} (confidence: {best_score:.2f})")
else:
# Parse artist from video title: "Artist - Title" pattern
if ' - ' in raw_title:
parts = raw_title.split(' - ', 1)
artist_name = parts[0].strip()
track_title = _re.sub(r'\s*[\(\[].*?[\)\]]', '', parts[1]).strip()
- print(f"🎬 [Music Video] No metadata match, using parsed: {artist_name} - {track_title}")
+ print(f"[Music Video] No metadata match, using parsed: {artist_name} - {track_title}")
except Exception as e:
- print(f"⚠️ [Music Video] Metadata lookup failed: {e}")
+ print(f"[Music Video] Metadata lookup failed: {e}")
if ' - ' in raw_title:
parts = raw_title.split(' - ', 1)
artist_name = parts[0].strip()
@@ -8470,17 +8470,17 @@ def download_music_video():
_music_video_downloads[video_id]['status'] = 'completed'
_music_video_downloads[video_id]['progress'] = 100
_music_video_downloads[video_id]['path'] = final_path
- print(f"✅ [Music Video] Downloaded: {artist_name} - {track_title} → {final_path}")
- add_activity_item("🎬", "Music Video Downloaded", f"{artist_name} - {track_title}", "Now")
+ print(f"[Music Video] Downloaded: {artist_name} - {track_title} → {final_path}")
+ add_activity_item("", "Music Video Downloaded", f"{artist_name} - {track_title}", "Now")
else:
_music_video_downloads[video_id]['status'] = 'error'
_music_video_downloads[video_id]['error'] = 'Download failed — file not found'
- print(f"❌ [Music Video] Download failed for: {artist_name} - {track_title}")
+ print(f"[Music Video] Download failed for: {artist_name} - {track_title}")
except Exception as e:
_music_video_downloads[video_id]['status'] = 'error'
_music_video_downloads[video_id]['error'] = str(e)
- print(f"❌ [Music Video] Error: {e}")
+ print(f"[Music Video] Error: {e}")
# Run in background thread
import threading
@@ -8553,8 +8553,8 @@ def start_download():
# Add activity for album download start
album_name = data.get('album_name', 'Unknown Album')
- logger.info(f"📥 Starting simple album download: '{album_name}' with {started_downloads}/{len(tracks)} tracks")
- add_activity_item("📥", "Album Download Started", f"'{album_name}' - {started_downloads} tracks", "Now")
+ logger.info(f"Starting simple album download: '{album_name}' with {started_downloads}/{len(tracks)} tracks")
+ add_activity_item("", "Album Download Started", f"'{album_name}' - {started_downloads} tracks", "Now")
return jsonify({
"success": True,
@@ -8567,13 +8567,13 @@ def start_download():
filename = data.get('filename')
file_size = data.get('size', 0)
- logger.info(f"📥 Download request - Username: {username}, Filename: {filename[:50]}...")
+ logger.info(f"Download request - Username: {username}, Filename: {filename[:50]}...")
if not username or not filename:
return jsonify({"error": "Missing username or filename."}), 400
download_id = run_async(soulseek_client.download(username, filename, file_size))
- logger.info(f"📥 Download ID returned: {download_id}")
+ logger.info(f"Download ID returned: {download_id}")
if download_id:
# Register download for post-processing (simple transfer to /Transfer)
@@ -8599,8 +8599,8 @@ def start_download():
# Extract track name from filename for activity
track_name = filename.split('/')[-1] if '/' in filename else filename.split('\\')[-1] if '\\' in filename else filename
- logger.info(f"📥 Starting simple track download: '{track_name}'")
- add_activity_item("📥", "Track Download Started", f"'{track_name}'", "Now")
+ logger.info(f"Starting simple track download: '{track_name}'")
+ add_activity_item("", "Track Download Started", f"'{track_name}'", "Now")
return jsonify({"success": True, "message": "Download started"})
else:
logger.error(f"Failed to start download for: {filename}")
@@ -8673,11 +8673,11 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
file_path = os.path.join(root, file)
# Fast path: if path aligns with expected directory structure, return now
if api_dir_parts and _path_matches_api_dirs(file_path):
- print(f"✅ Found path-confirmed match in {location_name}: {file_path}")
+ print(f"Found path-confirmed match in {location_name}: {file_path}")
return file_path, 1.0
if not api_dir_parts:
# No directory info to disambiguate — return first match (original behavior)
- print(f"✅ Found exact match in {location_name}: {file_path}")
+ print(f"Found exact match in {location_name}: {file_path}")
return file_path, 1.0
exact_matches.append(file_path)
continue
@@ -8689,10 +8689,10 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
if stripped_stem != file_stem and stripped_stem + file_ext_part == target_basename:
file_path = os.path.join(root, file)
if api_dir_parts and _path_matches_api_dirs(file_path):
- print(f"✅ Found path-confirmed dedup match in {location_name}: {file_path}")
+ print(f"Found path-confirmed dedup match in {location_name}: {file_path}")
return file_path, 1.0
if not api_dir_parts:
- print(f"✅ Found dedup-suffix match in {location_name}: {file_path}")
+ print(f"Found dedup-suffix match in {location_name}: {file_path}")
return file_path, 1.0
exact_matches.append(file_path)
continue
@@ -8708,7 +8708,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
# Return best exact match (disambiguated by path), or fall back to fuzzy
if exact_matches:
if len(exact_matches) == 1:
- print(f"✅ Found exact match in {location_name}: {exact_matches[0]}")
+ print(f"Found exact match in {location_name}: {exact_matches[0]}")
return exact_matches[0], 1.0
# Multiple files share the basename — pick the one whose path best
# matches the expected directory structure from the Soulseek remote path
@@ -8720,7 +8720,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
if score > best_score:
best_score = score
best = m
- print(f"⚠️ Found {len(exact_matches)} files named '{target_basename}' in {location_name}, picked best path match: {best}")
+ print(f"Found {len(exact_matches)} files named '{target_basename}' in {location_name}, picked best path match: {best}")
return best, 1.0
return best_fuzzy_path, highest_fuzzy_similarity
@@ -8742,7 +8742,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
if downloads_similarity > 0.85:
location = 'downloads'
if downloads_similarity < 1.0:
- print(f"✅ Found fuzzy match in downloads ({downloads_similarity:.2f}): {best_downloads_path}")
+ print(f"Found fuzzy match in downloads ({downloads_similarity:.2f}): {best_downloads_path}")
return (best_downloads_path, location)
# If not found in downloads and transfer_dir is provided, search there
@@ -8753,7 +8753,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
if transfer_similarity > 0.85:
location = 'transfer'
if transfer_similarity < 1.0:
- print(f"✅ Found fuzzy match in transfer ({transfer_similarity:.2f}): {best_transfer_path}")
+ print(f"Found fuzzy match in transfer ({transfer_similarity:.2f}): {best_transfer_path}")
return (best_transfer_path, location)
# Don't spam logs - file not found is common for completed/processed downloads
@@ -8818,7 +8818,7 @@ def get_download_status():
with matched_context_lock:
has_active_context = context_key in matched_downloads_context
if has_active_context:
- print(f"🔄 Orphaned key {context_key} has active context — retry re-used same source, treating as active")
+ print(f"Orphaned key {context_key} has active context — retry re-used same source, treating as active")
_orphaned_download_keys.discard(context_key)
# Fall through to normal processing below
else:
@@ -8829,10 +8829,10 @@ def get_download_status():
if found_path:
try:
os.remove(found_path)
- print(f"🧹 Deleted orphaned download: {os.path.basename(found_path)}")
+ print(f"Deleted orphaned download: {os.path.basename(found_path)}")
orphan_cleaned = True
except Exception as e:
- print(f"⚠️ Failed to delete orphaned file (will retry next poll): {e}")
+ print(f"Failed to delete orphaned file (will retry next poll): {e}")
else:
# File not on disk (already gone or never written) — nothing to clean
orphan_cleaned = True
@@ -8856,10 +8856,10 @@ def get_download_status():
available_keys = list(matched_downloads_context.keys())[:5] if not context else None
if context:
- print(f"✅ [Context Lookup] Found context for key: {context_key}")
+ print(f"[Context Lookup] Found context for key: {context_key}")
elif context_key not in _stale_transfer_keys:
# Only log once per stale key to avoid spamming every poll cycle
- print(f"⚠️ [Context Lookup] No context found for key: {context_key}")
+ print(f"[Context Lookup] No context found for key: {context_key}")
print(f" Available keys: {available_keys}...")
_stale_transfer_keys.add(context_key)
@@ -8873,10 +8873,10 @@ def get_download_status():
# Prevent two contexts from claiming the same physical file
_norm_path = os.path.normpath(found_path)
if _norm_path in _files_claimed_this_cycle:
- print(f"⚠️ File already claimed by another context this cycle: {os.path.basename(found_path)} — deferring to next poll")
+ print(f"File already claimed by another context this cycle: {os.path.basename(found_path)} — deferring to next poll")
else:
_files_claimed_this_cycle.add(_norm_path)
- print(f"🎯 Found completed matched file on disk: {found_path}")
+ print(f"Found completed matched file on disk: {found_path}")
completed_matched_downloads.append((context_key, context, found_path))
# Don't add to _processed_download_ids yet - wait until thread starts successfully
@@ -8885,7 +8885,7 @@ def get_download_status():
if context_key in _download_retry_attempts:
retry_count = _download_retry_attempts[context_key]['count']
elapsed = time.time() - _download_retry_attempts[context_key]['first_attempt']
- print(f"✅ File found after {retry_count} retry attempt(s) ({elapsed:.1f}s): {os.path.basename(filename_from_api)}")
+ print(f"File found after {retry_count} retry attempt(s) ({elapsed:.1f}s): {os.path.basename(filename_from_api)}")
del _download_retry_attempts[context_key]
else:
# File not found yet - implement retry logic instead of immediate give-up
@@ -8897,7 +8897,7 @@ def get_download_status():
'count': 1,
'first_attempt': time.time()
}
- print(f"⏳ File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt 1/{_download_retry_max})")
+ print(f"File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt 1/{_download_retry_max})")
else:
# Increment retry count
_download_retry_attempts[context_key]['count'] += 1
@@ -8906,19 +8906,19 @@ def get_download_status():
if retry_count >= _download_retry_max:
# Max retries reached, give up
- print(f"❌ CRITICAL: Could not find '{os.path.basename(filename_from_api)}' after {retry_count} attempts over {elapsed:.1f}s. Giving up.")
+ print(f"CRITICAL: Could not find '{os.path.basename(filename_from_api)}' after {retry_count} attempts over {elapsed:.1f}s. Giving up.")
_processed_download_ids.add(context_key)
# Clean up retry tracking
del _download_retry_attempts[context_key]
else:
- print(f"⏳ File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt {retry_count}/{_download_retry_max}, elapsed: {elapsed:.1f}s)")
+ print(f"File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt {retry_count}/{_download_retry_max}, elapsed: {elapsed:.1f}s)")
# If we found completed matched downloads, start processing them in background threads
if completed_matched_downloads:
def process_completed_downloads():
for context_key, context, found_path in completed_matched_downloads:
try:
- print(f"🚀 Starting post-processing thread for: {context_key}")
+ print(f"Starting post-processing thread for: {context_key}")
# Use verification wrapper if context has task tracking IDs,
# otherwise call directly (race guard flag still gets set on context)
_pp_task_id = context.get('task_id')
@@ -8935,16 +8935,16 @@ def get_download_status():
# Only mark as processed AFTER thread starts successfully
_processed_download_ids.add(context_key)
- print(f"✅ Marked as processed: {context_key}")
+ print(f"Marked as processed: {context_key}")
# DON'T remove context immediately - verification worker needs it
# Context will be cleaned up by verification worker after both processors complete
- print(f"💾 Keeping context for verification worker: {context_key}")
+ print(f"Keeping context for verification worker: {context_key}")
except Exception as e:
- print(f"❌ Error starting post-processing thread for {context_key}: {e}")
+ print(f"Error starting post-processing thread for {context_key}: {e}")
# Don't add to processed set if thread failed to start
- print(f"⚠️ Will retry {context_key} on next check")
+ print(f"Will retry {context_key} on next check")
# Start a single thread to manage the launching of all processing threads
processing_thread = threading.Thread(target=process_completed_downloads)
@@ -8990,14 +8990,14 @@ def get_download_status():
# Prevent two contexts from claiming the same physical file
_st_norm = os.path.normpath(found_path)
if _st_norm in _files_claimed_this_cycle:
- print(f"⚠️ [{source_label}] File already claimed this cycle: {os.path.basename(found_path)} — deferring")
+ print(f"[{source_label}] File already claimed this cycle: {os.path.basename(found_path)} — deferring")
continue
_files_claimed_this_cycle.add(_st_norm)
- print(f"🎯 [{source_label}] Found completed matched file on disk: {found_path}")
+ print(f"[{source_label}] Found completed matched file on disk: {found_path}")
# Start post-processing thread
def process_streaming_download(_ctx_key=context_key, _ctx=context, _path=found_path, _label=source_label):
try:
- print(f"🚀 [{_label}] Starting post-processing thread for: {_ctx_key}")
+ print(f"[{_label}] Starting post-processing thread for: {_ctx_key}")
# Use verification wrapper if context has task tracking IDs
_st_task_id = _ctx.get('task_id')
_st_batch_id = _ctx.get('batch_id')
@@ -9011,9 +9011,9 @@ def get_download_status():
thread.daemon = True
thread.start()
_processed_download_ids.add(_ctx_key)
- print(f"✅ [{_label}] Marked as processed: {_ctx_key}")
+ print(f"[{_label}] Marked as processed: {_ctx_key}")
except Exception as e:
- print(f"❌ [{_label}] Error starting post-processing thread for {_ctx_key}: {e}")
+ print(f"[{_label}] Error starting post-processing thread for {_ctx_key}: {e}")
processing_thread = threading.Thread(target=process_streaming_download)
processing_thread.daemon = True
@@ -9024,7 +9024,7 @@ def get_download_status():
_processed_download_ids.add(context_key)
except Exception as streaming_error:
import traceback
- print(f"⚠️ Could not fetch YouTube/Tidal downloads for status: {streaming_error}")
+ print(f"Could not fetch YouTube/Tidal downloads for status: {streaming_error}")
traceback.print_exc()
return jsonify({"transfers": all_transfers})
@@ -9147,7 +9147,7 @@ def get_task_candidates(task_id):
"candidate_count": len(serialized),
})
except Exception as e:
- print(f"❌ [Candidates] Error fetching candidates for task {task_id}: {e}")
+ print(f"[Candidates] Error fetching candidates for task {task_id}: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/downloads/task/
/download-candidate', methods=['POST'])
@@ -9255,11 +9255,11 @@ def download_selected_candidate(task_id):
missing_download_executor.submit(_run_manual_download)
track_name = track_info.get('name', 'Unknown')
- print(f"🎯 [Manual Download] User selected candidate for '{track_name}' from {username}")
+ print(f"[Manual Download] User selected candidate for '{track_name}' from {username}")
return jsonify({"success": True, "message": f"Download initiated for '{track_name}'"})
except Exception as e:
- print(f"❌ [Manual Download] Error: {e}")
+ print(f"[Manual Download] Error: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -9286,12 +9286,12 @@ def clear_quarantine():
shutil.rmtree(entry_path)
removed_files += 1
except Exception as e:
- print(f"⚠️ [Quarantine] Failed to remove {entry}: {e}")
+ print(f"[Quarantine] Failed to remove {entry}: {e}")
- print(f"🧹 [Quarantine] Cleared {removed_files} item(s) from quarantine folder")
+ print(f"[Quarantine] Cleared {removed_files} item(s) from quarantine folder")
return jsonify({"success": True, "message": f"Quarantine cleared ({removed_files} item{'s' if removed_files != 1 else ''} removed)."})
except Exception as e:
- print(f"❌ [Quarantine] Error clearing quarantine: {e}")
+ print(f"[Quarantine] Error clearing quarantine: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/scan/request', methods=['POST'])
@@ -9309,7 +9309,7 @@ def request_media_scan():
result = web_scan_manager.request_scan(reason=reason)
- add_activity_item("📡", "Media Scan", f"Scan requested: {reason}", "Now")
+ add_activity_item("", "Media Scan", f"Scan requested: {reason}", "Now")
return jsonify({
"success": True,
"scan_info": result,
@@ -9380,7 +9380,7 @@ def request_incremental_database_update():
})
db_update_executor.submit(_run_db_update_task, False, active_server)
- add_activity_item("🔄", "Database Update", f"Incremental update started: {reason}", "Now")
+ add_activity_item("", "Database Update", f"Incremental update started: {reason}", "Now")
return jsonify({
"success": True,
"message": "Incremental database update started",
@@ -9483,7 +9483,7 @@ def clear_all_searches():
try:
success = run_async(soulseek_client.clear_all_searches())
if success:
- add_activity_item("🧹", "Search Cleanup", "All search history cleared manually", "Now")
+ add_activity_item("", "Search Cleanup", "All search history cleared manually", "Now")
return jsonify({"success": True, "message": "All searches cleared."})
else:
return jsonify({"success": False, "error": "Backend failed to clear searches."}), 500
@@ -9505,7 +9505,7 @@ def maintain_search_history():
keep_searches=keep_searches, trigger_threshold=trigger_threshold
))
if success:
- add_activity_item("🧹", "Search Maintenance", f"Search history maintained (keeping {keep_searches} searches)", "Now")
+ add_activity_item("", "Search Maintenance", f"Search history maintained (keeping {keep_searches} searches)", "Now")
return jsonify({"success": True, "message": f"Search history maintained (keeping {keep_searches} searches)."})
else:
return jsonify({"success": False, "error": "Backend failed to maintain search history."}), 500
@@ -9531,13 +9531,13 @@ def fix_artist_image_url(thumb_url):
if needs_fixing:
active_server = config_manager.get_active_media_server()
- print(f"🔧 Fixing URL: {thumb_url}, Active server: {active_server}")
+ print(f"Fixing URL: {thumb_url}, Active server: {active_server}")
if active_server == 'plex':
plex_config = config_manager.get_plex_config()
plex_base_url = plex_config.get('base_url', '')
plex_token = plex_config.get('token', '')
- print(f"🔧 Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...")
+ print(f"Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...")
if plex_base_url and plex_token:
# Extract the path from URL
@@ -9552,14 +9552,14 @@ def fix_artist_image_url(thumb_url):
# Construct proper Plex URL with token
fixed_url = f"{plex_base_url.rstrip('/')}{path}?X-Plex-Token={plex_token}"
- print(f"🔧 Fixed URL: {fixed_url}")
+ print(f"Fixed URL: {fixed_url}")
return fixed_url
elif active_server == 'jellyfin':
jellyfin_config = config_manager.get_jellyfin_config()
jellyfin_base_url = jellyfin_config.get('base_url', '')
jellyfin_token = jellyfin_config.get('api_key', '')
- print(f"🔧 Jellyfin config - base_url: {jellyfin_base_url}, token: {jellyfin_token[:10] if jellyfin_token else 'None'}...")
+ print(f"Jellyfin config - base_url: {jellyfin_base_url}, token: {jellyfin_token[:10] if jellyfin_token else 'None'}...")
if jellyfin_base_url:
# Extract the path from URL
@@ -9578,7 +9578,7 @@ def fix_artist_image_url(thumb_url):
fixed_url = f"{jellyfin_base_url.rstrip('/')}{path}{separator}X-Emby-Token={jellyfin_token}"
else:
fixed_url = f"{jellyfin_base_url.rstrip('/')}{path}"
- print(f"🔧 Fixed URL: {fixed_url}")
+ print(f"Fixed URL: {fixed_url}")
return fixed_url
elif active_server == 'navidrome':
@@ -9586,7 +9586,7 @@ def fix_artist_image_url(thumb_url):
navidrome_base_url = navidrome_config.get('base_url', '')
navidrome_username = navidrome_config.get('username', '')
navidrome_password = navidrome_config.get('password', '')
- print(f"🔧 Navidrome config - base_url: {navidrome_base_url}, username: {navidrome_username}")
+ print(f"Navidrome config - base_url: {navidrome_base_url}, username: {navidrome_username}")
if navidrome_base_url and navidrome_username and navidrome_password:
# Extract the path from URL
@@ -9611,10 +9611,10 @@ def fix_artist_image_url(thumb_url):
# Construct proper Navidrome Subsonic URL
fixed_url = f"{navidrome_base_url.rstrip('/')}{path}{separator}{auth_params}"
- print(f"🔧 Fixed URL: {fixed_url}")
+ print(f"Fixed URL: {fixed_url}")
return fixed_url
- print(f"⚠️ No configuration found for {active_server} or unsupported server type")
+ print(f"No configuration found for {active_server} or unsupported server type")
# Return original URL if no fixing needed/possible
return thumb_url
@@ -9687,7 +9687,7 @@ def get_library_artists():
})
except Exception as e:
- print(f"❌ Error fetching library artists: {e}")
+ print(f"Error fetching library artists: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -9716,7 +9716,7 @@ def test_artist_endpoint(artist_id):
def get_artist_detail(artist_id):
"""Get artist detail data"""
try:
- print(f"🎵 Getting artist detail for ID: {artist_id}")
+ print(f"Getting artist detail for ID: {artist_id}")
# Get database instance
database = get_database()
@@ -9725,7 +9725,7 @@ def get_artist_detail(artist_id):
db_result = database.get_artist_discography(artist_id)
if not db_result.get('success'):
- print(f"❌ Database returned error: {db_result}")
+ print(f"Database returned error: {db_result}")
return jsonify({
"success": False,
"error": db_result.get('error', 'Artist not found')
@@ -9734,18 +9734,18 @@ def get_artist_detail(artist_id):
artist_info = db_result['artist']
owned_releases = db_result['owned_releases']
- print(f"✅ Found artist: {artist_info['name']} with {len(owned_releases['albums'])} albums")
+ print(f"Found artist: {artist_info['name']} with {len(owned_releases['albums'])} albums")
# Fix artist image URL
- print(f"🖼️ Artist image before fix: '{artist_info.get('image_url')}'")
+ print(f"Artist image before fix: '{artist_info.get('image_url')}'")
if artist_info.get('image_url'):
artist_info['image_url'] = fix_artist_image_url(artist_info['image_url'])
- print(f"🖼️ Artist image after fix: '{artist_info['image_url']}'")
+ print(f"Artist image after fix: '{artist_info['image_url']}'")
else:
- print(f"🖼️ No artist image URL found for {artist_info['name']}")
+ print(f"No artist image URL found for {artist_info['name']}")
# Debug final artist data being sent
- print(f"🖼️ Final artist data being sent: {artist_info}")
+ print(f"Final artist data being sent: {artist_info}")
# Fix image URLs for all albums
for album in owned_releases['albums']:
@@ -9767,7 +9767,7 @@ def get_artist_detail(artist_id):
spotify_discography = get_spotify_artist_discography(artist_info['name'])
if spotify_discography['success']:
- print(f"🎵 Spotify discography found - Albums: {len(spotify_discography['albums'])}, EPs: {len(spotify_discography['eps'])}, Singles: {len(spotify_discography['singles'])}")
+ print(f"Spotify discography found - Albums: {len(spotify_discography['albums'])}, EPs: {len(spotify_discography['eps'])}, Singles: {len(spotify_discography['singles'])}")
# Store Spotify artist data for the response
spotify_artist_data = {
@@ -9779,11 +9779,11 @@ def get_artist_detail(artist_id):
# Merge owned and Spotify data for complete picture
merged_discography = merge_discography_data(owned_releases, spotify_discography, db=database, artist_name=artist_info['name'])
else:
- print(f"⚠️ Spotify discography not found: {spotify_discography.get('error', 'Unknown error')}")
+ print(f"Spotify discography not found: {spotify_discography.get('error', 'Unknown error')}")
# Fall back to our database categorization
merged_discography = owned_releases
except Exception as spotify_error:
- print(f"⚠️ Error fetching Spotify data: {spotify_error}")
+ print(f"Error fetching Spotify data: {spotify_error}")
# Fall back to our database categorization
merged_discography = owned_releases
@@ -9837,7 +9837,7 @@ def get_artist_detail(artist_id):
return jsonify(response_data)
except Exception as e:
- print(f"❌ Error in get_artist_detail: {e}")
+ print(f"Error in get_artist_detail: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -9897,7 +9897,7 @@ def get_similar_artists_stream(artist_name):
"""
def generate():
try:
- print(f"🎵 Streaming similar artists for: {artist_name}")
+ print(f"Streaming similar artists for: {artist_name}")
# Import required libraries
from bs4 import BeautifulSoup
@@ -9906,7 +9906,7 @@ def get_similar_artists_stream(artist_name):
url_artist = artist_name.lower().replace(' ', '+')
musicmap_url = f'https://www.music-map.com/{url_artist}'
- print(f"🌐 Fetching MusicMap: {musicmap_url}")
+ print(f"Fetching MusicMap: {musicmap_url}")
# Set headers to mimic a browser
headers = {
@@ -9941,7 +9941,7 @@ def get_similar_artists_stream(artist_name):
similar_artist_names.append(artist_text)
- print(f"📦 Found {len(similar_artist_names)} similar artists from MusicMap")
+ print(f"Found {len(similar_artist_names)} similar artists from MusicMap")
# Determine metadata source
use_hydrabase = _is_hydrabase_active()
@@ -9960,9 +9960,9 @@ def get_similar_artists_stream(artist_name):
searched_results = spotify_client.search_artists(artist_name, limit=1)
if searched_results and len(searched_results) > 0:
searched_artist_id = searched_results[0].id
- print(f"🎯 Searched artist ID: {searched_artist_id}")
+ print(f"Searched artist ID: {searched_artist_id}")
except Exception as e:
- print(f"⚠️ Could not get searched artist ID: {e}")
+ print(f"Could not get searched artist ID: {e}")
# Match each artist one by one and stream results
max_artists = 20
@@ -9971,7 +9971,7 @@ def get_similar_artists_stream(artist_name):
for artist_name_to_match in similar_artist_names[:max_artists]:
try:
- print(f"🔍 Matching: {artist_name_to_match}")
+ print(f"Matching: {artist_name_to_match}")
# Search for the artist via active metadata source
if use_hydrabase:
@@ -9984,12 +9984,12 @@ def get_similar_artists_stream(artist_name):
# Skip if this is the searched artist
if spotify_artist.id == searched_artist_id:
- print(f"⏭️ Skipping searched artist: {spotify_artist.name}")
+ print(f"Skipping searched artist: {spotify_artist.name}")
continue
# Skip if we've already seen this artist ID (deduplication)
if spotify_artist.id in seen_artist_ids:
- print(f"⏭️ Skipping duplicate artist: {spotify_artist.name}")
+ print(f"Skipping duplicate artist: {spotify_artist.name}")
continue
seen_artist_ids.add(spotify_artist.id)
@@ -10006,24 +10006,24 @@ def get_similar_artists_stream(artist_name):
yield f"data: {json.dumps({'artist': artist_data})}\n\n"
matched_count += 1
- print(f"✅ Matched and streamed: {spotify_artist.name}")
+ print(f"Matched and streamed: {spotify_artist.name}")
else:
- print(f"❌ No Spotify match found for: {artist_name_to_match}")
+ print(f"No Spotify match found for: {artist_name_to_match}")
except Exception as match_error:
- print(f"⚠️ Error matching {artist_name_to_match}: {match_error}")
+ print(f"Error matching {artist_name_to_match}: {match_error}")
continue
# Send completion message
yield f"data: {json.dumps({'complete': True, 'total': matched_count})}\n\n"
- print(f"✅ Streaming complete: {matched_count} artists matched")
+ print(f"Streaming complete: {matched_count} artists matched")
except requests.exceptions.RequestException as e:
- print(f"❌ Error fetching MusicMap: {e}")
+ print(f"Error fetching MusicMap: {e}")
yield f"data: {json.dumps({'error': f'Failed to fetch from MusicMap: {str(e)}'})}\n\n"
except Exception as e:
- print(f"❌ Error streaming similar artists: {e}")
+ print(f"Error streaming similar artists: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'error': str(e)})}\n\n"
@@ -10042,7 +10042,7 @@ def get_similar_artists(artist_name):
JSON with similar artists matched to Spotify data
"""
try:
- print(f"🎵 Getting similar artists for: {artist_name}")
+ print(f"Getting similar artists for: {artist_name}")
# Import required libraries
from bs4 import BeautifulSoup
@@ -10051,7 +10051,7 @@ def get_similar_artists(artist_name):
url_artist = artist_name.lower().replace(' ', '+')
musicmap_url = f'https://www.music-map.com/{url_artist}'
- print(f"🌐 Fetching MusicMap: {musicmap_url}")
+ print(f"Fetching MusicMap: {musicmap_url}")
# Set headers to mimic a browser
headers = {
@@ -10088,7 +10088,7 @@ def get_similar_artists(artist_name):
similar_artist_names.append(artist_text)
- print(f"📦 Found {len(similar_artist_names)} similar artists from MusicMap")
+ print(f"Found {len(similar_artist_names)} similar artists from MusicMap")
# Determine metadata source
use_hydrabase = _is_hydrabase_active()
@@ -10109,9 +10109,9 @@ def get_similar_artists(artist_name):
searched_results = spotify_client.search_artists(artist_name, limit=1)
if searched_results and len(searched_results) > 0:
searched_artist_id = searched_results[0].id
- print(f"🎯 Searched artist ID: {searched_artist_id}")
+ print(f"Searched artist ID: {searched_artist_id}")
except Exception as e:
- print(f"⚠️ Could not get searched artist ID: {e}")
+ print(f"Could not get searched artist ID: {e}")
# Match each artist (limit to first 20 for performance)
matched_artists = []
@@ -10120,7 +10120,7 @@ def get_similar_artists(artist_name):
for artist_name_to_match in similar_artist_names[:max_artists]:
try:
- print(f"🔍 Matching: {artist_name_to_match}")
+ print(f"Matching: {artist_name_to_match}")
# Search for the artist via active metadata source
if use_hydrabase:
@@ -10133,12 +10133,12 @@ def get_similar_artists(artist_name):
# Skip if this is the searched artist
if spotify_artist.id == searched_artist_id:
- print(f"⏭️ Skipping searched artist: {spotify_artist.name}")
+ print(f"Skipping searched artist: {spotify_artist.name}")
continue
# Skip if we've already seen this artist ID (deduplication)
if spotify_artist.id in seen_artist_ids:
- print(f"⏭️ Skipping duplicate artist: {spotify_artist.name}")
+ print(f"Skipping duplicate artist: {spotify_artist.name}")
continue
seen_artist_ids.add(spotify_artist.id)
@@ -10151,15 +10151,15 @@ def get_similar_artists(artist_name):
'popularity': spotify_artist.popularity if hasattr(spotify_artist, 'popularity') else 0
})
- print(f"✅ Matched: {spotify_artist.name}")
+ print(f"Matched: {spotify_artist.name}")
else:
- print(f"❌ No Spotify match found for: {artist_name_to_match}")
+ print(f"No Spotify match found for: {artist_name_to_match}")
except Exception as match_error:
- print(f"⚠️ Error matching {artist_name_to_match}: {match_error}")
+ print(f"Error matching {artist_name_to_match}: {match_error}")
continue
- print(f"✅ Successfully matched {len(matched_artists)} artists to Spotify")
+ print(f"Successfully matched {len(matched_artists)} artists to Spotify")
return jsonify({
"success": True,
@@ -10170,14 +10170,14 @@ def get_similar_artists(artist_name):
})
except requests.exceptions.RequestException as e:
- print(f"❌ Error fetching MusicMap: {e}")
+ print(f"Error fetching MusicMap: {e}")
return jsonify({
"success": False,
"error": f"Failed to fetch from MusicMap: {str(e)}"
}), 500
except Exception as e:
- print(f"❌ Error getting similar artists: {e}")
+ print(f"Error getting similar artists: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -10260,7 +10260,7 @@ def get_artist_discography(artist_id):
fallback_client = _get_metadata_fallback_client()
fallback_source = _get_metadata_fallback_source()
- print(f"🎤 Fetching discography for artist: {artist_id} (name: {artist_name}, spotify: {spotify_available}, source_override: {source_override or 'auto'})")
+ print(f"Fetching discography for artist: {artist_id} (name: {artist_name}, spotify: {spotify_available}, source_override: {source_override or 'auto'})")
albums = []
active_source = None
@@ -10327,7 +10327,7 @@ def get_artist_discography(artist_id):
active_source = source_override
if albums:
- print(f"📊 Got {len(albums)} albums from {active_source} (source override)")
+ print(f"Got {len(albums)} albums from {active_source} (source override)")
except Exception as e:
print(f"Source override ({source_override}) lookup failed: {e}")
@@ -10337,7 +10337,7 @@ def get_artist_discography(artist_id):
albums = hydrabase_client.search_discography(artist_name, limit=50)
if albums:
active_source = 'hydrabase'
- print(f"📊 Got {len(albums)} albums from Hydrabase")
+ print(f"Got {len(albums)} albums from Hydrabase")
except Exception as e:
print(f"Hydrabase discography failed: {e}")
@@ -10351,7 +10351,7 @@ def get_artist_discography(artist_id):
albums = spotify_client.get_artist_albums(artist_id, album_type='album,single')
if albums:
active_source = 'spotify'
- print(f"📊 Got {len(albums)} albums from Spotify")
+ print(f"Got {len(albums)} albums from Spotify")
except Exception as e:
print(f"Spotify lookup failed: {e}")
@@ -10363,10 +10363,10 @@ def get_artist_discography(artist_id):
albums = fallback_client.get_artist_albums(artist_id, album_type='album,single', limit=50)
if albums:
active_source = fallback_source
- print(f"📊 Got {len(albums)} albums from {fallback_source} (direct ID)")
+ print(f"Got {len(albums)} albums from {fallback_source} (direct ID)")
elif artist_name:
# Search fallback by name
- print(f"🔄 Trying {fallback_source} search by name: '{artist_name}'")
+ print(f"Trying {fallback_source} search by name: '{artist_name}'")
fallback_artists = fallback_client.search_artists(artist_name, limit=5)
if fallback_artists:
# Find best match
@@ -10378,15 +10378,15 @@ def get_artist_discography(artist_id):
if not best_match:
best_match = fallback_artists[0]
- print(f"✅ Found {fallback_source} artist: {best_match.name} (ID: {best_match.id})")
+ print(f"Found {fallback_source} artist: {best_match.name} (ID: {best_match.id})")
albums = fallback_client.get_artist_albums(best_match.id, album_type='album,single', limit=50)
if albums:
active_source = fallback_source
- print(f"📊 Got {len(albums)} albums from {fallback_source} (name search)")
+ print(f"Got {len(albums)} albums from {fallback_source} (name search)")
except Exception as e:
print(f"{fallback_source} lookup failed: {e}")
- print(f"📊 Total albums returned: {len(albums)} (source: {active_source})")
+ print(f"Total albums returned: {len(albums)} (source: {active_source})")
if not albums:
return jsonify({
@@ -10425,7 +10425,7 @@ def get_artist_discography(artist_id):
# Skip obvious compilation issues but be more lenient for now
if hasattr(album, 'album_type') and album.album_type == 'compilation':
- print(f"📀 Found compilation: '{album.name}' - including for now")
+ print(f"Found compilation: '{album.name}' - including for now")
# Categorize by album type
if hasattr(album, 'album_type'):
@@ -10450,13 +10450,13 @@ def get_artist_discography(artist_id):
album_list.sort(key=get_release_year, reverse=True)
singles_list.sort(key=get_release_year, reverse=True)
- print(f"✅ Found {len(album_list)} albums and {len(singles_list)} singles for artist {artist_id}")
+ print(f"Found {len(album_list)} albums and {len(singles_list)} singles for artist {artist_id}")
# Debug: Log the final album list
for album in album_list:
- print(f"📀 Album: {album['name']} ({album['album_type']}) - {album['release_date']}")
+ print(f"Album: {album['name']} ({album['album_type']}) - {album['release_date']}")
for single in singles_list:
- print(f"🎵 Single/EP: {single['name']} ({single['album_type']}) - {single['release_date']}")
+ print(f"Single/EP: {single['name']} ({single['album_type']}) - {single['release_date']}")
# Gather artist enrichment info from cache + library
artist_info = {}
@@ -10563,7 +10563,7 @@ def get_artist_discography(artist_id):
})
except Exception as e:
- print(f"❌ Error fetching artist discography: {e}")
+ print(f"Error fetching artist discography: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -10605,20 +10605,20 @@ def _resolve_db_album_id(album_id, artist_id=None):
album_title = row['title']
artist_name = row['artist_name']
query = f"{artist_name} {album_title}"
- print(f"🔍 Searching for album by name: '{query}'")
+ print(f"Searching for album by name: '{query}'")
results = spotify_client.search_albums(query, limit=5)
if results:
# Pick the best match (search already ranks by relevance)
for album in results:
if album.name.lower().strip() == album_title.lower().strip():
- print(f"✅ Found exact album match: {album.name} (ID: {album.id})")
+ print(f"Found exact album match: {album.name} (ID: {album.id})")
return album.id
# Fall back to first result if no exact title match
- print(f"⚠️ No exact match, using best result: {results[0].name} (ID: {results[0].id})")
+ print(f"No exact match, using best result: {results[0].name} (ID: {results[0].id})")
return results[0].id
except Exception as e:
- print(f"⚠️ Error resolving DB album ID {album_id}: {e}")
+ print(f"Error resolving DB album ID {album_id}: {e}")
return None
@@ -10658,7 +10658,7 @@ def get_artist_album_tracks(artist_id, album_id):
'uri': '',
'album': album_info
})
- print(f"✅ Hydrabase returned {len(formatted_tracks)} tracks for album: {album_info['name']}")
+ print(f"Hydrabase returned {len(formatted_tracks)} tracks for album: {album_info['name']}")
return jsonify({
'success': True,
'album': album_info,
@@ -10686,7 +10686,7 @@ def get_artist_album_tracks(artist_id, album_id):
elif source_override == 'discogs':
client = _get_discogs_client()
- print(f"🎵 Fetching tracks for album: {album_id} by artist: {artist_id} (source: {source_override or 'auto'})")
+ print(f"Fetching tracks for album: {album_id} by artist: {artist_id} (source: {source_override or 'auto'})")
# Get album information first
album_data = client.get_album(album_id)
@@ -10696,7 +10696,7 @@ def get_artist_album_tracks(artist_id, album_id):
if not album_data:
resolved_album_id = _resolve_db_album_id(album_id, artist_id)
if resolved_album_id and resolved_album_id != album_id:
- print(f"🔄 Resolved DB album ID {album_id} -> external ID {resolved_album_id}")
+ print(f"Resolved DB album ID {album_id} -> external ID {resolved_album_id}")
album_data = client.get_album(resolved_album_id)
if not album_data:
@@ -10750,7 +10750,7 @@ def get_artist_album_tracks(artist_id, album_id):
}
formatted_tracks.append(formatted_track)
- print(f"✅ Successfully formatted {len(formatted_tracks)} tracks for album: {album_info['name']}")
+ print(f"Successfully formatted {len(formatted_tracks)} tracks for album: {album_info['name']}")
return jsonify({
'success': True,
@@ -10759,7 +10759,7 @@ def get_artist_album_tracks(artist_id, album_id):
})
except Exception as e:
- print(f"❌ Error fetching album tracks: {e}")
+ print(f"Error fetching album tracks: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -10878,7 +10878,7 @@ def download_discography(artist_id):
total_added += added
total_skipped += skipped
- print(f"📥 [Discography] {album_name}: {added} added, {skipped} skipped")
+ print(f"[Discography] {album_name}: {added} added, {skipped} skipped")
yield json.dumps({
"album_id": album_id, "name": album_name, "status": "done",
"tracks_added": added, "tracks_skipped": skipped, "tracks_total": len(tracks)
@@ -10887,13 +10887,13 @@ def download_discography(artist_id):
except Exception as album_err:
yield json.dumps({"album_id": album_id, "status": "error", "message": str(album_err)}) + '\n'
- print(f"📥 [Discography] Complete for {artist_name}: {total_added} tracks added, {total_skipped} skipped across {len(album_ids)} albums")
+ print(f"[Discography] Complete for {artist_name}: {total_added} tracks added, {total_skipped} skipped across {len(album_ids)} albums")
yield json.dumps({"status": "complete", "total_added": total_added, "total_skipped": total_skipped, "total_albums": len(album_ids)}) + '\n'
return app.response_class(generate_ndjson(), mimetype='application/x-ndjson', headers={'X-Accel-Buffering': 'no'})
except Exception as e:
- print(f"❌ Error in download discography: {e}")
+ print(f"Error in download discography: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/artist//completion', methods=['POST'])
@@ -10918,7 +10918,7 @@ def check_artist_discography_completion(artist_id):
# If no artist name provided, try to infer it from the request
if artist_name == 'Unknown Artist':
- print(f"⚠️ No artist name provided in request, attempting to infer from discography data")
+ print(f"No artist name provided in request, attempting to infer from discography data")
# Try to extract from first album's title by using a simple search
all_items = discography.get('albums', []) + discography.get('singles', [])
if all_items and spotify_client and spotify_client.is_authenticated():
@@ -10928,12 +10928,12 @@ def check_artist_discography_completion(artist_id):
search_results = spotify_client.search_tracks(first_item.get('name', ''), limit=1)
if search_results and len(search_results) > 0:
artist_name = search_results[0].artists[0] if search_results[0].artists else "Unknown Artist"
- print(f"🎤 Inferred artist name from search: {artist_name}")
+ print(f"Inferred artist name from search: {artist_name}")
except Exception as e:
- print(f"⚠️ Could not infer artist name: {e}")
+ print(f"Could not infer artist name: {e}")
artist_name = "Unknown Artist"
- print(f"🎤 Checking completion for artist: {artist_name}")
+ print(f"Checking completion for artist: {artist_name}")
# Process albums
for album in discography.get('albums', []):
@@ -10951,7 +10951,7 @@ def check_artist_discography_completion(artist_id):
})
except Exception as e:
- print(f"❌ Error checking discography completion: {e}")
+ print(f"Error checking discography completion: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -10975,7 +10975,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
except Exception:
pass
- print(f"🔍 Checking album: '{album_name}' ({total_tracks} tracks)")
+ print(f"Checking album: '{album_name}' ({total_tracks} tracks)")
formats = []
if test_mode:
@@ -10985,7 +10985,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
expected_tracks = total_tracks
confidence = random.uniform(0.7, 1.0)
db_album = True # Simulate found album
- print(f"🧪 TEST MODE: Simulating {owned_tracks}/{expected_tracks} tracks for '{album_name}'")
+ print(f"TEST MODE: Simulating {owned_tracks}/{expected_tracks} tracks for '{album_name}'")
else:
# Check if album exists in database with completeness info
try:
@@ -10999,7 +10999,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
server_source=active_server # Check only the active server
)
except Exception as db_error:
- print(f"⚠️ Database error for album '{album_name}': {db_error}")
+ print(f"Database error for album '{album_name}': {db_error}")
# Return error state for this album
return {
"id": album_id,
@@ -11030,7 +11030,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
else:
status = "missing"
- print(f" 📊 Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
+ print(f" Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
return {
"id": album_id,
@@ -11045,7 +11045,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
}
except Exception as e:
- print(f"❌ Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
+ print(f"Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
return {
"id": album_data.get('id', ''),
"name": album_data.get('name', 'Unknown'),
@@ -11067,7 +11067,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
album_type = single_data.get('album_type', 'single')
formats = []
- print(f"🎵 Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
+ print(f"Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
if test_mode:
# Generate test data for singles/EPs
@@ -11076,12 +11076,12 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
owned_tracks = random.randint(0, total_tracks)
expected_tracks = total_tracks
confidence = random.uniform(0.7, 1.0)
- print(f"🧪 TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks")
+ print(f"TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks")
else:
owned_tracks = random.choice([0, 1]) # 50/50 chance
expected_tracks = 1
confidence = random.uniform(0.7, 1.0) if owned_tracks else 0.0
- print(f"🧪 TEST MODE: Single with {owned_tracks}/{expected_tracks} tracks")
+ print(f"TEST MODE: Single with {owned_tracks}/{expected_tracks} tracks")
elif album_type == 'ep' or total_tracks > 1:
# Treat EPs like albums
try:
@@ -11095,7 +11095,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
server_source=active_server # Check only the active server
)
except Exception as db_error:
- print(f"⚠️ Database error for EP '{single_name}': {db_error}")
+ print(f"Database error for EP '{single_name}': {db_error}")
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
# Calculate completion percentage
@@ -11112,7 +11112,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
else:
status = "missing"
- print(f" 📊 EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
+ print(f" EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
else:
# Single track - just check if the track exists
@@ -11123,7 +11123,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
confidence_threshold=0.7
)
except Exception as db_error:
- print(f"⚠️ Database error for single '{single_name}': {db_error}")
+ print(f"Database error for single '{single_name}': {db_error}")
db_track, confidence = None, 0.0
owned_tracks = 1 if db_track else 0
@@ -11141,7 +11141,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
elif ext:
formats = [ext]
- print(f" 🎵 Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
+ print(f" Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
return {
"id": single_id,
@@ -11157,7 +11157,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
}
except Exception as e:
- print(f"❌ Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
+ print(f"Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
return {
"id": single_data.get('id', ''),
"name": single_data.get('name', 'Unknown'),
@@ -11189,7 +11189,7 @@ def check_artist_discography_completion_stream(artist_id):
def generate_completion_stream():
try:
- print(f"🎤 Starting streaming completion check for artist: {artist_name}")
+ print(f"Starting streaming completion check for artist: {artist_name}")
# Get database instance
from database.music_database import MusicDatabase
@@ -11254,7 +11254,7 @@ def check_artist_discography_completion_stream(artist_id):
yield f"data: {json.dumps({'type': 'complete', 'processed_count': processed_count})}\n\n"
except Exception as e:
- print(f"❌ Error in streaming completion check: {e}")
+ print(f"Error in streaming completion check: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
@@ -11322,7 +11322,7 @@ def library_completion_stream():
yield f"data: {json.dumps({'type': 'complete', 'processed_count': len(all_items)})}\n\n"
except Exception as e:
- print(f"❌ Error in library completion stream: {e}")
+ print(f"Error in library completion stream: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
@@ -11430,7 +11430,7 @@ def library_check_tracks():
return jsonify({"success": True, "owned_tracks": owned_map})
except Exception as e:
- print(f"❌ Error checking track ownership: {e}")
+ print(f"Error checking track ownership: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -11639,7 +11639,7 @@ def enhance_artist_quality(artist_id):
'external_urls': {},
}
except Exception as e:
- print(f"⚠️ [Enhance] Spotify lookup failed for {spotify_tid}: {e}")
+ print(f"[Enhance] Spotify lookup failed for {spotify_tid}: {e}")
if not matched_track_data and spotify_client:
# Fallback: Spotify search matching — need full track data for wishlist
@@ -11713,7 +11713,7 @@ def enhance_artist_quality(artist_id):
'external_urls': best_match.external_urls or {},
}
except Exception as e:
- print(f"⚠️ [Enhance] Search match failed for {title}: {e}")
+ print(f"[Enhance] Search match failed for {title}: {e}")
# Fallback source when Spotify unavailable or no match found
if not matched_track_data:
@@ -11782,9 +11782,9 @@ def enhance_artist_quality(artist_id):
'preview_url': itunes_best.preview_url,
'external_urls': itunes_best.external_urls or {},
}
- print(f"🍎 [Enhance] Fallback match for {title}: {itunes_best.artists[0]} - {itunes_best.name} (conf: {itunes_best_conf:.3f})")
+ print(f"[Enhance] Fallback match for {title}: {itunes_best.artists[0]} - {itunes_best.name} (conf: {itunes_best_conf:.3f})")
except Exception as e:
- print(f"⚠️ [Enhance] Fallback source failed for {title}: {e}")
+ print(f"[Enhance] Fallback source failed for {title}: {e}")
if not matched_track_data:
failed_count += 1
@@ -11811,7 +11811,7 @@ def enhance_artist_quality(artist_id):
if success:
enhanced_count += 1
- print(f"✅ [Enhance] Queued for upgrade: {artist_name} - {title} ({tier_name})")
+ print(f"[Enhance] Queued for upgrade: {artist_name} - {title} ({tier_name})")
else:
failed_count += 1
failed_tracks.append({'track_id': track_id, 'title': title, 'reason': 'Wishlist add failed'})
@@ -11823,7 +11823,7 @@ def enhance_artist_quality(artist_id):
'failed_tracks': failed_tracks
})
except Exception as e:
- print(f"❌ [Enhance] Error: {e}")
+ print(f"[Enhance] Error: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -12741,9 +12741,9 @@ def reorganize_album_files(album_id):
if not os.path.exists(sidecar_dst):
try:
shutil.move(sidecar_src, sidecar_dst)
- print(f"📷 [Reorganize] Moved {sidecar} to {dest_dir}")
+ print(f"[Reorganize] Moved {sidecar} to {dest_dir}")
except Exception as sc_err:
- print(f"⚠️ [Reorganize] Failed to move {sidecar}: {sc_err}")
+ print(f"[Reorganize] Failed to move {sidecar}: {sc_err}")
# Clean up empty directories left behind (after sidecars moved)
for src_dir in moved_dirs:
@@ -13256,7 +13256,7 @@ def library_play_track():
else:
return jsonify({"success": False, "error": _get_file_not_found_error(file_path)}), 404
- print(f"📚 Library play request: {os.path.basename(file_path)}")
+ print(f"Library play request: {os.path.basename(file_path)}")
# Set stream state to ready with the library file path directly
with stream_lock:
@@ -13276,7 +13276,7 @@ def library_play_track():
return jsonify({"success": True, "message": "Library track ready for playback"})
except Exception as e:
- print(f"❌ Error playing library track: {e}")
+ print(f"Error playing library track: {e}")
return jsonify({"success": False, "error": str(e)}), 500
_enrichment_locks = {svc: threading.Lock() for svc in ('audiodb', 'deezer', 'musicbrainz', 'spotify', 'itunes', 'lastfm', 'genius', 'tidal', 'qobuz', 'discogs')}
@@ -13350,7 +13350,7 @@ def library_enrich_entity():
})
except Exception as e:
- print(f"❌ Error enriching entity: {e}")
+ print(f"Error enriching entity: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -13507,7 +13507,7 @@ def library_search_service():
return jsonify({"success": True, "results": results})
except Exception as e:
- print(f"❌ Error searching service: {e}")
+ print(f"Error searching service: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -13831,7 +13831,7 @@ def library_manual_match():
})
except Exception as e:
- print(f"❌ Error manual matching: {e}")
+ print(f"Error manual matching: {e}")
@app.route('/api/library/clear-match', methods=['PUT'])
def library_clear_match():
@@ -13889,7 +13889,7 @@ def library_clear_match():
})
except Exception as e:
- print(f"❌ Error clearing match: {e}")
+ print(f"Error clearing match: {e}")
return jsonify({"success": False, "error": str(e)}), 500
import traceback
@@ -13955,7 +13955,7 @@ def library_delete_track(track_id):
return jsonify({"success": True, "deleted_count": cursor.rowcount, "file_deleted": file_deleted, "blacklisted": blacklisted})
except Exception as e:
- print(f"❌ Error deleting track {track_id}: {e}")
+ print(f"Error deleting track {track_id}: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -14582,11 +14582,11 @@ def sync_artist_library(artist_id):
if new_name != artist_name:
cursor.execute("UPDATE artists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_name, db_artist_id))
- print(f"🔄 [Artist Sync] Name updated: '{artist_name}' → '{new_name}'")
+ print(f"[Artist Sync] Name updated: '{artist_name}' → '{new_name}'")
artist_name = new_name
name_updated = True
except Exception as e:
- print(f"⚠️ [Artist Sync] Could not refresh name from {server_source}: {e}")
+ print(f"[Artist Sync] Could not refresh name from {server_source}: {e}")
stale_tracks = []
valid_tracks = 0
@@ -14625,7 +14625,7 @@ def sync_artist_library(artist_id):
conn.commit()
- print(f"🔄 [Artist Sync] {artist_name}: {valid_tracks} valid, {len(stale_tracks)} stale removed, {empty_albums_removed} empty albums cleaned")
+ print(f"[Artist Sync] {artist_name}: {valid_tracks} valid, {len(stale_tracks)} stale removed, {empty_albums_removed} empty albums cleaned")
return jsonify({
"success": True,
@@ -14637,7 +14637,7 @@ def sync_artist_library(artist_id):
})
except Exception as e:
- print(f"❌ Error syncing artist {artist_id}: {e}")
+ print(f"Error syncing artist {artist_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/library/album/', methods=['DELETE'])
@@ -14658,7 +14658,7 @@ def library_delete_album(album_id):
conn.commit()
return jsonify({"success": True, "deleted_count": 1, "tracks_deleted": tracks_deleted})
except Exception as e:
- print(f"❌ Error deleting album {album_id}: {e}")
+ print(f"Error deleting album {album_id}: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -14683,7 +14683,7 @@ def library_delete_tracks_batch():
conn.commit()
return jsonify({"success": True, "deleted_count": cursor.rowcount})
except Exception as e:
- print(f"❌ Error batch deleting tracks: {e}")
+ print(f"Error batch deleting tracks: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -14731,7 +14731,7 @@ def stream_start():
if not data:
return jsonify({"success": False, "error": "No track data provided"}), 400
- print(f"🎵 Web UI Stream request for: {data.get('filename')}")
+ print(f"Web UI Stream request for: {data.get('filename')}")
try:
# Stop any existing streaming task
@@ -14755,7 +14755,7 @@ def stream_start():
return jsonify({"success": True, "message": "Streaming started"})
except Exception as e:
- print(f"❌ Error starting stream: {e}")
+ print(f"Error starting stream: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/stream/status')
@@ -14771,7 +14771,7 @@ def stream_status():
"error_message": stream_state["error_message"]
})
except Exception as e:
- print(f"❌ Error getting stream status: {e}")
+ print(f"Error getting stream status: {e}")
return jsonify({
"status": "error",
"progress": 0,
@@ -14792,7 +14792,7 @@ def stream_audio():
if not os.path.exists(file_path):
return jsonify({"error": "Audio file not found"}), 404
- print(f"🎵 Serving audio file: {os.path.basename(file_path)}")
+ print(f"Serving audio file: {os.path.basename(file_path)}")
# Determine MIME type based on file extension
file_ext = os.path.splitext(file_path)[1].lower()
@@ -14874,7 +14874,7 @@ def stream_audio():
return response
except Exception as e:
- print(f"❌ Error serving audio file: {e}")
+ print(f"Error serving audio file: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/stream/stop', methods=['POST'])
@@ -14900,9 +14900,9 @@ def stream_stop():
file_path = os.path.join(stream_folder, filename)
if os.path.isfile(file_path):
os.remove(file_path)
- print(f"🗑️ Removed stream file: {filename}")
+ print(f"Removed stream file: {filename}")
else:
- print(f"📚 Library playback stopped - skipping file deletion")
+ print(f"Library playback stopped - skipping file deletion")
# Reset stream state
with stream_lock:
@@ -14918,7 +14918,7 @@ def stream_stop():
return jsonify({"success": True, "message": "Stream stopped"})
except Exception as e:
- print(f"❌ Error stopping stream: {e}")
+ print(f"Error stopping stream: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# --- Matched Downloads API Endpoints ---
@@ -14932,12 +14932,12 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non
return []
try:
- print(f"🔍 Generating artist suggestions for: {search_result.get('artist', '')} - {search_result.get('title', '')}")
+ print(f"Generating artist suggestions for: {search_result.get('artist', '')} - {search_result.get('title', '')}")
suggestions = []
# Special handling for albums - use album title to find artist
if is_album and album_result and album_result.get('album_title'):
- print(f"🎵 Album mode detected - using album title for artist search")
+ print(f"Album mode detected - using album title for artist search")
album_title = album_result.get('album_title', '')
# Clean album title (remove year prefixes like "(2005)")
@@ -14947,7 +14947,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non
# Search tracks using album title to find the artist
tracks = spotify_client.search_tracks(clean_album_title, limit=10)
- print(f"📊 Found {len(tracks)} tracks from album search")
+ print(f"Found {len(tracks)} tracks from album search")
# Collect unique artists and their associated tracks/albums
unique_artists = {} # artist_name -> list of (track, album) tuples
@@ -14967,7 +14967,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non
if matches:
return artist_name, matches[0]
except Exception as e:
- print(f"⚠️ Error fetching artist '{artist_name}': {e}")
+ print(f"Error fetching artist '{artist_name}': {e}")
return artist_name, None
# Use limited concurrency to respect rate limits
@@ -15018,7 +15018,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non
if not search_artist:
return []
- print(f"🎵 Single track mode - searching for artist: '{search_artist}'")
+ print(f"Single track mode - searching for artist: '{search_artist}'")
# Search for artists directly
artist_matches = spotify_client.search_artists(search_artist, limit=10)
@@ -15046,7 +15046,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non
return suggestions[:4]
except Exception as e:
- print(f"❌ Error generating artist suggestions: {e}")
+ print(f"Error generating artist suggestions: {e}")
return []
def _generate_album_suggestions(selected_artist, search_result):
@@ -15058,12 +15058,12 @@ def _generate_album_suggestions(selected_artist, search_result):
return []
try:
- print(f"🔍 Generating album suggestions for artist: {selected_artist['name']}")
+ print(f"Generating album suggestions for artist: {selected_artist['name']}")
# Determine target album name from search result
target_album_name = search_result.get('album', '') or search_result.get('album_title', '')
if not target_album_name:
- print("⚠️ No album name found in search result")
+ print("No album name found in search result")
return []
# Clean target album name
@@ -15073,7 +15073,7 @@ def _generate_album_suggestions(selected_artist, search_result):
# Get artist's albums from Spotify
artist_albums = spotify_client.get_artist_albums(selected_artist['id'])
- print(f"📊 Found {len(artist_albums)} albums for artist")
+ print(f"Found {len(artist_albums)} albums for artist")
album_matches = []
for album in artist_albums:
@@ -15100,7 +15100,7 @@ def _generate_album_suggestions(selected_artist, search_result):
return album_matches[:4]
except Exception as e:
- print(f"❌ Error generating album suggestions: {e}")
+ print(f"Error generating album suggestions: {e}")
return []
@app.route('/api/match/suggestions', methods=['POST'])
@@ -15124,7 +15124,7 @@ def get_match_suggestions():
return jsonify({"suggestions": suggestions})
except Exception as e:
- print(f"❌ Error in match suggestions: {e}")
+ print(f"Error in match suggestions: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/match/search', methods=['POST'])
@@ -15220,7 +15220,7 @@ def search_match():
return jsonify({"error": "Invalid context. Must be 'artist' or 'album'"}), 400
except Exception as e:
- print(f"❌ Error in match search: {e}")
+ print(f"Error in match search: {e}")
return jsonify({"error": str(e)}), 500
@@ -15247,7 +15247,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
Download album tracks that have been matched to Spotify with full track metadata.
This provides the best possible metadata enhancement and organization.
"""
- logger.info(f"🎯 Processing enhanced album download for '{spotify_album['name']}' with {len(enhanced_tracks)} matched tracks")
+ logger.info(f"Processing enhanced album download for '{spotify_album['name']}' with {len(enhanced_tracks)} matched tracks")
# Compute total_discs for multi-disc album subfolder support
total_discs = max((t['spotify_track'].get('disc_number', 1) for t in enhanced_tracks), default=1)
@@ -15270,10 +15270,10 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
_mb_release_cache[(spotify_album['name'].lower().strip(), _pf_artist_key)] = _pf_mbid
with _mb_release_detail_cache_lock:
_mb_release_detail_cache[_pf_mbid] = _pf_release
- print(f"🏷️ [Preflight] Pre-cached MB release for '{spotify_album['name']}': "
+ print(f"[Preflight] Pre-cached MB release for '{spotify_album['name']}': "
f"'{_pf_release.get('title', '')}' ({_pf_mbid[:8]}...)")
except Exception as pf_err:
- print(f"⚠️ [Preflight] MB release preflight failed: {pf_err}")
+ print(f"[Preflight] MB release preflight failed: {pf_err}")
# Process matched tracks with full Spotify metadata
for matched_item in enhanced_tracks:
@@ -15282,7 +15282,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
spotify_track = matched_item['spotify_track']
if _is_explicit_blocked(spotify_track):
- logger.info(f"🚫 [Content Filter] Skipping explicit track: '{spotify_track.get('name')}'")
+ logger.info(f"[Content Filter] Skipping explicit track: '{spotify_track.get('name')}'")
continue
username = slskd_track.get('username')
@@ -15319,7 +15319,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
"has_full_spotify_metadata": True # Flag for robust processing
}
- logger.info(f"✅ Queued matched track: '{spotify_track['name']}' (track #{spotify_track['track_number']})")
+ logger.info(f"Queued matched track: '{spotify_track['name']}' (track #{spotify_track['track_number']})")
started_count += 1
else:
logger.error(f"Failed to queue track: {filename}")
@@ -15356,7 +15356,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
'track_info': None
}
- logger.info(f"⚠️ Queued unmatched track (basic cleanup): {filename}")
+ logger.info(f"Queued unmatched track (basic cleanup): {filename}")
started_count += 1
except Exception as e:
@@ -15372,18 +15372,18 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
match and correct the metadata for each individual track before downloading,
ensuring perfect tagging and naming.
"""
- print(f"🎵 Processing matched album download for '{spotify_album['name']}' with {len(album_result.get('tracks', []))} tracks.")
+ print(f"Processing matched album download for '{spotify_album['name']}' with {len(album_result.get('tracks', []))} tracks.")
tracks_to_download = album_result.get('tracks', [])
if not tracks_to_download:
- print("⚠️ Album result contained no tracks. Aborting.")
+ print("Album result contained no tracks. Aborting.")
return 0
# --- THIS IS THE NEW LOGIC ---
# Fetch the official tracklist from Spotify ONCE for the entire album.
official_spotify_tracks = _get_spotify_album_tracks(spotify_album)
if not official_spotify_tracks:
- print("⚠️ Could not fetch official tracklist from Spotify. Metadata may be inaccurate.")
+ print("Could not fetch official tracklist from Spotify. Metadata may be inaccurate.")
# --- END OF NEW LOGIC ---
# Compute total_discs for multi-disc album subfolder support
@@ -15407,10 +15407,10 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
_mb_release_cache[(spotify_album['name'].lower().strip(), _pf_artist_key)] = _pf_mbid
with _mb_release_detail_cache_lock:
_mb_release_detail_cache[_pf_mbid] = _pf_release
- print(f"🏷️ [Preflight] Pre-cached MB release for '{spotify_album['name']}': "
+ print(f"[Preflight] Pre-cached MB release for '{spotify_album['name']}': "
f"'{_pf_release.get('title', '')}' ({_pf_mbid[:8]}...)")
except Exception as pf_err:
- print(f"⚠️ [Preflight] MB release preflight failed: {pf_err}")
+ print(f"[Preflight] MB release preflight failed: {pf_err}")
started_count = 0
for track_data in tracks_to_download:
@@ -15431,7 +15431,7 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
# --- END OF CRITICAL STEP ---
if _is_explicit_blocked(corrected_meta):
- print(f"🚫 [Content Filter] Skipping explicit track: '{corrected_meta.get('title')}'")
+ print(f"[Content Filter] Skipping explicit track: '{corrected_meta.get('title')}'")
continue
# Create a clean context object using the CORRECTED metadata
@@ -15467,7 +15467,7 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
print(f" - Failed to queue track: {filename}")
except Exception as e:
- print(f"❌ Error processing track in album batch: {track_data.get('filename')}. Error: {e}")
+ print(f"Error processing track in album batch: {track_data.get('filename')}. Error: {e}")
continue
return started_count
@@ -15503,7 +15503,7 @@ def start_matched_download():
if is_single_track and spotify_track:
if _is_explicit_blocked(spotify_track):
return jsonify({"success": False, "error": "Explicit content is disabled in settings", "explicit_blocked": True}), 403
- logger.info(f"🎯 Starting enhanced single track download: '{spotify_track['name']}' by {spotify_artist['name']}")
+ logger.info(f"Starting enhanced single track download: '{spotify_track['name']}' by {spotify_artist['name']}")
username = download_payload.get('username')
filename = download_payload.get('filename')
@@ -15536,14 +15536,14 @@ def start_matched_download():
"has_full_spotify_metadata": True # Flag for robust processing
}
- logger.info(f"✅ Queued enhanced single track: '{spotify_track['name']}'")
+ logger.info(f"Queued enhanced single track: '{spotify_track['name']}'")
return jsonify({"success": True, "message": "Enhanced single track download started"})
else:
return jsonify({"success": False, "error": "Failed to start download via slskd"}), 500
# NEW: Enhanced album download with track-to-track matching
if enhanced_tracks:
- logger.info(f"🎯 Starting enhanced album download: {len(enhanced_tracks)} matched tracks, {len(unmatched_tracks)} unmatched")
+ logger.info(f"Starting enhanced album download: {len(enhanced_tracks)} matched tracks, {len(unmatched_tracks)} unmatched")
started_count = _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_artist, spotify_album)
if started_count > 0:
return jsonify({"success": True, "message": f"Queued {started_count} tracks with full Spotify metadata."})
@@ -15672,7 +15672,7 @@ def _parse_filename_metadata(filename: str) -> dict:
cleaned_album = re.sub(r'^\d{4}\s*-\s*', '', potential_album).strip()
metadata['album'] = cleaned_album
- print(f"🧠 Parsed Filename '{base_name}': Artist='{metadata['artist']}', Title='{metadata['title']}', Album='{metadata['album']}', Track#='{metadata['track_number']}'")
+ print(f"Parsed Filename '{base_name}': Artist='{metadata['artist']}', Title='{metadata['title']}', Album='{metadata['album']}', Track#='{metadata['track_number']}'")
return metadata
@@ -15842,7 +15842,7 @@ def _search_track_in_album_context(original_search: dict, artist: dict) -> dict:
matching_engine.normalize_string(track_data['name'])
)
if similarity > 0.7:
- print(f"✅ Found track in album context: '{track_data['name']}'")
+ print(f"Found track in album context: '{track_data['name']}'")
return {
'is_album': True,
'album_name': spotify_album.name,
@@ -15852,7 +15852,7 @@ def _search_track_in_album_context(original_search: dict, artist: dict) -> dict:
}
return None
except Exception as e:
- print(f"❌ Error in _search_track_in_album_context: {e}")
+ print(f"Error in _search_track_in_album_context: {e}")
return None
@@ -15866,8 +15866,8 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
try:
# Log available data for debugging (GUI PARITY)
original_search = context.get("original_search_result", {})
- print(f"\n🔍 [Album Detection] Starting for track: '{original_search.get('title', 'Unknown')}'")
- print(f"📊 [Data Available]:")
+ print(f"\n[Album Detection] Starting for track: '{original_search.get('title', 'Unknown')}'")
+ print(f"[Data Available]:")
print(f" - Clean Spotify title: '{original_search.get('spotify_clean_title', 'None')}'")
print(f" - Clean Spotify album: '{original_search.get('spotify_clean_album', 'None')}'")
print(f" - Filename album: '{original_search.get('album', 'None')}'")
@@ -15878,7 +15878,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
is_album_download = context.get("is_album_download", False)
artist_name = artist['name']
- print(f"🔍 Album detection for '{original_search.get('title', 'Unknown')}' by '{artist_name}':")
+ print(f"Album detection for '{original_search.get('title', 'Unknown')}' by '{artist_name}':")
print(f" Has album attr: {bool(original_search.get('album'))}")
if original_search.get('album'):
print(f" Album value: '{original_search.get('album')}'")
@@ -15887,7 +15887,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
# If this is part of a matched album download, we TRUST the context data completely.
# This is the exact logic from downloads.py.
if is_album_download and spotify_album_context:
- print("✅ Matched Album context found. Prioritizing pre-matched Spotify data.")
+ print("Matched Album context found. Prioritizing pre-matched Spotify data.")
# We exclusively use the track number and title that were matched
# *before* the download started. We do not try to re-parse the filename.
@@ -15921,7 +15921,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
if album_name_to_use:
track_title = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown')
- print(f"🎯 ALBUM-AWARE SEARCH ({album_source}): Looking for '{track_title}' in album '{album_name_to_use}'")
+ print(f"ALBUM-AWARE SEARCH ({album_source}): Looking for '{track_title}' in album '{album_name_to_use}'")
# Temporarily set the album for the search
original_album = original_search.get('album')
@@ -15930,10 +15930,10 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
try:
album_result = _search_track_in_album_context_web(context, artist)
if album_result:
- print(f"✅ PRIORITY 1 SUCCESS: Found track using {album_source} album name - FORCING album classification")
+ print(f"PRIORITY 1 SUCCESS: Found track using {album_source} album name - FORCING album classification")
return album_result
else:
- print(f"⚠️ PRIORITY 1 FAILED: Track not found using {album_source} album name")
+ print(f"PRIORITY 1 FAILED: Track not found using {album_source} album name")
finally:
# Restore original album value
if original_album is not None:
@@ -15942,13 +15942,13 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
original_search.pop('album', None)
# PRIORITY 2: Fallback to individual track search for clean metadata
- print(f"🔍 Searching Spotify for individual track info (PRIORITY 2)...")
+ print(f"Searching Spotify for individual track info (PRIORITY 2)...")
# Clean the track title before searching - remove artist prefix
# Prioritize clean Spotify title over filename-parsed title
track_title_to_use = original_search.get('spotify_clean_title') or original_search.get('title', '')
clean_title = _clean_track_title_web(track_title_to_use, artist_name)
- print(f"🧹 Cleaned title: '{track_title_to_use}' -> '{clean_title}'")
+ print(f"Cleaned title: '{track_title_to_use}' -> '{clean_title}'")
# Search for the track by artist and cleaned title
query = f"artist:{artist_name} track:{clean_title}"
@@ -15987,25 +15987,25 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
# If we found a good Spotify match, use it for clean metadata
if best_match and best_confidence > 0.75:
- print(f"✅ Found matching Spotify track: '{best_match.name}' - Album: '{best_match.album}' (confidence: {best_confidence:.2f})")
+ print(f"Found matching Spotify track: '{best_match.name}' - Album: '{best_match.album}' (confidence: {best_confidence:.2f})")
# Get detailed track information using Spotify's track API
detailed_track = None
if hasattr(best_match, 'id') and best_match.id:
- print(f"🔍 Getting detailed track info from Spotify API for track ID: {best_match.id}")
+ print(f"Getting detailed track info from Spotify API for track ID: {best_match.id}")
detailed_track = spotify_client.get_track_details(best_match.id)
# Use detailed track data if available
if detailed_track:
- print(f"✅ Got detailed track data from Spotify API")
+ print(f"Got detailed track data from Spotify API")
album_name = _clean_album_title_web(detailed_track['album']['name'], artist_name)
clean_track_name = detailed_track['name'] # Use Spotify's clean track name
album_type = detailed_track['album'].get('album_type', 'album')
total_tracks = detailed_track['album'].get('total_tracks', 1)
spotify_track_number = detailed_track.get('track_number', 1)
- print(f"📀 Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})")
- print(f"🎵 Clean track name from Spotify: '{clean_track_name}'")
+ print(f"Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})")
+ print(f"Clean track name from Spotify: '{clean_track_name}'")
# Enhanced album detection using detailed API data (GUI PARITY)
is_album = (
@@ -16023,7 +16023,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
if detailed_track['album'].get('images'):
album_image_url = detailed_track['album']['images'][0].get('url')
- print(f"📊 Album classification: {is_album} (type={album_type}, tracks={total_tracks})")
+ print(f"Album classification: {is_album} (type={album_type}, tracks={total_tracks})")
return {
'is_album': is_album,
@@ -16036,7 +16036,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
}
# Fallback: Use original data with basic cleaning
- print("⚠️ No good Spotify match found, using original data")
+ print("No good Spotify match found, using original data")
fallback_title = _clean_track_title_web(original_search.get('title', 'Unknown Track'), artist_name)
# Preserve track_number from context if available (playlist sync tracks have it)
@@ -16054,7 +16054,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
}
except Exception as e:
- print(f"❌ Error in _detect_album_info_web: {e}")
+ print(f"Error in _detect_album_info_web: {e}")
clean_title = _clean_track_title_web(context.get("original_search_result", {}).get('title', 'Unknown'), artist.get('name', ''))
_err_tn = (context.get("original_search_result", {}).get('track_number')
or context.get('track_info', {}).get('track_number')
@@ -16120,10 +16120,10 @@ def _sweep_empty_download_directories():
pass # Directory not actually empty or locked — skip silently
if removed > 0:
- print(f"🧹 [Folder Cleanup] Removed {removed} empty director{'y' if removed == 1 else 'ies'} from downloads folder")
+ print(f"[Folder Cleanup] Removed {removed} empty director{'y' if removed == 1 else 'ies'} from downloads folder")
return removed
except Exception as e:
- print(f"⚠️ [Folder Cleanup] Error sweeping empty directories: {e}")
+ print(f"[Folder Cleanup] Error sweeping empty directories: {e}")
return 0
@@ -16183,7 +16183,7 @@ def _detect_deluxe_edition(album_name: str) -> bool:
for indicator in deluxe_indicators:
if indicator in album_lower:
- print(f"🎯 Detected deluxe edition: '{album_name}' contains '{indicator}'")
+ print(f"Detected deluxe edition: '{album_name}' contains '{indicator}'")
return True
return False
@@ -16206,7 +16206,7 @@ def _normalize_base_album_name(base_album: str, artist_name: str) -> str:
# Check for exact matches in our corrections
for variant, correction in known_corrections.items():
if normalized_lower == variant.lower():
- print(f"📀 Album correction applied: '{base_album}' -> '{correction}'")
+ print(f"Album correction applied: '{base_album}' -> '{correction}'")
return correction
# Handle punctuation variations
@@ -16217,7 +16217,7 @@ def _normalize_base_album_name(base_album: str, artist_name: str) -> str:
normalized = re.sub(r'\s+', ' ', normalized) # Clean multiple spaces
normalized = normalized.strip()
- print(f"📀 Album variant normalization: '{base_album}' -> '{normalized}'")
+ print(f"Album variant normalization: '{base_album}' -> '{normalized}'")
return normalized
def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: str = None) -> str:
@@ -16250,10 +16250,10 @@ def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album:
# Check if we already have a cached result for this album
if album_key in album_name_cache:
cached_name = album_name_cache[album_key]
- print(f"🔍 Using cached album name for '{album_key}': '{cached_name}'")
+ print(f"Using cached album name for '{album_key}': '{cached_name}'")
return cached_name
- print(f"🔍 Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
+ print(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
# Check if this track indicates a deluxe edition
is_deluxe_track = False
@@ -16267,7 +16267,7 @@ def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album:
# SMART ALGORITHM: Upgrade to deluxe if this track is deluxe
if is_deluxe_track and current_edition == "standard":
- print(f"🎯 UPGRADE: Album '{base_album}' upgraded from standard to deluxe!")
+ print(f"UPGRADE: Album '{base_album}' upgraded from standard to deluxe!")
album_editions[album_key] = "deluxe"
current_edition = "deluxe"
@@ -16282,12 +16282,12 @@ def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album:
album_name_cache[album_key] = final_album_name
album_artists[album_key] = artist_name
- print(f"🔗 Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})")
+ print(f"Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})")
return final_album_name
except Exception as e:
- print(f"❌ Error resolving album group: {e}")
+ print(f"Error resolving album group: {e}")
return album_info.get('album_name', 'Unknown Album')
def _clean_album_title_web(album_title: str, artist_name: str) -> str:
@@ -16297,7 +16297,7 @@ def _clean_album_title_web(album_title: str, artist_name: str) -> str:
# Start with the original title
original = album_title.strip()
cleaned = original
- print(f"🧹 Album Title Cleaning: '{original}' (artist: '{artist_name}')")
+ print(f"Album Title Cleaning: '{original}' (artist: '{artist_name}')")
# Remove "Album - " prefix
cleaned = re.sub(r'^Album\s*-\s*', '', cleaned, flags=re.IGNORECASE)
@@ -16336,7 +16336,7 @@ def _clean_album_title_web(album_title: str, artist_name: str) -> str:
# Remove leading/trailing punctuation
cleaned = re.sub(r'^[-\s]+|[-\s]+$', '', cleaned)
- print(f"🧹 Album Title Result: '{original}' -> '{cleaned}'")
+ print(f"Album Title Result: '{original}' -> '{cleaned}'")
return cleaned if cleaned else original
def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> dict:
@@ -16355,10 +16355,10 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d
artist_name = spotify_artist["name"]
if not album_name or not track_title:
- print(f"❌ Album-aware search failed: Missing album ({album_name}) or track ({track_title})")
+ print(f"Album-aware search failed: Missing album ({album_name}) or track ({track_title})")
return None
- print(f"🎯 Album-aware search: '{track_title}' in album '{album_name}' by '{artist_name}'")
+ print(f"Album-aware search: '{track_title}' in album '{album_name}' by '{artist_name}'")
# Clean the album name for better search results
clean_album = _clean_album_title_web(album_name, artist_name)
@@ -16366,21 +16366,21 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d
# Search for the specific album first
album_query = f"album:{clean_album} artist:{artist_name}"
- print(f"🔍 Searching albums: {album_query}")
+ print(f"Searching albums: {album_query}")
albums = spotify_client.search_albums(album_query, limit=5)
if not albums:
- print(f"❌ No albums found for query: {album_query}")
+ print(f"No albums found for query: {album_query}")
return None
# Check each album to see if our track is in it
for album in albums:
- print(f"🎵 Checking album: '{album.name}' ({album.total_tracks} tracks)")
+ print(f"Checking album: '{album.name}' ({album.total_tracks} tracks)")
# Get tracks from this album
album_tracks_data = spotify_client.get_album_tracks(album.id)
if not album_tracks_data or 'items' not in album_tracks_data:
- print(f"❌ Could not get tracks for album: {album.name}")
+ print(f"Could not get tracks for album: {album.name}")
continue
# Check if our track is in this album
@@ -16399,7 +16399,7 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d
threshold = 0.9 if is_remix else 0.65 # Lower threshold to favor album matches over singles
if similarity > threshold:
- print(f"✅ FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})")
+ print(f"FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})")
# Classify as album vs single using same logic as _detect_album_info_web
ctx_album_type = getattr(album, 'album_type', 'album') or 'album'
@@ -16410,7 +16410,7 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d
matching_engine.normalize_string(album.name) != matching_engine.normalize_string(clean_track) and
matching_engine.normalize_string(album.name) != matching_engine.normalize_string(artist_name)
)
- print(f"📊 Album context classification: is_album={ctx_is_album} (type={ctx_album_type}, tracks={ctx_total_tracks})")
+ print(f"Album context classification: is_album={ctx_is_album} (type={ctx_album_type}, tracks={ctx_total_tracks})")
return {
'is_album': ctx_is_album,
@@ -16422,13 +16422,13 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d
'source': 'album_context_search'
}
- print(f"❌ Track '{clean_track}' not found in album '{album.name}'")
+ print(f"Track '{clean_track}' not found in album '{album.name}'")
- print(f"❌ Track '{clean_track}' not found in any matching albums")
+ print(f"Track '{clean_track}' not found in any matching albums")
return None
except Exception as e:
- print(f"❌ Error in album-aware search: {e}")
+ print(f"Error in album-aware search: {e}")
return None
def _clean_track_title_web(track_title: str, artist_name: str) -> str:
@@ -16438,7 +16438,7 @@ def _clean_track_title_web(track_title: str, artist_name: str) -> str:
# Start with the original title
original = track_title.strip()
cleaned = original
- print(f"🧹 Track Title Cleaning: '{original}' (artist: '{artist_name}')")
+ print(f"Track Title Cleaning: '{original}' (artist: '{artist_name}')")
# Remove artist name prefix if it appears at the beginning
# This handles cases like "Kendrick Lamar - HUMBLE."
@@ -16467,7 +16467,7 @@ def _clean_track_title_web(track_title: str, artist_name: str) -> str:
# Remove leading/trailing punctuation
cleaned = re.sub(r'^[-\s]+|[-\s]+$', '', cleaned)
- print(f"🧹 Track Title Result: '{original}' -> '{cleaned}'")
+ print(f"Track Title Result: '{original}' -> '{cleaned}'")
return cleaned if cleaned else original
@@ -16503,7 +16503,7 @@ def clean_youtube_track_title(title, artist_name=None):
cleaned_title = re.sub(artist_pattern, '', title, flags=re.IGNORECASE).strip()
if cleaned_title != title:
- print(f"🎯 Removed artist from start: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')")
+ print(f"Removed artist from start: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')")
title = cleaned_title
artist_removed = True
else:
@@ -16513,7 +16513,7 @@ def clean_youtube_track_title(title, artist_name=None):
cleaned_title = re.sub(artist_end_pattern, '', title, flags=re.IGNORECASE).strip()
if cleaned_title != title:
- print(f"🎯 Removed artist from end: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')")
+ print(f"Removed artist from end: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')")
title = cleaned_title
artist_removed = True
@@ -16575,7 +16575,7 @@ def clean_youtube_track_title(title, artist_name=None):
title = re.sub(rf'\b{re.escape(artist_name)}\s*[-–—:]\s*', '', title, flags=re.IGNORECASE)
title = re.sub(rf'^{re.escape(artist_name)}\s*[-–—:]\s*', '', title, flags=re.IGNORECASE)
else:
- print(f"⚠️ Skipping artist removal - collaboration detected: '{title}'")
+ print(f"Skipping artist removal - collaboration detected: '{title}'")
# Remove "prod. Producer" patterns
title = re.sub(r'\s+prod\.?\s+\S+', '', title, flags=re.IGNORECASE)
@@ -16603,7 +16603,7 @@ def clean_youtube_track_title(title, artist_name=None):
title = original_title
if title != original_title:
- print(f"🧹 YouTube title cleaned: '{original_title}' → '{title}'")
+ print(f"YouTube title cleaned: '{original_title}' → '{title}'")
return title
@@ -16668,7 +16668,7 @@ def clean_youtube_artist(artist_string):
artist_string = original_artist
if artist_string != original_artist:
- print(f"🧹 YouTube artist cleaned: '{original_artist}' → '{artist_string}'")
+ print(f"YouTube artist cleaned: '{original_artist}' → '{artist_string}'")
return artist_string
@@ -16695,14 +16695,14 @@ def parse_youtube_playlist(url):
playlist_info = ydl.extract_info(url, download=False)
if not playlist_info:
- print("❌ Could not extract playlist information")
+ print("Could not extract playlist information")
return None
playlist_name = playlist_info.get('title', 'Unknown Playlist')
playlist_id = playlist_info.get('id', 'unknown_id')
entries = list(playlist_info.get('entries', []) or [])
- print(f"🎵 Found YouTube playlist: '{playlist_name}' with {len(entries)} entries")
+ print(f"Found YouTube playlist: '{playlist_name}' with {len(entries)} entries")
for entry in entries:
if not entry:
@@ -16741,11 +16741,11 @@ def parse_youtube_playlist(url):
'source': 'youtube'
}
- print(f"✅ Successfully parsed YouTube playlist: {len(tracks)} tracks extracted")
+ print(f"Successfully parsed YouTube playlist: {len(tracks)} tracks extracted")
return playlist_data
except Exception as e:
- print(f"❌ Error parsing YouTube playlist: {e}")
+ print(f"Error parsing YouTube playlist: {e}")
return None
@@ -16825,7 +16825,7 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
original_stem = os.path.splitext(os.path.basename(original_path))[0]
final_path = os.path.join(original_dir, original_stem + file_ext)
os.makedirs(original_dir, exist_ok=True)
- print(f"🔄 [Enhance] Using original file location: {final_path}")
+ print(f"[Enhance] Using original file location: {final_path}")
return final_path, True
# Extract year and album_type from spotify_album for template use (safe for all modes)
@@ -17199,13 +17199,13 @@ def _downsample_hires_flac(final_path, context):
original_bits = audio.info.bits_per_sample
original_rate = audio.info.sample_rate
except Exception as e:
- print(f"⚠️ [Downsample] Could not read FLAC info: {e}")
+ print(f"[Downsample] Could not read FLAC info: {e}")
return None
if original_bits <= 16 and original_rate <= 44100:
return None # Already CD quality or below
- print(f"📀 [Downsample] Converting {original_bits}-bit/{original_rate}Hz → 16-bit/44100Hz: {os.path.basename(final_path)}")
+ print(f"[Downsample] Converting {original_bits}-bit/{original_rate}Hz → 16-bit/44100Hz: {os.path.basename(final_path)}")
ffmpeg_bin = shutil.which('ffmpeg')
if not ffmpeg_bin:
@@ -17213,7 +17213,7 @@ def _downsample_hires_flac(final_path, context):
if os.path.isfile(local):
ffmpeg_bin = local
else:
- print("⚠️ [Downsample] ffmpeg not found — skipping hi-res conversion")
+ print("[Downsample] ffmpeg not found — skipping hi-res conversion")
return None
temp_path = final_path + '.tmp.flac'
@@ -17228,27 +17228,27 @@ def _downsample_hires_flac(final_path, context):
], capture_output=True, text=True, timeout=300)
if result.returncode != 0:
- print(f"⚠️ [Downsample] ffmpeg failed: {result.stderr[:200]}")
+ print(f"[Downsample] ffmpeg failed: {result.stderr[:200]}")
if os.path.exists(temp_path):
os.remove(temp_path)
return None
# Verify the output is a valid 16-bit FLAC
if not os.path.isfile(temp_path) or os.path.getsize(temp_path) == 0:
- print(f"⚠️ [Downsample] Output file missing or empty")
+ print(f"[Downsample] Output file missing or empty")
if os.path.exists(temp_path):
os.remove(temp_path)
return None
verify_audio = FLAC(temp_path)
if verify_audio.info.bits_per_sample != 16:
- print(f"⚠️ [Downsample] Output not 16-bit ({verify_audio.info.bits_per_sample}-bit), aborting")
+ print(f"[Downsample] Output not 16-bit ({verify_audio.info.bits_per_sample}-bit), aborting")
os.remove(temp_path)
return None
# Atomic swap — replace original with downsampled version
os.replace(temp_path, final_path)
- print(f"✅ [Downsample] Converted to 16-bit/44.1kHz: {os.path.basename(final_path)}")
+ print(f"[Downsample] Converted to 16-bit/44.1kHz: {os.path.basename(final_path)}")
# Update QUALITY tag in the new file
new_quality = 'FLAC 16bit'
@@ -17257,7 +17257,7 @@ def _downsample_hires_flac(final_path, context):
updated_audio['QUALITY'] = new_quality
updated_audio.save()
except Exception as tag_err:
- print(f"⚠️ [Downsample] Could not update QUALITY tag: {tag_err}")
+ print(f"[Downsample] Could not update QUALITY tag: {tag_err}")
# Update context so downstream (lossy copy, metadata) reflects new quality
old_quality = context.get('_audio_quality', '')
@@ -17269,7 +17269,7 @@ def _downsample_hires_flac(final_path, context):
new_path = os.path.join(os.path.dirname(final_path), new_basename)
try:
os.rename(final_path, new_path)
- print(f"📝 [Downsample] Renamed: {os.path.basename(final_path)} → {new_basename}")
+ print(f"[Downsample] Renamed: {os.path.basename(final_path)} → {new_basename}")
# Rename matching lyrics sidecar file if it exists (.lrc or .txt)
for lyrics_ext in ('.lrc', '.txt'):
old_lyrics = os.path.splitext(final_path)[0] + lyrics_ext
@@ -17278,16 +17278,16 @@ def _downsample_hires_flac(final_path, context):
os.rename(old_lyrics, new_lyrics)
return new_path
except Exception as rename_err:
- print(f"⚠️ [Downsample] Could not rename file: {rename_err}")
+ print(f"[Downsample] Could not rename file: {rename_err}")
return final_path
except subprocess.TimeoutExpired:
- print(f"⚠️ [Downsample] Conversion timed out for: {os.path.basename(final_path)}")
+ print(f"[Downsample] Conversion timed out for: {os.path.basename(final_path)}")
if os.path.exists(temp_path):
os.remove(temp_path)
except Exception as e:
- print(f"⚠️ [Downsample] Conversion error: {e}")
+ print(f"[Downsample] Conversion error: {e}")
if os.path.exists(temp_path):
try:
os.remove(temp_path)
@@ -17327,7 +17327,7 @@ def _create_lossy_copy(final_path):
}
if codec not in codec_map:
- print(f"⚠️ [Lossy Copy] Unknown codec '{codec}' — skipping conversion")
+ print(f"[Lossy Copy] Unknown codec '{codec}' — skipping conversion")
return None
ffmpeg_codec, out_ext, quality_label, extra_args = codec_map[codec]
@@ -17347,11 +17347,11 @@ def _create_lossy_copy(final_path):
if os.path.isfile(local):
ffmpeg_bin = local
else:
- print(f"⚠️ [Lossy Copy] ffmpeg not found — skipping {codec.upper()} conversion")
+ print(f"[Lossy Copy] ffmpeg not found — skipping {codec.upper()} conversion")
return None
try:
- print(f"🎵 [Lossy Copy] Converting to {quality_label}: {os.path.basename(final_path)}")
+ print(f"[Lossy Copy] Converting to {quality_label}: {os.path.basename(final_path)}")
cmd = [
ffmpeg_bin, '-i', final_path,
'-codec:a', ffmpeg_codec,
@@ -17362,7 +17362,7 @@ def _create_lossy_copy(final_path):
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0:
- print(f"✅ [Lossy Copy] Created {quality_label} copy: {os.path.basename(out_path)}")
+ print(f"[Lossy Copy] Created {quality_label} copy: {os.path.basename(out_path)}")
# Fix QUALITY tag — the FLAC's tag was copied verbatim by ffmpeg
try:
@@ -17379,7 +17379,7 @@ def _create_lossy_copy(final_path):
audio['----:com.apple.iTunes:QUALITY'] = [MP4FreeForm(quality_label.encode('utf-8'))]
audio.save()
except Exception as tag_err:
- print(f"⚠️ [Lossy Copy] Could not update QUALITY tag: {tag_err}")
+ print(f"[Lossy Copy] Could not update QUALITY tag: {tag_err}")
# Embed cover art from source FLAC into the lossy copy
# Opus/OGG can't inherit FLAC cover art via ffmpeg -map_metadata alone
@@ -17409,7 +17409,7 @@ def _create_lossy_copy(final_path):
pic.depth = 0
pic.colors = 0
pic.data = img_data
- print(f"🎨 [Lossy Copy] Using cover.jpg as art source (FLAC had no embedded art)")
+ print(f"[Lossy Copy] Using cover.jpg as art source (FLAC had no embedded art)")
except Exception:
pass
@@ -17434,15 +17434,15 @@ def _create_lossy_copy(final_path):
)
dest_audio['METADATA_BLOCK_PICTURE'] = [base64.b64encode(picture_data).decode('ascii')]
dest_audio.save()
- print(f"🎨 [Lossy Copy] Embedded cover art in Opus file")
+ print(f"[Lossy Copy] Embedded cover art in Opus file")
elif codec == 'aac':
from mutagen.mp4 import MP4Cover
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG
dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)]
dest_audio.save()
- print(f"🎨 [Lossy Copy] Embedded cover art in M4A file")
+ print(f"[Lossy Copy] Embedded cover art in M4A file")
except Exception as art_err:
- print(f"⚠️ [Lossy Copy] Could not embed cover art: {art_err}")
+ print(f"[Lossy Copy] Could not embed cover art: {art_err}")
# Blasphemy Mode: delete original FLAC if enabled and output is verified
if config_manager.get('lossy_copy.delete_original', False):
@@ -17458,7 +17458,7 @@ def _create_lossy_copy(final_path):
except Exception:
pass
os.remove(final_path)
- print(f"🔥 [Blasphemy Mode] Deleted original: {os.path.basename(final_path)}")
+ print(f"[Blasphemy Mode] Deleted original: {os.path.basename(final_path)}")
# Rename lyrics sidecar file to match the output filename
for lyrics_ext in ('.lrc', '.txt'):
src_lyrics = os.path.splitext(final_path)[0] + lyrics_ext
@@ -17466,22 +17466,22 @@ def _create_lossy_copy(final_path):
dst_lyrics = os.path.splitext(out_path)[0] + lyrics_ext
try:
os.rename(src_lyrics, dst_lyrics)
- print(f"🔥 [Blasphemy Mode] Renamed {lyrics_ext}: {os.path.basename(src_lyrics)} -> {os.path.basename(dst_lyrics)}")
+ print(f"[Blasphemy Mode] Renamed {lyrics_ext}: {os.path.basename(src_lyrics)} -> {os.path.basename(dst_lyrics)}")
except Exception as lrc_err:
- print(f"⚠️ [Blasphemy Mode] Could not rename {lyrics_ext}: {lrc_err}")
+ print(f"[Blasphemy Mode] Could not rename {lyrics_ext}: {lrc_err}")
return out_path
else:
- print(f"⚠️ [Blasphemy Mode] Output failed audio validation, keeping original: {os.path.basename(final_path)}")
+ print(f"[Blasphemy Mode] Output failed audio validation, keeping original: {os.path.basename(final_path)}")
else:
- print(f"⚠️ [Blasphemy Mode] Output missing or empty, keeping original: {os.path.basename(final_path)}")
+ print(f"[Blasphemy Mode] Output missing or empty, keeping original: {os.path.basename(final_path)}")
except Exception as del_err:
- print(f"⚠️ [Blasphemy Mode] Error during original deletion, keeping original: {del_err}")
+ print(f"[Blasphemy Mode] Error during original deletion, keeping original: {del_err}")
else:
- print(f"⚠️ [Lossy Copy] ffmpeg failed: {result.stderr[:200]}")
+ print(f"[Lossy Copy] ffmpeg failed: {result.stderr[:200]}")
except subprocess.TimeoutExpired:
- print(f"⚠️ [Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")
+ print(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")
except Exception as e:
- print(f"⚠️ [Lossy Copy] Conversion error: {e}")
+ print(f"[Lossy Copy] Conversion error: {e}")
return None
def _apply_path_template(template: str, context: dict) -> str:
@@ -17672,11 +17672,11 @@ def _strip_all_non_audio_tags(file_path: str) -> dict:
apev2_tags.delete(file_path)
summary['apev2_stripped'] = True
summary['apev2_tag_count'] = tag_count
- print(f"🧹 Stripped {tag_count} APEv2 tags: {', '.join(tag_keys[:10])}")
+ print(f"Stripped {tag_count} APEv2 tags: {', '.join(tag_keys[:10])}")
except APENoHeaderError:
pass # No APEv2 tags — common case
except Exception as e:
- print(f"⚠️ Could not strip APEv2 tags (non-fatal): {e}")
+ print(f"Could not strip APEv2 tags (non-fatal): {e}")
return summary
def _verify_metadata_written(file_path: str) -> bool:
@@ -17684,7 +17684,7 @@ def _verify_metadata_written(file_path: str) -> bool:
try:
check = MutagenFile(file_path)
if check is None or check.tags is None:
- print(f"❌ [VERIFY] Tags are None after save: {file_path}")
+ print(f"[VERIFY] Tags are None after save: {file_path}")
return False
title_found = False
artist_found = False
@@ -17694,7 +17694,7 @@ def _verify_metadata_written(file_path: str) -> bool:
# Confirm APEv2 is gone
try:
APEv2(file_path)
- print(f"⚠️ [VERIFY] APEv2 tags still present after processing!")
+ print(f"[VERIFY] APEv2 tags still present after processing!")
return False
except APENoHeaderError:
pass
@@ -17705,12 +17705,12 @@ def _verify_metadata_written(file_path: str) -> bool:
title_found = bool(check.get('\xa9nam'))
artist_found = bool(check.get('\xa9ART'))
if not title_found or not artist_found:
- print(f"❌ [VERIFY] Missing metadata - title:{title_found} artist:{artist_found}")
+ print(f"[VERIFY] Missing metadata - title:{title_found} artist:{artist_found}")
return False
- print(f"✅ [VERIFY] Metadata verified OK")
+ print(f"[VERIFY] Metadata verified OK")
return True
except Exception as e:
- print(f"⚠️ [VERIFY] Verification error (non-fatal): {e}")
+ print(f"[VERIFY] Verification error (non-fatal): {e}")
return False
def _is_ogg_opus(audio_file):
@@ -17728,20 +17728,20 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
which stripped the ID3v2 header from MP3 files, leaving them tagless.
"""
if not config_manager.get('metadata_enhancement.enabled', True):
- print("🎵 Metadata enhancement disabled in config.")
+ print("Metadata enhancement disabled in config.")
return True
# Acquire per-file lock to prevent concurrent metadata writes to the same file
file_lock = _get_file_lock(file_path)
with file_lock:
- print(f"🎵 Enhancing metadata for: {os.path.basename(file_path)}")
+ print(f"Enhancing metadata for: {os.path.basename(file_path)}")
try:
# Strip APEv2 tags from MP3 (invisible to ID3 handler)
strip_summary = _strip_all_non_audio_tags(file_path)
audio_file = MutagenFile(file_path)
if audio_file is None:
- print(f"❌ Could not load audio file with Mutagen: {file_path}")
+ print(f"Could not load audio file with Mutagen: {file_path}")
return False
# ── Wipe ALL existing tags and save immediately ──
@@ -17756,7 +17756,7 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
if audio_file.tags is not None:
if len(audio_file.tags) > 0:
tag_keys = list(audio_file.tags.keys())[:15]
- print(f"🧹 Clearing {len(audio_file.tags)} existing tags: "
+ print(f"Clearing {len(audio_file.tags)} existing tags: "
f"{', '.join(str(k) for k in tag_keys)}")
audio_file.tags.clear()
else:
@@ -17772,7 +17772,7 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
metadata = _extract_spotify_metadata(context, artist, album_info)
if not metadata:
- print("⚠️ Could not extract Spotify metadata, saving with cleared tags.")
+ print("Could not extract Spotify metadata, saving with cleared tags.")
if isinstance(audio_file.tags, ID3):
audio_file.save(v1=0, v2_version=4)
elif isinstance(audio_file, FLAC):
@@ -17875,18 +17875,18 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
# Verify metadata was written
verified = _verify_metadata_written(file_path)
if verified:
- print("✅ Metadata enhanced successfully.")
+ print("Metadata enhanced successfully.")
else:
- print("⚠️ Metadata saved but verification found issues (see above).")
+ print("Metadata saved but verification found issues (see above).")
return True
except Exception as e:
import traceback
- print(f"❌ Error enhancing metadata for {file_path}: {e}")
- print(f"❌ [Metadata Debug] Exception type: {type(e).__name__}")
- print(f"❌ [Metadata Debug] File exists: {os.path.exists(file_path)}")
- print(f"❌ [Metadata Debug] Artist: {artist.get('name', 'MISSING') if artist else 'None'}")
- print(f"❌ [Metadata Debug] Album info: {album_info.get('album_name', 'MISSING') if album_info else 'None'}")
- print(f"❌ [Metadata Debug] Traceback:\n{traceback.format_exc()}")
+ print(f"Error enhancing metadata for {file_path}: {e}")
+ print(f"[Metadata Debug] Exception type: {type(e).__name__}")
+ print(f"[Metadata Debug] File exists: {os.path.exists(file_path)}")
+ print(f"[Metadata Debug] Artist: {artist.get('name', 'MISSING') if artist else 'None'}")
+ print(f"[Metadata Debug] Album info: {album_info.get('album_name', 'MISSING') if album_info else 'None'}")
+ print(f"[Metadata Debug] Traceback:\n{traceback.format_exc()}")
return False
def _generate_lrc_file(file_path: str, context: dict, artist: dict, album_info: dict) -> bool:
@@ -17935,14 +17935,14 @@ def _generate_lrc_file(file_path: str, context: dict, artist: dict, album_info:
)
if success:
- print(f"🎵 LRC file generated for: {track_name}")
+ print(f"LRC file generated for: {track_name}")
else:
- print(f"🎵 No lyrics found for: {track_name}")
+ print(f"No lyrics found for: {track_name}")
return success
except Exception as e:
- print(f"❌ Error generating LRC file for {file_path}: {e}")
+ print(f"Error generating LRC file for {file_path}: {e}")
return False
def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> dict:
@@ -17954,15 +17954,15 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
# Priority 1: Spotify clean title from context
if original_search.get('spotify_clean_title'):
metadata['title'] = original_search['spotify_clean_title']
- print(f"🎵 Metadata: Using Spotify clean title: '{metadata['title']}'")
+ print(f"Metadata: Using Spotify clean title: '{metadata['title']}'")
# Priority 2: Album info clean name
elif album_info.get('clean_track_name'):
metadata['title'] = album_info['clean_track_name']
- print(f"🎵 Metadata: Using album info clean name: '{metadata['title']}'")
+ print(f"Metadata: Using album info clean name: '{metadata['title']}'")
# Priority 3: Original title as fallback
else:
metadata['title'] = original_search.get('title', '')
- print(f"🎵 Metadata: Using original title as fallback: '{metadata['title']}'")
+ print(f"Metadata: Using original title as fallback: '{metadata['title']}'")
# Handle multiple artists from Spotify data
original_search = context.get("original_search_result", {})
if 'artists' in original_search and isinstance(original_search['artists'], list) and len(original_search['artists']) > 0:
@@ -17976,11 +17976,11 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
else:
all_artists.append(str(a))
metadata['artist'] = ', '.join(all_artists)
- print(f"🎵 Metadata: Using all artists: '{metadata['artist']}'")
+ print(f"Metadata: Using all artists: '{metadata['artist']}'")
else:
# Fallback to single artist
metadata['artist'] = artist.get('name', '')
- print(f"🎵 Metadata: Using primary artist: '{metadata['artist']}'")
+ print(f"Metadata: Using primary artist: '{metadata['artist']}'")
# Resolve album_artist for consistent tagging across all tracks in an album.
# Priority: 1) explicit batch artist context (same artist for whole album)
@@ -18040,18 +18040,18 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
track_num = album_info.get('track_number', 1)
metadata['track_number'] = track_num
metadata['total_tracks'] = spotify_album.get('total_tracks', 1) if spotify_album else 1
- print(f"🎵 [METADATA] Album track - track_number: {track_num}, album: {metadata['album']}")
+ print(f"[METADATA] Album track - track_number: {track_num}, album: {metadata['album']}")
else:
# SAFEGUARD: If we have spotify_album context, never use track title as album name
# This prevents album tracks from being tagged as singles due to classification errors
if spotify_album and spotify_album.get('name'):
- print(f"🛡️ [SAFEGUARD] Using spotify_album name instead of track title for album metadata")
+ print(f"[SAFEGUARD] Using spotify_album name instead of track title for album metadata")
metadata['album'] = spotify_album['name']
# Use corrected track_number from album_info (which should be updated by post-processing)
corrected_track_number = album_info.get('track_number', 1) if album_info else 1
metadata['track_number'] = corrected_track_number
metadata['total_tracks'] = spotify_album.get('total_tracks', 1)
- print(f"🛡️ [SAFEGUARD] Using track_number: {corrected_track_number}")
+ print(f"[SAFEGUARD] Using track_number: {corrected_track_number}")
else:
metadata['album'] = metadata['title'] # For true singles, album is the title
metadata['track_number'] = 1
@@ -18100,7 +18100,7 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
metadata['spotify_album_id'] = album_id
# Summary log for debugging metadata issues (e.g. wrong album_artist / track_number)
- print(f"📋 [Metadata Summary] title='{metadata.get('title')}' | artist='{metadata.get('artist')}' | album_artist='{metadata.get('album_artist')}' | album='{metadata.get('album')}' | track={metadata.get('track_number')}/{metadata.get('total_tracks')} | disc={metadata.get('disc_number')}")
+ print(f"[Metadata Summary] title='{metadata.get('title')}' | artist='{metadata.get('artist')}' | album_artist='{metadata.get('album_artist')}' | album='{metadata.get('album')}' | track={metadata.get('track_number')}/{metadata.get('total_tracks')} | disc={metadata.get('disc_number')}")
return metadata
@@ -18147,7 +18147,7 @@ def _embed_album_art_metadata(audio_file, metadata: dict):
image_data = response.read()
mime_type = response.info().get_content_type() or 'image/jpeg'
if image_data and len(image_data) > 1000:
- print(f"🎨 Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
+ print(f"Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
else:
image_data = None # Too small, likely an error page
except Exception:
@@ -18157,14 +18157,14 @@ def _embed_album_art_metadata(audio_file, metadata: dict):
if not image_data:
art_url = metadata.get('album_art_url')
if not art_url:
- print("🎨 No album art URL available for embedding.")
+ print("No album art URL available for embedding.")
return
with urllib.request.urlopen(art_url, timeout=10) as response:
image_data = response.read()
mime_type = response.info().get_content_type()
if not image_data:
- print("❌ Failed to download album art data.")
+ print("Failed to download album art data.")
return
# MP3 (ID3)
@@ -18187,9 +18187,9 @@ def _embed_album_art_metadata(audio_file, metadata: dict):
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in mime_type else MP4Cover.FORMAT_PNG
audio_file['covr'] = [MP4Cover(image_data, imageformat=fmt)]
- print("🎨 Album art successfully embedded.")
+ print("Album art successfully embedded.")
except Exception as e:
- print(f"❌ Error embedding album art: {e}")
+ print(f"Error embedding album art: {e}")
def _embed_source_ids(audio_file, metadata: dict):
"""
@@ -18380,13 +18380,13 @@ def _embed_source_ids(audio_file, metadata: dict):
""", (int(release_year), _pp_album_name, _pp_artist_name))
if cursor.rowcount > 0:
conn.commit()
- print(f"📅 Updated album year to {release_year} in database")
+ print(f"Updated album year to {release_year} in database")
else:
conn.rollback()
finally:
conn.close()
except Exception as e:
- print(f"⚠️ Could not update album year in DB: {e}")
+ print(f"Could not update album year in DB: {e}")
# (All source lookups now handled by _pp_lookup_* functions called via configurable order above)
if False: # Dead code — old inline blocks preserved for reference during transition
@@ -18458,10 +18458,10 @@ def _embed_source_ids(audio_file, metadata: dict):
break
except (ValueError, TypeError):
pass
- print(f"🎵 MusicBrainz release details: type={primary_type or '?'}, "
+ print(f"MusicBrainz release details: type={primary_type or '?'}, "
f"country={country or '?'}, media={id_tags.get('MEDIA', '?')}")
except Exception as e:
- print(f"⚠️ MusicBrainz release detail lookup failed (non-fatal): {e}")
+ print(f"MusicBrainz release detail lookup failed (non-fatal): {e}")
# ── 2b. Deezer lookup for BPM, ISRC fallback, and source IDs ──
deezer_bpm = None
@@ -18480,7 +18480,7 @@ def _embed_source_ids(audio_file, metadata: dict):
dz_artist_id = dz_result.get('artist', {}).get('id')
if dz_artist_id:
id_tags['DEEZER_ARTIST_ID'] = str(dz_artist_id)
- print(f"🎵 Deezer track matched: {dz_track_id}")
+ print(f"Deezer track matched: {dz_track_id}")
# Get full track details for BPM and ISRC
dz_details = dz_client.get_track_details(dz_track_id)
@@ -18492,9 +18492,9 @@ def _embed_source_ids(audio_file, metadata: dict):
if dz_isrc:
deezer_isrc = dz_isrc
else:
- print("⚠️ Deezer worker not available, skipping Deezer lookup")
+ print("Deezer worker not available, skipping Deezer lookup")
except Exception as e:
- print(f"⚠️ Deezer lookup failed (non-fatal): {e}")
+ print(f"Deezer lookup failed (non-fatal): {e}")
# ── 2c. AudioDB lookup for mood, style, genre, and source ID ──
audiodb_mood = None
@@ -18512,13 +18512,13 @@ def _embed_source_ids(audio_file, metadata: dict):
adb_track_id = adb_result.get('idTrack')
if adb_track_id:
id_tags['AUDIODB_TRACK_ID'] = str(adb_track_id)
- print(f"🎵 AudioDB track matched: {adb_track_id}")
+ print(f"AudioDB track matched: {adb_track_id}")
# Use AudioDB's MusicBrainz IDs as fallbacks for any missing from MB lookup
adb_mb_track = adb_result.get('strMusicBrainzID')
if adb_mb_track and 'MUSICBRAINZ_RECORDING_ID' not in id_tags:
id_tags['MUSICBRAINZ_RECORDING_ID'] = adb_mb_track
recording_mbid = adb_mb_track
- print(f"🎵 MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}")
+ print(f"MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}")
# NOTE: AudioDB's strMusicBrainzAlbumID is intentionally
# NOT used as a fallback for MUSICBRAINZ_RELEASE_ID.
# AudioDB links each track to its original album in MB,
@@ -18529,14 +18529,14 @@ def _embed_source_ids(audio_file, metadata: dict):
if adb_mb_artist and 'MUSICBRAINZ_ARTIST_ID' not in id_tags:
id_tags['MUSICBRAINZ_ARTIST_ID'] = adb_mb_artist
artist_mbid = adb_mb_artist
- print(f"🎵 MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}")
+ print(f"MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}")
audiodb_mood = adb_result.get('strMood') or None
audiodb_style = adb_result.get('strStyle') or None
audiodb_genre = adb_result.get('strGenre') or None
else:
- print("⚠️ AudioDB worker not available, skipping AudioDB lookup")
+ print("AudioDB worker not available, skipping AudioDB lookup")
except Exception as e:
- print(f"⚠️ AudioDB lookup failed (non-fatal): {e}")
+ print(f"AudioDB lookup failed (non-fatal): {e}")
# ── 2d. Tidal lookup for ISRC fallback, copyright, and source IDs ──
tidal_isrc = None
@@ -18551,7 +18551,7 @@ def _embed_source_ids(audio_file, metadata: dict):
td_track_id = td_result.get('id')
if td_track_id:
id_tags['TIDAL_TRACK_ID'] = str(td_track_id)
- print(f"🎵 Tidal track matched: {td_track_id}")
+ print(f"Tidal track matched: {td_track_id}")
td_artist = td_result.get('artist', {})
if isinstance(td_artist, dict) and td_artist.get('id'):
id_tags['TIDAL_ARTIST_ID'] = str(td_artist['id'])
@@ -18568,7 +18568,7 @@ def _embed_source_ids(audio_file, metadata: dict):
if td_copyright:
tidal_copyright = td_copyright
except Exception as e:
- print(f"⚠️ Tidal lookup failed (non-fatal): {e}")
+ print(f"Tidal lookup failed (non-fatal): {e}")
# ── 2e. Qobuz lookup for ISRC fallback, copyright, label, and source IDs ──
qobuz_isrc = None
@@ -18591,7 +18591,7 @@ def _embed_source_ids(audio_file, metadata: dict):
qz_track_id = qz_result.get('id')
if qz_track_id:
id_tags['QOBUZ_TRACK_ID'] = str(qz_track_id)
- print(f"🎵 Qobuz track matched: {qz_track_id}")
+ print(f"Qobuz track matched: {qz_track_id}")
if isinstance(qz_performer, dict) and qz_performer.get('id'):
id_tags['QOBUZ_ARTIST_ID'] = str(qz_performer['id'])
qz_isrc = qz_result.get('isrc')
@@ -18610,7 +18610,7 @@ def _embed_source_ids(audio_file, metadata: dict):
if isinstance(qz_label_info, dict) and qz_label_info.get('name'):
qobuz_label = qz_label_info['name']
except Exception as e:
- print(f"⚠️ Qobuz lookup failed (non-fatal): {e}")
+ print(f"Qobuz lookup failed (non-fatal): {e}")
# ── 2f. Last.fm lookup for tags (genre merge) and URL ──
lastfm_tags = []
@@ -18633,9 +18633,9 @@ def _embed_source_ids(audio_file, metadata: dict):
lastfm_tags = [t.get('name', '') for t in tag_list if isinstance(t, dict) and t.get('name')]
elif isinstance(tag_list, dict) and tag_list.get('name'):
lastfm_tags = [tag_list['name']]
- print(f"🎵 Last.fm track info found: {len(lastfm_tags)} tags")
+ print(f"Last.fm track info found: {len(lastfm_tags)} tags")
except Exception as e:
- print(f"⚠️ Last.fm lookup failed (non-fatal): {e}")
+ print(f"Last.fm lookup failed (non-fatal): {e}")
# ── 2g. Genius lookup for source ID and URL ──
# Genius has an aggressive global rate limiter (30→60→120s backoff) that
@@ -18649,7 +18649,7 @@ def _embed_source_ids(audio_file, metadata: dict):
try:
import core.genius_client as _genius_module
if time.time() < _genius_module._rate_limit_until:
- print("⏭️ Genius rate-limited, skipping (non-blocking)")
+ print("Genius rate-limited, skipping (non-blocking)")
else:
g_client = genius_worker.client if genius_worker else None
if g_client:
@@ -18658,12 +18658,12 @@ def _embed_source_ids(audio_file, metadata: dict):
g_id = g_result.get('id')
if g_id:
id_tags['GENIUS_TRACK_ID'] = str(g_id)
- print(f"🎵 Genius song matched: {g_id}")
+ print(f"Genius song matched: {g_id}")
g_url = g_result.get('url')
if g_url:
genius_url = g_url
except Exception as e:
- print(f"⚠️ Genius lookup failed (non-fatal): {e}")
+ print(f"Genius lookup failed (non-fatal): {e}")
if not id_tags and not deezer_bpm and not deezer_isrc and not audiodb_mood and not audiodb_style:
return
@@ -18751,7 +18751,7 @@ def _embed_source_ids(audio_file, metadata: dict):
written.append(key)
if written:
- print(f"🔗 Embedded IDs: {', '.join(written)}")
+ print(f"Embedded IDs: {', '.join(written)}")
# ── 3a½. Write date tag if discovered during lookups (initial write had no date) ──
if _needs_date_tag and release_year:
@@ -18761,7 +18761,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['date'] = [release_year]
elif isinstance(audio_file, MP4):
audio_file['\xa9day'] = [release_year]
- print(f"📅 Date tag: {release_year}")
+ print(f"Date tag: {release_year}")
# ── 3b. Write BPM tag (from Deezer) ──
if _tag_enabled('deezer.tags.bpm') and deezer_bpm and deezer_bpm > 0:
@@ -18772,7 +18772,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['BPM'] = [str(bpm_int)]
elif isinstance(audio_file, MP4):
audio_file['tmpo'] = [bpm_int]
- print(f"🥁 BPM: {bpm_int}")
+ print(f"BPM: {bpm_int}")
# ── 3c. Write mood tag (from AudioDB) ──
if _tag_enabled('audiodb.tags.mood') and audiodb_mood:
@@ -18782,7 +18782,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['MOOD'] = [audiodb_mood]
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:MOOD'] = [MP4FreeForm(audiodb_mood.encode('utf-8'))]
- print(f"🎭 Mood: {audiodb_mood}")
+ print(f"Mood: {audiodb_mood}")
# ── 3d. Write style tag (from AudioDB) ──
if _tag_enabled('audiodb.tags.style') and audiodb_style:
@@ -18792,7 +18792,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['STYLE'] = [audiodb_style]
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:STYLE'] = [MP4FreeForm(audiodb_style.encode('utf-8'))]
- print(f"🎨 Style: {audiodb_style}")
+ print(f"Style: {audiodb_style}")
# ── 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag ──
if _tag_enabled('metadata_enhancement.tags.genre_merge'):
@@ -18819,7 +18819,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['GENRE'] = [genre_string]
elif isinstance(audio_file, MP4):
audio_file['\xa9gen'] = [genre_string]
- print(f"🎶 Genres merged: {genre_string}")
+ print(f"Genres merged: {genre_string}")
# ── 5. Write ISRC if available (per-source fallback chain) ──
_isrc_candidates = []
@@ -18839,7 +18839,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['ISRC'] = [final_isrc]
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))]
- print(f"🔖 ISRC ({source}): {final_isrc}")
+ print(f"ISRC ({source}): {final_isrc}")
# ── 6. Write copyright tag (Tidal → Qobuz fallback) ──
_copyright_candidates = []
@@ -18865,7 +18865,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['LABEL'] = [qobuz_label]
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:LABEL'] = [MP4FreeForm(qobuz_label.encode('utf-8'))]
- print(f"🏷️ Label (Qobuz): {qobuz_label}")
+ print(f"Label (Qobuz): {qobuz_label}")
# ── 8. Write Last.fm and Genius URLs as custom tags ──
if _tag_enabled('lastfm.tags.url') and lastfm_url:
@@ -18885,7 +18885,7 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['----:com.apple.iTunes:GENIUS_URL'] = [MP4FreeForm(genius_url.encode('utf-8'))]
except Exception as e:
- print(f"⚠️ Error embedding source IDs (non-fatal): {e}")
+ print(f"Error embedding source IDs (non-fatal): {e}")
def _download_cover_art(album_info: dict, target_dir: str, context: dict = None):
"""Downloads cover.jpg into the specified directory.
@@ -18910,7 +18910,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
return # Already high-res, skip
# Low-res cover exists — try to upgrade from CAA
is_upgrade = True
- print(f"🔄 Existing cover.jpg is {existing_size // 1024}KB — attempting CAA upgrade...")
+ print(f"Existing cover.jpg is {existing_size // 1024}KB — attempting CAA upgrade...")
except Exception:
return
else:
@@ -18929,7 +18929,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
with urllib.request.urlopen(req, timeout=10) as response:
image_data = response.read()
if image_data and len(image_data) > 1000:
- print(f"🎨 Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
+ print(f"Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
else:
image_data = None
except Exception:
@@ -18937,7 +18937,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
# If upgrading and CAA failed, keep existing cover — don't overwrite with same low-res
if is_upgrade and not image_data:
- print(f"📷 CAA upgrade failed — keeping existing cover.jpg")
+ print(f"CAA upgrade failed — keeping existing cover.jpg")
return
# Fallback to Spotify/iTunes/Deezer URL (typically 640x640)
@@ -18953,9 +18953,9 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
if images and isinstance(images, list) and len(images) > 0:
art_url = images[0].get('url') if isinstance(images[0], dict) else None
if art_url:
- print(f"📷 Using cover art URL from spotify_album context")
+ print(f"Using cover art URL from spotify_album context")
if not art_url:
- print("📷 No cover art URL available for download.")
+ print("No cover art URL available for download.")
return
with urllib.request.urlopen(art_url, timeout=10) as response:
image_data = response.read()
@@ -18966,9 +18966,9 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
with open(cover_path, 'wb') as f:
f.write(image_data)
- print(f"✅ Cover art downloaded to: {cover_path}")
+ print(f"Cover art downloaded to: {cover_path}")
except Exception as e:
- print(f"❌ Error downloading cover.jpg: {e}")
+ print(f"Error downloading cover.jpg: {e}")
@@ -18989,7 +18989,7 @@ def _get_spotify_album_tracks(spotify_album: dict) -> list:
} for item in tracks_data['items']]
return []
except Exception as e:
- print(f"❌ Error fetching Spotify album tracks: {e}")
+ print(f"Error fetching Spotify album tracks: {e}")
return []
def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -> dict:
@@ -19005,7 +19005,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
track_num = slsk_track_meta['track_number']
for sp_track in spotify_tracks:
if sp_track.get('track_number') == track_num:
- print(f"✅ Matched track by number ({track_num}): '{slsk_track_meta['title']}' -> '{sp_track['name']}'")
+ print(f"Matched track by number ({track_num}): '{slsk_track_meta['title']}' -> '{sp_track['name']}'")
# Return a new dict with the corrected title and number
return {
'title': sp_track['name'],
@@ -19029,7 +19029,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
best_match = sp_track
if best_match:
- print(f"✅ Matched track by title similarity ({best_score:.2f}): '{slsk_track_meta['title']}' -> '{best_match['name']}'")
+ print(f"Matched track by title similarity ({best_score:.2f}): '{slsk_track_meta['title']}' -> '{best_match['name']}'")
return {
'title': best_match['name'],
'artist': slsk_track_meta.get('artist'),
@@ -19039,7 +19039,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
'explicit': best_match.get('explicit', False)
}
- print(f"⚠️ Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.")
+ print(f"Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.")
return slsk_track_meta # Fallback to original
@@ -19062,13 +19062,13 @@ def _pp_lookup_musicbrainz(pp, _names_match):
try:
mb_service = mb_worker.mb_service if mb_worker else None
if not mb_service:
- print("⚠️ MusicBrainz worker not available, skipping MBID lookup")
+ print("MusicBrainz worker not available, skipping MBID lookup")
return
result = mb_service.match_recording(track_title, artist_name)
if result and result.get('mbid'):
pp['recording_mbid'] = result['mbid']
id_tags['MUSICBRAINZ_RECORDING_ID'] = pp['recording_mbid']
- print(f"🎵 MusicBrainz recording matched: {pp['recording_mbid']}")
+ print(f"MusicBrainz recording matched: {pp['recording_mbid']}")
details = mb_service.mb_client.get_recording(pp['recording_mbid'], includes=['isrcs', 'genres'])
if details:
isrcs = details.get('isrcs', [])
@@ -19166,7 +19166,7 @@ def _pp_lookup_musicbrainz(pp, _names_match):
except (ValueError, TypeError):
pass
except Exception as e:
- print(f"⚠️ MusicBrainz lookup failed (non-fatal): {e}")
+ print(f"MusicBrainz lookup failed (non-fatal): {e}")
def _pp_lookup_deezer(pp, _names_match):
@@ -19180,7 +19180,7 @@ def _pp_lookup_deezer(pp, _names_match):
try:
dz_client = deezer_worker.client if deezer_worker else None
if not dz_client:
- print("⚠️ Deezer worker not available, skipping Deezer lookup")
+ print("Deezer worker not available, skipping Deezer lookup")
return
dz_result = dz_client.search_track(artist_name, track_title)
if dz_result and _names_match(dz_result.get('title', ''), track_title) and \
@@ -19190,7 +19190,7 @@ def _pp_lookup_deezer(pp, _names_match):
dz_artist_id = dz_result.get('artist', {}).get('id')
if dz_artist_id:
id_tags['DEEZER_ARTIST_ID'] = str(dz_artist_id)
- print(f"🎵 Deezer track matched: {dz_track_id}")
+ print(f"Deezer track matched: {dz_track_id}")
dz_details = dz_client.get_track_details(dz_track_id)
if dz_details:
bpm_val = dz_details.get('bpm')
@@ -19206,7 +19206,7 @@ def _pp_lookup_deezer(pp, _names_match):
if len(dz_release) >= 4 and dz_release[:4].isdigit():
pp['release_year'] = dz_release[:4]
except Exception as e:
- print(f"⚠️ Deezer lookup failed (non-fatal): {e}")
+ print(f"Deezer lookup failed (non-fatal): {e}")
def _pp_lookup_audiodb(pp, _names_match):
@@ -19220,7 +19220,7 @@ def _pp_lookup_audiodb(pp, _names_match):
try:
adb_client = audiodb_worker.client if audiodb_worker else None
if not adb_client:
- print("⚠️ AudioDB worker not available, skipping AudioDB lookup")
+ print("AudioDB worker not available, skipping AudioDB lookup")
return
adb_result = adb_client.search_track(artist_name, track_title)
if adb_result and _names_match(adb_result.get('strTrack', ''), track_title) and \
@@ -19228,22 +19228,22 @@ def _pp_lookup_audiodb(pp, _names_match):
adb_track_id = adb_result.get('idTrack')
if adb_track_id:
id_tags['AUDIODB_TRACK_ID'] = str(adb_track_id)
- print(f"🎵 AudioDB track matched: {adb_track_id}")
+ print(f"AudioDB track matched: {adb_track_id}")
adb_mb_track = adb_result.get('strMusicBrainzID')
if adb_mb_track and 'MUSICBRAINZ_RECORDING_ID' not in id_tags:
id_tags['MUSICBRAINZ_RECORDING_ID'] = adb_mb_track
pp['recording_mbid'] = adb_mb_track
- print(f"🎵 MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}")
+ print(f"MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}")
adb_mb_artist = adb_result.get('strMusicBrainzArtistID')
if adb_mb_artist and 'MUSICBRAINZ_ARTIST_ID' not in id_tags:
id_tags['MUSICBRAINZ_ARTIST_ID'] = adb_mb_artist
pp['artist_mbid'] = adb_mb_artist
- print(f"🎵 MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}")
+ print(f"MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}")
pp['audiodb_mood'] = adb_result.get('strMood') or None
pp['audiodb_style'] = adb_result.get('strStyle') or None
pp['audiodb_genre'] = adb_result.get('strGenre') or None
except Exception as e:
- print(f"⚠️ AudioDB lookup failed (non-fatal): {e}")
+ print(f"AudioDB lookup failed (non-fatal): {e}")
def _pp_lookup_tidal(pp, _names_match):
@@ -19262,7 +19262,7 @@ def _pp_lookup_tidal(pp, _names_match):
td_track_id = td_result.get('id')
if td_track_id:
id_tags['TIDAL_TRACK_ID'] = str(td_track_id)
- print(f"🎵 Tidal track matched: {td_track_id}")
+ print(f"Tidal track matched: {td_track_id}")
td_artist = td_result.get('artist', {})
if isinstance(td_artist, dict) and td_artist.get('id'):
id_tags['TIDAL_ARTIST_ID'] = str(td_artist['id'])
@@ -19286,7 +19286,7 @@ def _pp_lookup_tidal(pp, _names_match):
if len(td_release) >= 4 and td_release[:4].isdigit():
pp['release_year'] = td_release[:4]
except Exception as e:
- print(f"⚠️ Tidal lookup failed (non-fatal): {e}")
+ print(f"Tidal lookup failed (non-fatal): {e}")
def _pp_lookup_qobuz(pp, _names_match):
@@ -19312,7 +19312,7 @@ def _pp_lookup_qobuz(pp, _names_match):
qz_track_id = qz_result.get('id')
if qz_track_id:
id_tags['QOBUZ_TRACK_ID'] = str(qz_track_id)
- print(f"🎵 Qobuz track matched: {qz_track_id}")
+ print(f"Qobuz track matched: {qz_track_id}")
if isinstance(qz_performer, dict) and qz_performer.get('id'):
id_tags['QOBUZ_ARTIST_ID'] = str(qz_performer['id'])
qz_isrc = qz_result.get('isrc')
@@ -19342,7 +19342,7 @@ def _pp_lookup_qobuz(pp, _names_match):
if len(qz_release) >= 4 and qz_release[:4].isdigit():
pp['release_year'] = qz_release[:4]
except Exception as e:
- print(f"⚠️ Qobuz lookup failed (non-fatal): {e}")
+ print(f"Qobuz lookup failed (non-fatal): {e}")
def _pp_lookup_lastfm(pp, _names_match):
@@ -19368,9 +19368,9 @@ def _pp_lookup_lastfm(pp, _names_match):
pp['lastfm_tags'] = [t.get('name', '') for t in tag_list if isinstance(t, dict) and t.get('name')]
elif isinstance(tag_list, dict) and tag_list.get('name'):
pp['lastfm_tags'] = [tag_list['name']]
- print(f"🎵 Last.fm track info found: {len(pp['lastfm_tags'])} tags")
+ print(f"Last.fm track info found: {len(pp['lastfm_tags'])} tags")
except Exception as e:
- print(f"⚠️ Last.fm lookup failed (non-fatal): {e}")
+ print(f"Last.fm lookup failed (non-fatal): {e}")
def _pp_lookup_genius(pp, _names_match):
@@ -19384,7 +19384,7 @@ def _pp_lookup_genius(pp, _names_match):
try:
import core.genius_client as _genius_module
if time.time() < _genius_module._rate_limit_until:
- print("⏭️ Genius rate-limited, skipping (non-blocking)")
+ print("Genius rate-limited, skipping (non-blocking)")
return
g_client = genius_worker.client if genius_worker else None
if not g_client:
@@ -19394,12 +19394,12 @@ def _pp_lookup_genius(pp, _names_match):
g_id = g_result.get('id')
if g_id:
id_tags['GENIUS_TRACK_ID'] = str(g_id)
- print(f"🎵 Genius song matched: {g_id}")
+ print(f"Genius song matched: {g_id}")
g_url = g_result.get('url')
if g_url:
pp['genius_url'] = g_url
except Exception as e:
- print(f"⚠️ Genius lookup failed (non-fatal): {e}")
+ print(f"Genius lookup failed (non-fatal): {e}")
def _post_process_matched_download_with_verification(context_key, context, file_path, task_id, batch_id):
@@ -19482,7 +19482,7 @@ def _post_process_matched_download_with_verification(context_key, context, file_
# causing false "file verification failed" errors on successfully processed files.
expected_final_path = context.get('_final_processed_path')
if not expected_final_path:
- _pp.info(f"⚠️ No _final_processed_path in context for task {task_id} — cannot verify, assuming success")
+ _pp.info(f"No _final_processed_path in context for task {task_id} — cannot verify, assuming success")
with tasks_lock:
if task_id in download_tasks:
_mark_task_completed(task_id, context.get('track_info'))
@@ -19603,16 +19603,16 @@ def _check_flac_bit_depth(file_path, context, context_key):
track_info = context.get('track_info', {})
track_name = track_info.get('name', os.path.basename(file_path))
if _downsample_enabled:
- print(f"📀 [FLAC Downsample] Accepted {_actual_bits}-bit FLAC (will be downsampled to {_flac_pref}-bit): {track_name}")
+ print(f"[FLAC Downsample] Accepted {_actual_bits}-bit FLAC (will be downsampled to {_flac_pref}-bit): {track_name}")
else:
- print(f"📀 [FLAC Fallback] Accepted {_actual_bits}-bit FLAC (preferred {_flac_pref}-bit): {track_name}")
+ print(f"[FLAC Fallback] Accepted {_actual_bits}-bit FLAC (preferred {_flac_pref}-bit): {track_name}")
return False
# Strict mode — reject and quarantine
rejection_msg = f"FLAC bit depth mismatch: file is {_actual_bits}-bit, preference is {_flac_pref}-bit"
try:
quarantine_path = _move_to_quarantine(file_path, context, rejection_msg)
- print(f"🚫 File quarantined due to bit depth filter: {quarantine_path}")
+ print(f"File quarantined due to bit depth filter: {quarantine_path}")
except Exception as quarantine_error:
logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}")
try:
@@ -19700,7 +19700,7 @@ def _move_to_quarantine(file_path: str, context: dict, reason: str) -> str:
except Exception as e:
logger.warning(f"Failed to write quarantine metadata: {e}")
- logger.warning(f"🚫 File quarantined: {quarantine_path} - Reason: {reason}")
+ logger.warning(f"File quarantined: {quarantine_path} - Reason: {reason}")
try:
if automation_engine:
@@ -19781,7 +19781,7 @@ def _safe_move_file(src, dst):
# to delete the source (e.g. permission denied on slskd-owned downloads).
# If destination exists with content, treat as success.
if dst.exists() and dst.stat().st_size > 0:
- logger.warning(f"⚠️ Move raised {type(e).__name__} but destination exists, treating as success: {e}")
+ logger.warning(f"Move raised {type(e).__name__} but destination exists, treating as success: {e}")
# Try to clean up source, but don't fail if we can't
try:
src.unlink()
@@ -19791,7 +19791,7 @@ def _safe_move_file(src, dst):
# Cross-device link error — do manual binary copy
if "cross-device" in error_msg or "operation not permitted" in error_msg or "permission denied" in error_msg:
- logger.warning(f"⚠️ Cross-device move detected, using fallback copy method: {e}")
+ logger.warning(f"Cross-device move detected, using fallback copy method: {e}")
try:
# Simple copy without metadata preservation (avoids permission errors)
with open(src, 'rb') as f_src:
@@ -19805,10 +19805,10 @@ def _safe_move_file(src, dst):
src.unlink()
except PermissionError:
logger.info(f"Could not delete source file (may be owned by another process): {src}")
- logger.info(f"✅ Successfully moved file using fallback method: {src} -> {dst}")
+ logger.info(f"Successfully moved file using fallback method: {src} -> {dst}")
return
except Exception as fallback_error:
- logger.error(f"❌ Fallback copy also failed: {fallback_error}")
+ logger.error(f"Fallback copy also failed: {fallback_error}")
raise
else:
# Re-raise if it's a different error
@@ -19849,9 +19849,9 @@ def _post_process_matched_download(context_key, context, file_path):
if not os.path.exists(file_path):
existing_final = context.get('_final_processed_path')
if existing_final and os.path.exists(existing_final):
- print(f"✅ [Race Guard] Source gone but destination exists — already processed by another thread: {os.path.basename(existing_final)}")
+ print(f"[Race Guard] Source gone but destination exists — already processed by another thread: {os.path.basename(existing_final)}")
return
- print(f"⚠️ [Race Guard] Source file gone and no known destination — marking as failed: {os.path.basename(file_path)}")
+ print(f"[Race Guard] Source file gone and no known destination — marking as failed: {os.path.basename(file_path)}")
context['_race_guard_failed'] = True
return
# --- END RACE CONDITION GUARD ---
@@ -19872,10 +19872,10 @@ def _post_process_matched_download(context_key, context, file_path):
break
_prev_size = _cur_size
if _stability_check == 0:
- print(f"⏳ Waiting for file to stabilise: {_basename} ({_cur_size} bytes)")
+ print(f"Waiting for file to stabilise: {_basename} ({_cur_size} bytes)")
time.sleep(1.5)
else:
- print(f"⚠️ File may still be writing after stability checks: {_basename} ({_prev_size} bytes)")
+ print(f"File may still be writing after stability checks: {_basename} ({_prev_size} bytes)")
# --- END FILE STABILITY CHECK ---
# --- ACOUSTID VERIFICATION ---
@@ -19919,26 +19919,26 @@ def _post_process_matched_download(context_key, context, file_path):
expected_artist = spotify_artist.get('name', '')
if expected_track and expected_artist:
- print(f"🔍 Running AcoustID verification for: '{expected_track}' by '{expected_artist}'")
+ print(f"Running AcoustID verification for: '{expected_track}' by '{expected_artist}'")
verification_result, verification_msg = verifier.verify_audio_file(
file_path,
expected_track,
expected_artist,
context
)
- print(f"🔍 AcoustID verification result: {verification_result.value} - {verification_msg}")
+ print(f"AcoustID verification result: {verification_result.value} - {verification_msg}")
context['_acoustid_result'] = verification_result.value
if verification_result == VerificationResult.FAIL:
# Move to quarantine instead of Transfer
try:
quarantine_path = _move_to_quarantine(file_path, context, verification_msg)
- print(f"🚫 File quarantined due to verification failure: {quarantine_path}")
+ print(f"File quarantined due to verification failure: {quarantine_path}")
except Exception as quarantine_error:
# Quarantine failed — delete the known-wrong file instead
# NEVER save a file we've confirmed is wrong
logger.error(f"Quarantine failed ({quarantine_error}), deleting wrong file: {file_path}")
- print(f"🚫 Quarantine failed, deleting wrong file: {file_path}")
+ print(f"Quarantine failed, deleting wrong file: {file_path}")
try:
os.remove(file_path)
except Exception as del_error:
@@ -19968,14 +19968,14 @@ def _post_process_matched_download(context_key, context, file_path):
return # NEVER continue processing a known-wrong file
else:
- print(f"⚠️ AcoustID verification skipped: missing track/artist info")
+ print(f"AcoustID verification skipped: missing track/artist info")
context['_acoustid_result'] = 'skip'
else:
print(f"ℹ️ AcoustID verification not available: {available_reason}")
context['_acoustid_result'] = 'disabled'
except Exception as verify_error:
# Any verification error should NOT block the download - fail open
- print(f"⚠️ AcoustID verification error (continuing normally): {verify_error}")
+ print(f"AcoustID verification error (continuing normally): {verify_error}")
context['_acoustid_result'] = 'error'
# --- END ACOUSTID VERIFICATION ---
@@ -20022,7 +20022,7 @@ def _post_process_matched_download(context_key, context, file_path):
logger.info(f"Moving to Transfer root (single track)")
_safe_move_file(file_path, destination)
- logger.info(f"✅ Moved simple download to: {destination}")
+ logger.info(f"Moved simple download to: {destination}")
# Clean up context
with matched_context_lock:
@@ -20036,8 +20036,8 @@ def _post_process_matched_download(context_key, context, file_path):
daemon=True
).start()
- add_activity_item("✅", "Download Complete", f"{album_name}/{filename}", "Now")
- logger.info(f"✅ Simple download post-processing complete: {album_name}/{filename}")
+ add_activity_item("", "Download Complete", f"{album_name}/{filename}", "Now")
+ logger.info(f"Simple download post-processing complete: {album_name}/{filename}")
# Set flag in context so verification function knows this was fully handled
context['_simple_download_completed'] = True
@@ -20048,39 +20048,39 @@ def _post_process_matched_download(context_key, context, file_path):
return
# --- END SIMPLE DOWNLOAD HANDLING ---
- print(f"🎯 Starting robust post-processing for: {context_key}")
+ print(f"Starting robust post-processing for: {context_key}")
spotify_artist = context.get("spotify_artist")
if not spotify_artist:
- print(f"❌ Post-processing failed: Missing spotify_artist context.")
+ print(f"Post-processing failed: Missing spotify_artist context.")
return
# Check if playlist folder mode is enabled (sync page playlists only)
track_info = context.get("track_info", {})
playlist_folder_mode = track_info.get("_playlist_folder_mode", False)
- print(f"🔍 [Debug] Post-processing - track_info type: {type(track_info)}, is None: {track_info is None}, is empty: {not track_info}")
- print(f"🔍 [Debug] Post-processing - playlist_folder_mode: {playlist_folder_mode}")
+ print(f"[Debug] Post-processing - track_info type: {type(track_info)}, is None: {track_info is None}, is empty: {not track_info}")
+ print(f"[Debug] Post-processing - playlist_folder_mode: {playlist_folder_mode}")
if track_info:
- print(f"🔍 [Debug] Post-processing - track_info keys: {list(track_info.keys())}")
+ print(f"[Debug] Post-processing - track_info keys: {list(track_info.keys())}")
if playlist_folder_mode:
# Use shared path builder for playlist mode
playlist_name = track_info.get("_playlist_name", "Unknown Playlist")
- print(f"📁 [Playlist Folder Mode] Organizing in playlist folder: {playlist_name}")
+ print(f"[Playlist Folder Mode] Organizing in playlist folder: {playlist_name}")
file_ext = os.path.splitext(file_path)[1]
# Build final path FIRST so we can check for already-processed files
final_path, _ = _build_final_path_for_track(context, spotify_artist, None, file_ext)
- print(f"📁 Playlist mode final path: '{final_path}'")
+ print(f"Playlist mode final path: '{final_path}'")
# RACE CONDITION GUARD: If source file is gone but destination exists,
# another thread (stream processor or verification worker) already moved it.
# Return early to avoid deleting the successfully processed file.
if not os.path.exists(file_path):
if os.path.exists(final_path):
- print(f"✅ [Playlist Folder Mode] Source gone but destination exists — already processed by another thread: {os.path.basename(final_path)}")
+ print(f"[Playlist Folder Mode] Source gone but destination exists — already processed by another thread: {os.path.basename(final_path)}")
context['_final_processed_path'] = final_path
return
else:
@@ -20089,7 +20089,7 @@ def _post_process_matched_download(context_key, context, file_path):
context['_audio_quality'] = _get_audio_quality_string(file_path)
if context['_audio_quality']:
- print(f"🎧 Audio quality detected: {context['_audio_quality']}")
+ print(f"Audio quality detected: {context['_audio_quality']}")
# FLAC bit depth filter
if _check_flac_bit_depth(file_path, context, context_key):
@@ -20097,14 +20097,14 @@ def _post_process_matched_download(context_key, context, file_path):
# Enhance metadata before moving
try:
- print(f"🔍 [Metadata Input] Playlist mode - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
+ print(f"[Metadata Input] Playlist mode - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
_enhance_file_metadata(file_path, context, spotify_artist, None)
except Exception as meta_err:
import traceback
pp_logger.info(f"[inner] Metadata enhancement FAILED for {context_key}: {meta_err}\n{traceback.format_exc()}")
# Move file to playlist folder
- print(f"🚚 Moving '{os.path.basename(file_path)}' to '{final_path}'")
+ print(f"Moving '{os.path.basename(file_path)}' to '{final_path}'")
_safe_move_file(file_path, final_path)
# Store final path for verification wrapper (before conversions may override)
@@ -20125,13 +20125,13 @@ def _post_process_matched_download(context_key, context, file_path):
downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
_cleanup_empty_directories(downloads_path, file_path)
- print(f"✅ [Playlist Folder Mode] Post-processing complete: {final_path}")
+ print(f"[Playlist Folder Mode] Post-processing complete: {final_path}")
# WISHLIST REMOVAL: Check if this track should be removed from wishlist
try:
_check_and_remove_from_wishlist(context)
except Exception as wishlist_error:
- print(f"⚠️ [Playlist Folder] Error checking wishlist removal: {wishlist_error}")
+ print(f"[Playlist Folder] Error checking wishlist removal: {wishlist_error}")
_emit_track_downloaded(context)
_record_library_history_download(context)
@@ -20146,7 +20146,7 @@ def _post_process_matched_download(context_key, context, file_path):
if task_id in download_tasks:
download_tasks[task_id]['stream_processed'] = True
download_tasks[task_id]['status'] = 'completed'
- print(f"✅ [Playlist Folder Mode] Marked task {task_id} as completed")
+ print(f"[Playlist Folder Mode] Marked task {task_id} as completed")
_on_download_completed(batch_id, task_id, success=True)
return # Skip normal album/artist folder structure processing
@@ -20156,7 +20156,7 @@ def _post_process_matched_download(context_key, context, file_path):
if is_album_download and has_clean_spotify_data:
# Build album_info directly from clean Spotify metadata (GUI PARITY)
- print("✅ Album context with clean Spotify data found - using direct album info")
+ print("Album context with clean Spotify data found - using direct album info")
original_search = context.get("original_search_result", {})
spotify_album = context.get("spotify_album", {})
@@ -20165,7 +20165,7 @@ def _post_process_matched_download(context_key, context, file_path):
clean_album_name = original_search.get('spotify_clean_album', 'Unknown Album')
# DEBUG: Check what's in original_search
- print(f"🔍 [DEBUG] Path 1 - Clean Spotify data path:")
+ print(f"[DEBUG] Path 1 - Clean Spotify data path:")
print(f" original_search keys: {list(original_search.keys())}")
print(f" track_number in original_search: {'track_number' in original_search}")
print(f" track_number value: {original_search.get('track_number', 'NOT_FOUND')}")
@@ -20181,16 +20181,16 @@ def _post_process_matched_download(context_key, context, file_path):
'source': 'clean_spotify_metadata'
}
- print(f"🎯 Using clean Spotify album: '{clean_album_name}' for track: '{clean_track_name}'")
+ print(f"Using clean Spotify album: '{clean_album_name}' for track: '{clean_track_name}'")
elif is_album_download:
# CRITICAL FIX: Album context without clean Spotify data - still force album treatment
- print("⚠️ Album context found but no clean Spotify data - using enhanced fallback")
+ print("Album context found but no clean Spotify data - using enhanced fallback")
original_search = context.get("original_search_result", {})
spotify_album = context.get("spotify_album", {})
clean_track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track')
# DEBUG: Check what's in original_search for path 2
- print(f"🔍 [DEBUG] Path 2 - Enhanced fallback album context path:")
+ print(f"[DEBUG] Path 2 - Enhanced fallback album context path:")
print(f" original_search keys: {list(original_search.keys())}")
print(f" track_number in original_search: {'track_number' in original_search}")
print(f" track_number value: {original_search.get('track_number', 'NOT_FOUND')}")
@@ -20211,10 +20211,10 @@ def _post_process_matched_download(context_key, context, file_path):
'confidence': 0.9, # Higher confidence - user explicitly chose album
'source': 'enhanced_fallback_album_context'
}
- print(f"🎯 [FORCED ALBUM] Using album: '{album_name}' for track: '{clean_track_name}'")
+ print(f"[FORCED ALBUM] Using album: '{album_name}' for track: '{clean_track_name}'")
else:
# For singles, we still need to detect if they belong to an album.
- print("🎵 Single track download - attempting album detection")
+ print("Single track download - attempting album detection")
album_info = _detect_album_info_web(context, spotify_artist)
# --- Album grouping resolution ---
@@ -20222,7 +20222,7 @@ def _post_process_matched_download(context_key, context, file_path):
# Explicit album downloads already have the correct Spotify album name —
# re-grouping would mangle names like "(Reworked and Remastered)" into "(Deluxe Edition)".
if album_info and album_info['is_album'] and not is_album_download:
- print(f"\n🎯 SMART ALBUM GROUPING for track: '{album_info.get('clean_track_name', 'Unknown')}'")
+ print(f"\nSMART ALBUM GROUPING for track: '{album_info.get('clean_track_name', 'Unknown')}'")
print(f" Original album: '{album_info.get('album_name', 'None')}'")
# Get original album name from context if available
@@ -20235,16 +20235,16 @@ def _post_process_matched_download(context_key, context, file_path):
album_info['album_name'] = consistent_album_name
print(f" Final album name: '{consistent_album_name}'")
- print(f"🔗 ✅ Album grouping complete!\n")
+ print(f"Album grouping complete!\n")
elif album_info and album_info['is_album'] and is_album_download:
- print(f"\n🎯 EXPLICIT ALBUM DOWNLOAD - preserving Spotify album name: '{album_info.get('album_name', 'None')}'")
+ print(f"\nEXPLICIT ALBUM DOWNLOAD - preserving Spotify album name: '{album_info.get('album_name', 'None')}'")
print(f" Skipping smart grouping (not needed for explicit album downloads)\n")
# 1. Get transfer path (directory creation handled by _build_final_path_for_track)
file_ext = os.path.splitext(file_path)[1]
context['_audio_quality'] = _get_audio_quality_string(file_path)
if context['_audio_quality']:
- print(f"🎧 Audio quality detected: {context['_audio_quality']}")
+ print(f"Audio quality detected: {context['_audio_quality']}")
# FLAC bit depth filter
if _check_flac_bit_depth(file_path, context, context_key):
@@ -20261,46 +20261,46 @@ def _post_process_matched_download(context_key, context, file_path):
# Priority 1: Spotify clean title from context
if original_search.get('spotify_clean_title'):
clean_track_name = original_search['spotify_clean_title']
- print(f"🎵 Using Spotify clean title: '{clean_track_name}'")
+ print(f"Using Spotify clean title: '{clean_track_name}'")
# Priority 2: Album info clean name
elif album_info.get('clean_track_name'):
clean_track_name = album_info['clean_track_name']
- print(f"🎵 Using album info clean name: '{clean_track_name}'")
+ print(f"Using album info clean name: '{clean_track_name}'")
# Priority 3: Original title as fallback
else:
clean_track_name = original_search.get('title', 'Unknown Track')
- print(f"🎵 Using original title as fallback: '{clean_track_name}'")
+ print(f"Using original title as fallback: '{clean_track_name}'")
final_track_name_sanitized = _sanitize_filename(clean_track_name)
track_number = album_info['track_number']
# DEBUG: Check final track_number values
- print(f"🔍 [DEBUG] Final track_number processing:")
+ print(f"[DEBUG] Final track_number processing:")
print(f" album_info source: {album_info.get('source', 'unknown')}")
print(f" album_info track_number: {album_info.get('track_number', 'NOT_FOUND')}")
print(f" track_number variable: {track_number}")
# Fix: Handle None track_number
if track_number is None:
- print(f"⚠️ Track number is None, extracting from filename: {os.path.basename(file_path)}")
+ print(f"Track number is None, extracting from filename: {os.path.basename(file_path)}")
track_number = _extract_track_number_from_filename(file_path)
print(f" -> Extracted track number: {track_number}")
# Ensure track_number is valid
if not isinstance(track_number, int) or track_number < 1:
- print(f"⚠️ Invalid track number ({track_number}), defaulting to 1")
+ print(f"Invalid track number ({track_number}), defaulting to 1")
track_number = 1
- print(f"🎯 [DEBUG] FINAL track_number used for filename: {track_number}")
+ print(f"[DEBUG] FINAL track_number used for filename: {track_number}")
# CRITICAL FIX: Update album_info with corrected track_number for metadata enhancement
album_info['track_number'] = track_number
album_info['clean_track_name'] = clean_track_name # Ensure clean name is in album_info
- print(f"✅ [FIX] Updated album_info track_number to {track_number} for consistent metadata")
+ print(f"[FIX] Updated album_info track_number to {track_number} for consistent metadata")
# Use shared path builder for album mode
final_path, _ = _build_final_path_for_track(context, spotify_artist, album_info, file_ext)
- print(f"📁 Album path: '{final_path}'")
+ print(f"Album path: '{final_path}'")
else:
# Single track structure: Transfer/ARTIST/ARTIST - SINGLE/SINGLE.ext
# --- GUI PARITY: Use multiple sources for clean track name ---
@@ -20310,15 +20310,15 @@ def _post_process_matched_download(context_key, context, file_path):
# Priority 1: Spotify clean title from context
if original_search.get('spotify_clean_title'):
clean_track_name = original_search['spotify_clean_title']
- print(f"🎵 Using Spotify clean title: '{clean_track_name}'")
+ print(f"Using Spotify clean title: '{clean_track_name}'")
# Priority 2: Album info clean name
elif album_info and album_info.get('clean_track_name'):
clean_track_name = album_info['clean_track_name']
- print(f"🎵 Using album info clean name: '{clean_track_name}'")
+ print(f"Using album info clean name: '{clean_track_name}'")
# Priority 3: Original title as fallback
else:
clean_track_name = original_search.get('title', 'Unknown Track')
- print(f"🎵 Using original title as fallback: '{clean_track_name}'")
+ print(f"Using original title as fallback: '{clean_track_name}'")
# Ensure clean name is in album_info for path builder
if album_info:
@@ -20326,7 +20326,7 @@ def _post_process_matched_download(context_key, context, file_path):
# Use shared path builder for single mode
final_path, _ = _build_final_path_for_track(context, spotify_artist, album_info, file_ext)
- print(f"📁 Single path: '{final_path}'")
+ print(f"Single path: '{final_path}'")
# Store the actual computed path so verification uses this exact path
# instead of recomputing independently (which can produce mismatches)
@@ -20334,11 +20334,11 @@ def _post_process_matched_download(context_key, context, file_path):
# 3. Enhance metadata, move file, download art, and cleanup
try:
- print(f"🔍 [Metadata Input] artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
+ print(f"[Metadata Input] artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
if album_info:
- print(f"🔍 [Metadata Input] album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, disc#: {album_info.get('disc_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}")
+ print(f"[Metadata Input] album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, disc#: {album_info.get('disc_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}")
else:
- print(f"🔍 [Metadata Input] album_info: None (single track)")
+ print(f"[Metadata Input] album_info: None (single track)")
_enhance_file_metadata(file_path, context, spotify_artist, album_info)
except Exception as meta_err:
import traceback
@@ -20354,12 +20354,12 @@ def _post_process_matched_download(context_key, context, file_path):
_enhance_source_info = {}
is_enhance_download = _enhance_source_info.get('enhance', False)
- print(f"🚚 Moving '{os.path.basename(file_path)}' to '{final_path}'")
+ print(f"Moving '{os.path.basename(file_path)}' to '{final_path}'")
if os.path.exists(final_path):
# PROTECTION: If destination already exists, check before overwriting
# If the source file is gone, another thread already handled this - don't delete the destination
if not os.path.exists(file_path):
- print(f"✅ [Protection] Destination exists and source already gone - file already transferred: {os.path.basename(final_path)}")
+ print(f"[Protection] Destination exists and source already gone - file already transferred: {os.path.basename(final_path)}")
return
try:
from mutagen import File as MutagenFile
@@ -20374,50 +20374,50 @@ def _post_process_matched_download(context_key, context, file_path):
_incoming_tier = _get_quality_tier_from_extension(file_path)
if _incoming_tier[1] < _existing_tier[1]:
# Incoming is higher quality (lower tier number) — replace
- print(f"🔄 [Quality Replace] Replacing {_existing_tier[0]} with {_incoming_tier[0]}: {os.path.basename(final_path)}")
+ print(f"[Quality Replace] Replacing {_existing_tier[0]} with {_incoming_tier[0]}: {os.path.basename(final_path)}")
try:
os.remove(final_path)
except Exception as e:
- print(f"⚠️ [Quality Replace] Could not remove existing file: {e}")
+ print(f"[Quality Replace] Could not remove existing file: {e}")
else:
- print(f"⚠️ [Protection] Existing file is same or better quality ({_existing_tier[0]} vs {_incoming_tier[0]}) - skipping: {os.path.basename(final_path)}")
+ print(f"[Protection] Existing file is same or better quality ({_existing_tier[0]} vs {_incoming_tier[0]}) - skipping: {os.path.basename(final_path)}")
try:
os.remove(file_path)
except FileNotFoundError:
pass
except Exception as e:
- print(f"⚠️ [Protection] Error removing redundant file: {e}")
+ print(f"[Protection] Error removing redundant file: {e}")
return
else:
- print(f"⚠️ [Protection] Existing file already has metadata enhancement - skipping overwrite: {os.path.basename(final_path)}")
- print(f"🗑️ [Protection] Removing redundant download file: {os.path.basename(file_path)}")
+ print(f"[Protection] Existing file already has metadata enhancement - skipping overwrite: {os.path.basename(final_path)}")
+ print(f"[Protection] Removing redundant download file: {os.path.basename(file_path)}")
try:
os.remove(file_path)
except FileNotFoundError:
- print(f"⚠️ [Protection] Could not remove redundant file (already gone): {file_path}")
+ print(f"[Protection] Could not remove redundant file (already gone): {file_path}")
except Exception as e:
- print(f"⚠️ [Protection] Error removing redundant file: {e}")
+ print(f"[Protection] Error removing redundant file: {e}")
return # Don't overwrite the good file
elif is_enhance_download:
# ENHANCE BYPASS: Allow overwrite — backup original, then remove to allow move
- print(f"🔄 [Enhance] Quality enhance mode — replacing existing file: {os.path.basename(final_path)}")
+ print(f"[Enhance] Quality enhance mode — replacing existing file: {os.path.basename(final_path)}")
try:
os.remove(final_path)
except Exception as e:
- print(f"⚠️ [Enhance] Could not remove existing file for replacement: {e}")
+ print(f"[Enhance] Could not remove existing file for replacement: {e}")
else:
- print(f"🔄 [Protection] Existing file lacks metadata - safe to overwrite: {os.path.basename(final_path)}")
+ print(f"[Protection] Existing file lacks metadata - safe to overwrite: {os.path.basename(final_path)}")
try:
os.remove(final_path)
except FileNotFoundError:
pass # It was just there, but now gone?
except Exception as check_error:
- print(f"⚠️ [Protection] Error checking existing file metadata, proceeding with overwrite: {check_error}")
+ print(f"[Protection] Error checking existing file metadata, proceeding with overwrite: {check_error}")
try:
if os.path.exists(final_path):
os.remove(final_path)
except Exception as e:
- print(f"⚠️ [Protection] Failed to remove existing file for overwrite: {e}")
+ print(f"[Protection] Failed to remove existing file for overwrite: {e}")
# --- PRE-MOVE SOURCE CHECK ---
# Right before moving, verify the source file still exists.
@@ -20425,7 +20425,7 @@ def _post_process_matched_download(context_key, context, file_path):
# already moved this file during the sleep + metadata enhancement window.
if not os.path.exists(file_path):
if os.path.exists(final_path):
- print(f"✅ [Pre-Move] Source already gone and destination exists - another thread completed transfer: {os.path.basename(final_path)}")
+ print(f"[Pre-Move] Source already gone and destination exists - another thread completed transfer: {os.path.basename(final_path)}")
# Still do cover art + lyrics since the other thread might not have finished those
_download_cover_art(album_info, os.path.dirname(final_path), context)
_generate_lrc_file(final_path, context, spotify_artist, album_info)
@@ -20453,13 +20453,13 @@ def _post_process_matched_download(context_key, context, file_path):
found_variant = os.path.join(expected_dir, f)
break
if found_variant:
- print(f"✅ [Pre-Move] Source gone but found variant in destination (stream processor handled it): {os.path.basename(found_variant)}")
+ print(f"[Pre-Move] Source gone but found variant in destination (stream processor handled it): {os.path.basename(found_variant)}")
context['_final_processed_path'] = found_variant
_download_cover_art(album_info, expected_dir, context)
_generate_lrc_file(found_variant, context, spotify_artist, album_info)
return
else:
- print(f"⚠️ [Pre-Move] Source file gone and no matching file in destination: {os.path.basename(file_path)}")
+ print(f"[Pre-Move] Source file gone and no matching file in destination: {os.path.basename(file_path)}")
raise FileNotFoundError(f"Source file vanished before move and destination does not exist: {file_path}")
_safe_move_file(file_path, final_path)
@@ -20472,13 +20472,13 @@ def _post_process_matched_download(context_key, context, file_path):
os.remove(original_enhance_path)
old_fmt = os.path.splitext(original_enhance_path)[1]
new_fmt = os.path.splitext(final_path)[1]
- print(f"✅ [Enhance] Upgraded {old_fmt} → {new_fmt}: {os.path.basename(final_path)}")
+ print(f"[Enhance] Upgraded {old_fmt} → {new_fmt}: {os.path.basename(final_path)}")
except Exception as e:
- print(f"⚠️ [Enhance] Could not remove old-format file: {e}")
+ print(f"[Enhance] Could not remove old-format file: {e}")
elif is_enhance_download:
old_fmt = _enhance_source_info.get('original_format', 'unknown')
new_fmt = os.path.splitext(final_path)[1]
- print(f"✅ [Enhance] Replaced in-place ({old_fmt} → {new_fmt}): {os.path.basename(final_path)}")
+ print(f"[Enhance] Replaced in-place ({old_fmt} → {new_fmt}): {os.path.basename(final_path)}")
_download_cover_art(album_info, os.path.dirname(final_path), context)
@@ -20499,7 +20499,7 @@ def _post_process_matched_download(context_key, context, file_path):
downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
_cleanup_empty_directories(downloads_path, file_path)
- print(f"✅ Post-processing complete for: {context.get('_final_processed_path', final_path)}")
+ print(f"Post-processing complete for: {context.get('_final_processed_path', final_path)}")
_emit_track_downloaded(context)
_record_library_history_download(context)
@@ -20511,7 +20511,7 @@ def _post_process_matched_download(context_key, context, file_path):
completed_path = context.get('_final_processed_path', final_path)
_record_retag_download(context, spotify_artist, album_info, completed_path)
except Exception as retag_err:
- print(f"⚠️ [Post-Process] Retag data capture failed (non-fatal): {retag_err}")
+ print(f"[Post-Process] Retag data capture failed (non-fatal): {retag_err}")
# REPAIR: Register album folder for repair scanning when batch completes
try:
@@ -20522,19 +20522,19 @@ def _post_process_matched_download(context_key, context, file_path):
if album_folder:
repair_worker.register_folder(batch_id_for_repair, album_folder)
except Exception as repair_err:
- print(f"⚠️ [Post-Process] Repair folder registration failed: {repair_err}")
+ print(f"[Post-Process] Repair folder registration failed: {repair_err}")
# WISHLIST REMOVAL: Check if this track should be removed from wishlist after successful download
try:
_check_and_remove_from_wishlist(context)
except Exception as wishlist_error:
- print(f"⚠️ [Post-Process] Error checking wishlist removal: {wishlist_error}")
+ print(f"[Post-Process] Error checking wishlist removal: {wishlist_error}")
# Call completion callback for missing downloads tasks to start next batch
task_id = context.get('task_id')
batch_id = context.get('batch_id')
if task_id and batch_id:
- print(f"🎯 [Post-Process] Calling completion callback for task {task_id} in batch {batch_id}")
+ print(f"[Post-Process] Calling completion callback for task {task_id} in batch {batch_id}")
# Mark task as stream processed and set terminal status so
# _validate_worker_counts won't count this task as active
@@ -20546,7 +20546,7 @@ def _post_process_matched_download(context_key, context, file_path):
if task_id in download_tasks:
download_tasks[task_id]['stream_processed'] = True
download_tasks[task_id]['status'] = 'completed'
- print(f"✅ [Post-Process] Marked task {task_id} as completed")
+ print(f"[Post-Process] Marked task {task_id} as completed")
_on_download_completed(batch_id, task_id, success=True)
@@ -20554,7 +20554,7 @@ def _post_process_matched_download(context_key, context, file_path):
import traceback
pp_logger.info(f"[inner] EXCEPTION in post-processing for {context_key}: {e}")
pp_logger.info(traceback.format_exc())
- print(f"\n❌ CRITICAL ERROR in post-processing for {context_key}: {e}")
+ print(f"\nCRITICAL ERROR in post-processing for {context_key}: {e}")
traceback.print_exc()
# Only retry if the source file still exists - otherwise retrying is pointless
@@ -20565,15 +20565,15 @@ def _post_process_matched_download(context_key, context, file_path):
# Remove from processed set so it can be retried
if context_key in _processed_download_ids:
_processed_download_ids.remove(context_key)
- print(f"🔄 Removed {context_key} from processed set - will retry on next check")
+ print(f"Removed {context_key} from processed set - will retry on next check")
# Re-add to matched context for retry
with matched_context_lock:
if context_key not in matched_downloads_context:
matched_downloads_context[context_key] = context
- print(f"♻️ Re-added {context_key} to context for retry")
+ print(f"Re-added {context_key} to context for retry")
else:
- print(f"⚠️ Source file gone, not retrying: {context_key}")
+ print(f"Source file gone, not retrying: {context_key}")
finally:
file_lock.release()
# Clean up the lock entry to prevent unbounded memory growth
@@ -20671,7 +20671,7 @@ def _record_retag_download(context, spotify_artist, album_info, final_path):
title=title, file_path=str(final_path), file_format=file_format,
spotify_track_id=spotify_track_id, itunes_track_id=itunes_track_id
)
- print(f"📝 [Retag] Recorded track for retag: '{title}' in '{album_name}'")
+ print(f"[Retag] Recorded track for retag: '{title}' in '{album_name}'")
# Cap retag groups at 100, remove oldest
db.trim_retag_groups(100)
@@ -20770,7 +20770,7 @@ def _execute_retag(group_id, album_id):
if best_match:
matched_pairs.append((existing_track, best_match))
else:
- print(f"⚠️ [Retag] No match found for track: '{existing_track.get('title')}'")
+ print(f"[Retag] No match found for track: '{existing_track.get('title')}'")
matched_pairs.append((existing_track, None))
with retag_lock:
@@ -20792,7 +20792,7 @@ def _execute_retag(group_id, album_id):
# Verify file exists
if not os.path.exists(current_file_path):
- print(f"⚠️ [Retag] File not found, skipping: {current_file_path}")
+ print(f"[Retag] File not found, skipping: {current_file_path}")
with retag_lock:
retag_state['processed'] += 1
retag_state['progress'] = int(retag_state['processed'] / retag_state['total_tracks'] * 100)
@@ -20834,9 +20834,9 @@ def _execute_retag(group_id, album_id):
# Re-write metadata tags
try:
_enhance_file_metadata(current_file_path, context, new_artist, album_info)
- print(f"✅ [Retag] Re-tagged: '{track_title}'")
+ print(f"[Retag] Re-tagged: '{track_title}'")
except Exception as meta_err:
- print(f"⚠️ [Retag] Metadata write failed for '{track_title}': {meta_err}")
+ print(f"[Retag] Metadata write failed for '{track_title}': {meta_err}")
# Compute new path and move if different
file_ext = os.path.splitext(current_file_path)[1]
@@ -20844,7 +20844,7 @@ def _execute_retag(group_id, album_id):
new_path, _ = _build_final_path_for_track(context, new_artist, album_info, file_ext)
if os.path.normpath(current_file_path) != os.path.normpath(new_path):
- print(f"🚚 [Retag] Moving '{os.path.basename(current_file_path)}' -> '{new_path}'")
+ print(f"[Retag] Moving '{os.path.basename(current_file_path)}' -> '{new_path}'")
old_dir = os.path.dirname(current_file_path)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
_safe_move_file(current_file_path, new_path)
@@ -20856,9 +20856,9 @@ def _execute_retag(group_id, album_id):
new_lyrics = os.path.splitext(new_path)[0] + lyrics_ext
try:
_safe_move_file(old_lyrics, new_lyrics)
- print(f"📝 [Retag] Moved {lyrics_ext} file alongside audio")
+ print(f"[Retag] Moved {lyrics_ext} file alongside audio")
except Exception as lrc_err:
- print(f"⚠️ [Retag] Failed to move {lyrics_ext} file: {lrc_err}")
+ print(f"[Retag] Failed to move {lyrics_ext} file: {lrc_err}")
# Remove old cover.jpg if directory changed and old dir is now empty of audio
new_dir = os.path.dirname(new_path)
@@ -20872,7 +20872,7 @@ def _execute_retag(group_id, album_id):
if not remaining_audio:
try:
os.remove(old_cover)
- print(f"🗑️ [Retag] Removed orphaned cover.jpg from old directory")
+ print(f"[Retag] Removed orphaned cover.jpg from old directory")
except Exception:
pass
@@ -20884,15 +20884,15 @@ def _execute_retag(group_id, album_id):
db.update_retag_track_path(existing_track['id'], str(new_path))
current_file_path = new_path
else:
- print(f"📍 [Retag] Path unchanged for '{track_title}', no move needed")
+ print(f"[Retag] Path unchanged for '{track_title}', no move needed")
except Exception as move_err:
- print(f"⚠️ [Retag] Path/move failed for '{track_title}': {move_err}")
+ print(f"[Retag] Path/move failed for '{track_title}': {move_err}")
# Download cover art to album directory
try:
_download_cover_art(album_info, os.path.dirname(current_file_path), context)
except Exception as cover_err:
- print(f"⚠️ [Retag] Cover art download failed: {cover_err}")
+ print(f"[Retag] Cover art download failed: {cover_err}")
with retag_lock:
retag_state['processed'] += 1
@@ -20923,11 +20923,11 @@ def _execute_retag(group_id, album_id):
"progress": 100,
"current_track": ""
})
- print(f"✅ [Retag] Retag operation complete for group {group_id}")
+ print(f"[Retag] Retag operation complete for group {group_id}")
except Exception as e:
import traceback
- print(f"❌ [Retag] Error during retag: {e}")
+ print(f"[Retag] Error during retag: {e}")
print(traceback.format_exc())
with retag_lock:
retag_state.update({
@@ -20953,17 +20953,17 @@ def _check_and_remove_from_wishlist(context):
track_info = context.get('track_info', {})
if track_info.get('id'):
spotify_track_id = track_info['id']
- print(f"📋 [Wishlist] Found Spotify ID from track_info: {spotify_track_id}")
+ print(f"[Wishlist] Found Spotify ID from track_info: {spotify_track_id}")
# Method 2: From original search result
elif context.get('original_search_result', {}).get('id'):
spotify_track_id = context['original_search_result']['id']
- print(f"📋 [Wishlist] Found Spotify ID from original_search_result: {spotify_track_id}")
+ print(f"[Wishlist] Found Spotify ID from original_search_result: {spotify_track_id}")
# Method 3: Check if this is a wishlist download (context has wishlist_id)
elif 'wishlist_id' in track_info:
wishlist_id = track_info['wishlist_id']
- print(f"📋 [Wishlist] Found wishlist_id in context: {wishlist_id}")
+ print(f"[Wishlist] Found wishlist_id in context: {wishlist_id}")
# Get the Spotify track ID from the wishlist entry (search all profiles)
database = get_database()
@@ -20974,7 +20974,7 @@ def _check_and_remove_from_wishlist(context):
for wl_track in wishlist_tracks:
if wl_track.get('wishlist_id') == wishlist_id:
spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id')
- print(f"📋 [Wishlist] Found Spotify ID from wishlist entry: {spotify_track_id}")
+ print(f"[Wishlist] Found Spotify ID from wishlist entry: {spotify_track_id}")
break
# Method 4: Try to construct ID from track metadata for fuzzy matching
@@ -20983,7 +20983,7 @@ def _check_and_remove_from_wishlist(context):
artist_name = _get_track_artist_name(track_info) or _get_track_artist_name(context.get('original_search_result', {}))
if track_name and artist_name:
- print(f"📋 [Wishlist] No Spotify ID found, checking for fuzzy match: '{track_name}' by '{artist_name}'")
+ print(f"[Wishlist] No Spotify ID found, checking for fuzzy match: '{track_name}' by '{artist_name}'")
# Get all wishlist tracks and find potential matches (search all profiles)
if not wishlist_tracks:
@@ -21007,22 +21007,22 @@ def _check_and_remove_from_wishlist(context):
# Simple fuzzy matching
if (wl_name == track_name.lower() and wl_artist_name == artist_name.lower()):
spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id')
- print(f"📋 [Wishlist] Found fuzzy match - Spotify ID: {spotify_track_id}")
+ print(f"[Wishlist] Found fuzzy match - Spotify ID: {spotify_track_id}")
break
# If we found a Spotify track ID, remove it from wishlist
if spotify_track_id:
- print(f"📋 [Wishlist] Attempting to remove track from wishlist: {spotify_track_id}")
+ print(f"[Wishlist] Attempting to remove track from wishlist: {spotify_track_id}")
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
if removed:
- print(f"✅ [Wishlist] Successfully removed track from wishlist: {spotify_track_id}")
+ print(f"[Wishlist] Successfully removed track from wishlist: {spotify_track_id}")
else:
print(f"ℹ️ [Wishlist] Track not found in wishlist or already removed: {spotify_track_id}")
else:
print(f"ℹ️ [Wishlist] No Spotify track ID found for wishlist removal check")
except Exception as e:
- print(f"❌ [Wishlist] Error in wishlist removal check: {e}")
+ print(f"[Wishlist] Error in wishlist removal check: {e}")
import traceback
traceback.print_exc()
@@ -21040,13 +21040,13 @@ def _check_and_remove_track_from_wishlist_by_metadata(track_data):
track_id = track_data.get('id', '')
artists = track_data.get('artists', [])
- print(f"📋 [Analysis] Checking if track should be removed from wishlist: '{track_name}' (ID: {track_id})")
+ print(f"[Analysis] Checking if track should be removed from wishlist: '{track_name}' (ID: {track_id})")
# Method 1: Direct Spotify ID match
if track_id:
removed = wishlist_service.mark_track_download_result(track_id, success=True)
if removed:
- print(f"✅ [Analysis] Removed track from wishlist via direct ID match: {track_id}")
+ print(f"[Analysis] Removed track from wishlist via direct ID match: {track_id}")
return True
# Method 2: Fuzzy matching by name and artist if no direct ID match
@@ -21060,7 +21060,7 @@ def _check_and_remove_track_from_wishlist_by_metadata(track_data):
else:
primary_artist = str(artists[0])
- print(f"📋 [Analysis] No direct ID match, trying fuzzy match: '{track_name}' by '{primary_artist}'")
+ print(f"[Analysis] No direct ID match, trying fuzzy match: '{track_name}' by '{primary_artist}'")
# Get all wishlist tracks and find matches (search all profiles)
database = get_database()
@@ -21086,14 +21086,14 @@ def _check_and_remove_track_from_wishlist_by_metadata(track_data):
if spotify_track_id:
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
if removed:
- print(f"✅ [Analysis] Removed track from wishlist via fuzzy match: {spotify_track_id}")
+ print(f"[Analysis] Removed track from wishlist via fuzzy match: {spotify_track_id}")
return True
print(f"ℹ️ [Analysis] Track not found in wishlist or already removed: '{track_name}'")
return False
except Exception as e:
- print(f"❌ [Analysis] Error checking wishlist removal by metadata: {e}")
+ print(f"[Analysis] Error checking wishlist removal by metadata: {e}")
import traceback
traceback.print_exc()
return False
@@ -21111,7 +21111,7 @@ def _automatic_wishlist_cleanup_after_db_update():
db = MusicDatabase()
active_server = config_manager.get_active_media_server()
- print("📋 [Auto Cleanup] Starting automatic wishlist cleanup after database update...")
+ print("[Auto Cleanup] Starting automatic wishlist cleanup after database update...")
# Get all wishlist tracks (across all profiles - cleanup is global)
database = get_database()
@@ -21120,10 +21120,10 @@ def _automatic_wishlist_cleanup_after_db_update():
for p in all_profiles:
wishlist_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id']))
if not wishlist_tracks:
- print("📋 [Auto Cleanup] No tracks in wishlist to clean up")
+ print("[Auto Cleanup] No tracks in wishlist to clean up")
return
- print(f"📋 [Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
+ print(f"[Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
removed_count = 0
@@ -21158,11 +21158,11 @@ def _automatic_wishlist_cleanup_after_db_update():
if db_track and confidence >= 0.7:
found_in_db = True
- print(f"📋 [Auto Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
+ print(f"[Auto Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
break
except Exception as db_error:
- print(f"⚠️ [Auto Cleanup] Error checking database for track '{track_name}': {db_error}")
+ print(f"[Auto Cleanup] Error checking database for track '{track_name}': {db_error}")
continue
# If found in database, remove from wishlist
@@ -21171,14 +21171,14 @@ def _automatic_wishlist_cleanup_after_db_update():
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
if removed:
removed_count += 1
- print(f"✅ [Auto Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
+ print(f"[Auto Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
except Exception as remove_error:
- print(f"❌ [Auto Cleanup] Error removing track from wishlist: {remove_error}")
+ print(f"[Auto Cleanup] Error removing track from wishlist: {remove_error}")
- print(f"📋 [Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist")
+ print(f"[Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist")
except Exception as e:
- print(f"❌ [Auto Cleanup] Error in automatic wishlist cleanup: {e}")
+ print(f"[Auto Cleanup] Error in automatic wishlist cleanup: {e}")
import traceback
traceback.print_exc()
@@ -21253,7 +21253,7 @@ def get_version_info():
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
{
- "title": "🎬 Music Videos — Search & Download from YouTube",
+ "title": "Music Videos — Search & Download from YouTube",
"description": "New Music Videos tab in enhanced and global search for finding and downloading music videos",
"features": [
"• Music Videos pill tab alongside Spotify/Deezer/iTunes/Discogs in both search bars",
@@ -21266,7 +21266,7 @@ def get_version_info():
"usage_note": "Set your Music Videos directory in Settings > Downloads, then search for any artist in the search bar and click the Music Videos tab."
},
{
- "title": "📦 Lidarr Download Source (Development)",
+ "title": "Lidarr Download Source (Development)",
"description": "Use Lidarr as a download source for Usenet and torrent content",
"features": [
"• 7th download source alongside Soulseek, YouTube, Tidal, Qobuz, HiFi, and Deezer",
@@ -21278,7 +21278,7 @@ def get_version_info():
"usage_note": "Requires a running Lidarr instance with configured indexers and download clients. Set Download Source to 'Lidarr Only (Development)' or add to Hybrid order."
},
{
- "title": "🔧 Metadata Pipeline Overhaul — Fix Unknown Artist & Source Selection",
+ "title": "Metadata Pipeline Overhaul — Fix Unknown Artist & Source Selection",
"description": "Major fix for tracks downloading as 'Unknown Artist' and Spotify being used when Deezer/iTunes was selected",
"features": [
"• Fixed playlist pipeline (discover → sync → wishlist → download) losing artist, track number, and album year data",
@@ -21292,7 +21292,7 @@ def get_version_info():
"usage_note": "If you have existing Unknown Artist tracks, run the Fix Unknown Artists job from Settings > Maintenance."
},
{
- "title": "🛡️ Matching Engine — Artist Verification Gate",
+ "title": "Matching Engine — Artist Verification Gate",
"description": "Prevents downloading tracks from completely wrong artists on Soulseek and YouTube",
"features": [
"• New artist gate rejects candidates where the artist doesn't match the target (Soulseek: < 0.25, YouTube: < 0.15)",
@@ -21304,7 +21304,7 @@ def get_version_info():
"usage_note": "No action needed — matching improvements apply automatically to all new downloads."
},
{
- "title": "🎵 Deezer User Playlists — Browse & Download Your Library",
+ "title": "Deezer User Playlists — Browse & Download Your Library",
"description": "New Deezer tab on the Sync page shows your personal playlists via ARL token — same flow as Spotify",
"features": [
"• Click Refresh to load all your Deezer playlists with track counts",
@@ -21316,7 +21316,7 @@ def get_version_info():
"usage_note": "Configure your ARL token in Settings > Connections or Downloads, then open the Deezer tab on the Sync page."
},
{
- "title": "🔒 Qobuz Token Auth — CAPTCHA Bypass",
+ "title": "Qobuz Token Auth — CAPTCHA Bypass",
"description": "Qobuz added reCAPTCHA to their login — token auth lets you paste your session token directly",
"features": [
"• New 'Auth Token' field on both Connections and Downloads tabs for Qobuz",
@@ -21327,7 +21327,7 @@ def get_version_info():
"usage_note": "If Qobuz email/password login fails, use the Auth Token field instead."
},
{
- "title": "🛡️ Streaming Source Matching — Artist Gate",
+ "title": "Streaming Source Matching — Artist Gate",
"description": "Tidal, Qobuz, HiFi, and Deezer downloads no longer match to wrong artists",
"features": [
"• Artist similarity gate rejects candidates below 0.4 match threshold",
@@ -21339,7 +21339,7 @@ def get_version_info():
"usage_note": "Downloads from official sources are now much more accurate. Check Download History for verification details."
},
{
- "title": "📋 Download History — Source Provenance",
+ "title": "Download History — Source Provenance",
"description": "Collapsible download history with full source tracking and AcoustID verification badges",
"features": [
"• Expected vs Downloaded comparison — shows what you asked for vs what the source provided",
@@ -21351,7 +21351,7 @@ def get_version_info():
"usage_note": "Click 'Download History' on the Dashboard to see source provenance for new downloads."
},
{
- "title": "🔧 Fixes & Improvements",
+ "title": "Fixes & Improvements",
"description": "Bug fixes, quality of life improvements, and new settings",
"features": [
"• Dismissed maintenance findings no longer reappear on next scan — dedup check now includes dismissed status",
@@ -21382,7 +21382,7 @@ def get_version_info():
]
},
{
- "title": "🗺️ Artist Map — Visualize Your Music Universe",
+ "title": "Artist Map — Visualize Your Music Universe",
"description": "Three interactive canvas-based visualization modes on the Discover page",
"features": [
"• Watchlist Constellation — your watched artists as large nodes with similar artists orbiting around them",
@@ -21401,7 +21401,7 @@ def get_version_info():
"usage_note": "Navigate to Discover and click the Artist Map section. Choose Watchlist, Genre, or Explorer mode."
},
{
- "title": "⚡ Wing It — Download or Sync Without Discovery",
+ "title": "Wing It — Download or Sync Without Discovery",
"description": "Bypass metadata discovery and use raw track names directly",
"features": [
"• Wing It button on all discovery modals and ListenBrainz Discover page cards",
@@ -21412,10 +21412,10 @@ def get_version_info():
"• Live sync progress displayed inline just like normal sync",
"• Download creates a bubble on the dashboard for progress tracking"
],
- "usage_note": "Click the ⚡ Wing It button next to Start Discovery or Download Missing in any playlist modal."
+ "usage_note": "Click the Wing It button next to Start Discovery or Download Missing in any playlist modal."
},
{
- "title": "🔍 Global Search Bar — Search From Anywhere",
+ "title": "Global Search Bar — Search From Anywhere",
"description": "Spotlight-style search bar accessible from every page",
"features": [
"• Persistent search bar at the bottom of the screen — faded when idle, expands on focus",
@@ -21429,7 +21429,7 @@ def get_version_info():
"usage_note": "Press / or Ctrl+K from any page, or click the search bar at the bottom of the screen."
},
{
- "title": "🔔 Redesigned Notification System",
+ "title": "Redesigned Notification System",
"description": "Modern compact toasts with notification history and bell button",
"features": [
"• Compact pill-shaped toasts in the bottom-right — one at a time, auto-dismiss after 3.5s",
@@ -21442,7 +21442,7 @@ def get_version_info():
]
},
{
- "title": "🔄 Track Redownload — Fix Mismatched Downloads",
+ "title": "Track Redownload — Fix Mismatched Downloads",
"description": "Replace wrong downloads with the correct version using manual source selection",
"features": [
"• Redownload button (↻) on each track in the enhanced library view",
@@ -21456,10 +21456,10 @@ def get_version_info():
"• Download Blacklist: block specific sources from the Source Info popover — blacklisted sources skipped in all future downloads",
"• Blacklist viewer on dashboard Tools section with remove capability"
],
- "usage_note": "In the enhanced library view, click ↻ to redownload, ℹ for source info, or ✕ to delete."
+ "usage_note": "In the enhanced library view, click ↻ to redownload, ℹ for source info, or to delete."
},
{
- "title": "⚡ Spotify API Rate Limit Improvements",
+ "title": "Spotify API Rate Limit Improvements",
"description": "Reduced Spotify API usage through caching and smart worker management",
"features": [
"• get_artist_albums now cached — discography views, completion badges hit cache instead of API",
@@ -21472,7 +21472,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Additional Fixes",
+ "title": "Additional Fixes",
"description": "Bug fixes and quality-of-life improvements",
"features": [
"• $discnum template variable — unpadded disc number for multi-disc album path templates",
@@ -21485,7 +21485,7 @@ def get_version_info():
"• Genius API interval increased from 1.5s to 2s to reduce 429 rate limits",
"• MusicBrainz cache now visible in Cache Browser with browse, clear, and clear-failed-only options",
"• Cache Health popup shows MusicBrainz alongside other sources, 'Failed Lookups' clarified as MB-specific",
- "• Block artists from discovery — hover any track in a discovery playlist and click ✕ to permanently exclude that artist",
+ "• Block artists from discovery — hover any track in a discovery playlist and click to permanently exclude that artist",
"• Configurable concurrent downloads (1-10) — Settings → Downloads, Soulseek albums stay at 1",
"• Streaming search sources — Apple Music results load progressively instead of blocking for 9+ seconds",
"• API Rate Monitor — real-time speedometer gauges for all services on Dashboard, click for 24h history",
@@ -21508,7 +21508,7 @@ def get_version_info():
]
},
{
- "title": "🖥️ Server Playlist Manager — Compare & Fix Matches",
+ "title": "Server Playlist Manager — Compare & Fix Matches",
"description": "Review and fix track matches between your source playlists and media server",
"features": [
"• New Server Playlists tab (default on Sync page) — shows server playlists that match your mirrored playlists",
@@ -21526,7 +21526,7 @@ def get_version_info():
"usage_note": "Navigate to Sync → Server Playlists tab. Click any playlist card to open the comparison editor."
},
{
- "title": "📊 Sync History Dashboard with Per-Track Details",
+ "title": "Sync History Dashboard with Per-Track Details",
"description": "Dashboard shows recent syncs as visual cards with full per-track match data",
"features": [
"• Recent Syncs section on dashboard with scrolling cards showing match percentage and health indicators",
@@ -21537,7 +21537,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Japanese Song Searches Producing Gibberish",
+ "title": "Fix Japanese Song Searches Producing Gibberish",
"description": "CJK text no longer mangled by unidecode in Soulseek search queries",
"features": [
"• Japanese kanji, hiragana, katakana, and Korean hangul preserved in search queries",
@@ -21546,7 +21546,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Partial Name Matching False Positives (#225)",
+ "title": "Fix Partial Name Matching False Positives (#225)",
"description": "Track ownership check no longer falsely matches prefix/suffix variations",
"features": [
"• 'Believe' no longer matches 'Believe In Me' — length ratio penalty prevents partial title matches",
@@ -21555,7 +21555,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Pipeline Stops When Metadata Match Fails (#224)",
+ "title": "Fix Pipeline Stops When Metadata Match Fails (#224)",
"description": "Playlist sync no longer drops tracks that failed iTunes/Apple Music discovery",
"features": [
"• Tracks that fail metadata discovery now continue through the pipeline using original playlist data",
@@ -21564,7 +21564,7 @@ def get_version_info():
]
},
{
- "title": "🌳 Playlist Explorer — Visual Discovery Tree",
+ "title": "Playlist Explorer — Visual Discovery Tree",
"description": "Use playlists as seeds to discover full albums and discographies",
"features": [
"• New Explorer page with interactive tree visualization",
@@ -21578,7 +21578,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix .LRC Files Written Without Timestamps",
+ "title": "Fix .LRC Files Written Without Timestamps",
"description": "Plain lyrics now saved as .txt instead of invalid .lrc files",
"features": [
"• Synced (timestamped) lyrics → .lrc file — valid format for Plex, Navidrome, Jellyfin",
@@ -21588,7 +21588,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Collaborative Album Artist Not Applied to Singles (#215)",
+ "title": "Fix Collaborative Album Artist Not Applied to Singles (#215)",
"description": "Single path template now respects the First Listed Artist setting",
"features": [
"• Single downloads now include structured artists list for collab artist extraction",
@@ -21597,7 +21597,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Enrichment Overwriting Manual Matches (#221)",
+ "title": "Fix Enrichment Overwriting Manual Matches (#221)",
"description": "Enriching an entity that was manually matched no longer reverts the status to not_found",
"features": [
"• Genius and AudioDB workers now check for existing service IDs before searching by name",
@@ -21608,7 +21608,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Spotify OAuth ERR_EMPTY_RESPONSE in Docker (#220)",
+ "title": "Fix Spotify OAuth ERR_EMPTY_RESPONSE in Docker (#220)",
"description": "OAuth callback server hardened for Docker/SSH tunnel setups",
"features": [
"• Top-level error handler ensures an HTTP response is always sent (no more ERR_EMPTY_RESPONSE)",
@@ -21619,7 +21619,7 @@ def get_version_info():
]
},
{
- "title": "📊 Show All Services on Dashboard (#219)",
+ "title": "Show All Services on Dashboard (#219)",
"description": "Dashboard now shows connection status for all external services, not just the core three",
"features": [
"• Enrichment services shown as color-coded chips below core service cards",
@@ -21632,7 +21632,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Add Qobuz to Connections Tab (#218)",
+ "title": "Add Qobuz to Connections Tab (#218)",
"description": "Qobuz credentials now available on the Connections tab for metadata enrichment",
"features": [
"• New Qobuz section on Settings → Connections tab for enrichment auth",
@@ -21641,7 +21641,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Enrichment Widget Showing 'Running' When Rate Limited",
+ "title": "Fix Enrichment Widget Showing 'Running' When Rate Limited",
"description": "Enrichment tooltip now shows Rate Limited or Daily Limit Reached instead of stuck on Running",
"features": [
"• Shows 'Rate Limited' with countdown when Spotify rate limit is active",
@@ -21650,7 +21650,7 @@ def get_version_info():
]
},
{
- "title": "🧹 Metadata Cache Maintenance",
+ "title": "Metadata Cache Maintenance",
"description": "The cache evictor now runs four maintenance phases to keep the metadata cache clean",
"features": [
"• Input validation prevents junk entities (Unknown Artist, empty names) from being cached",
@@ -21661,7 +21661,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Wishlist Download Selection Ignoring Checkboxes",
+ "title": "Fix Wishlist Download Selection Ignoring Checkboxes",
"description": "Download Selection now respects which tracks are checked in the wishlist overview",
"features": [
"• Selected track IDs are collected before closing the overview modal",
@@ -21670,7 +21670,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Tidal OAuth Redirect URI in Docker",
+ "title": "Fix Tidal OAuth Redirect URI in Docker",
"description": "Tidal OAuth now uses the configured redirect URI instead of the Docker container hostname",
"features": [
"• Respects the redirect URI set in Settings instead of overriding with request hostname",
@@ -21679,7 +21679,7 @@ def get_version_info():
]
},
{
- "title": "🎨 High-Resolution Cover Art from Cover Art Archive",
+ "title": "High-Resolution Cover Art from Cover Art Archive",
"description": "Album art now sourced from Cover Art Archive when available — often 1200x1200+ original quality",
"features": [
"• Tries Cover Art Archive first using MusicBrainz release ID (full resolution)",
@@ -21688,7 +21688,7 @@ def get_version_info():
]
},
{
- "title": "🎵 Embedded Lyrics in Audio Files",
+ "title": "Embedded Lyrics in Audio Files",
"description": "Lyrics are now embedded directly in audio file tags alongside the .lrc sidecar file",
"features": [
"• Lyrics embedded as USLT (MP3), lyrics (FLAC/OGG), or ©lyr (M4A) tags",
@@ -21697,7 +21697,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix AcoustID False Positives for Non-English Tracks",
+ "title": "Fix AcoustID False Positives for Non-English Tracks",
"description": "AcoustID no longer quarantines correct files when titles are in different languages",
"features": [
"• High-confidence fingerprint matches (95%+) now SKIP instead of FAIL when title/artist don't match",
@@ -21706,7 +21706,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Soulseek Junk Tags Surviving Post-Processing",
+ "title": "Fix Soulseek Junk Tags Surviving Post-Processing",
"description": "Tags from Soulseek source files are now wiped to disk immediately, before metadata enhancement",
"features": [
"• Clears and saves tags before any API calls or metadata extraction",
@@ -21716,7 +21716,7 @@ def get_version_info():
]
},
{
- "title": "👁️ Watch All Unwatched Preview Modal",
+ "title": "Watch All Unwatched Preview Modal",
"description": "The Watch All Unwatched button now opens a modal showing exactly which artists will be added",
"features": [
"• Preview list shows all eligible artists with images, track counts, and matched sources",
@@ -21727,7 +21727,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Watch All Unwatched Skipping Deezer Artists",
+ "title": "Fix Watch All Unwatched Skipping Deezer Artists",
"description": "Watch All Unwatched now supports Deezer as an ID source",
"features": [
"• Added Deezer ID support to the bulk watchlist add flow",
@@ -21736,7 +21736,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Library Maintenance Path Fixes Failing Silently",
+ "title": "Fix Library Maintenance Path Fixes Failing Silently",
"description": "Path mismatch fixes now use fresh config and report errors to the UI",
"features": [
"• Transfer folder path is re-read from config before each fix attempt",
@@ -21745,7 +21745,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Fix Spotify Manual Match Storing Wrong IDs",
+ "title": "Fix Spotify Manual Match Storing Wrong IDs",
"description": "Manual match modals no longer store iTunes/Deezer IDs in Spotify ID columns",
"features": [
"• Detects actual provider from result IDs — Spotify IDs are alphanumeric, iTunes/Deezer are numeric",
@@ -21755,7 +21755,7 @@ def get_version_info():
]
},
{
- "title": "🛡️ Spotify Enrichment Daily Budget",
+ "title": "Spotify Enrichment Daily Budget",
"description": "The background enrichment worker now caps itself at 3,000 items per day to prevent rate limit bans",
"features": [
"• Worker-only daily budget — user-initiated searches, playlist operations, etc. are unaffected",
@@ -21765,7 +21765,7 @@ def get_version_info():
]
},
{
- "title": "🎧 Deezer Download Source",
+ "title": "Deezer Download Source",
"description": "Download music directly from Deezer with ARL authentication",
"features": [
"• New download source: Deezer joins Soulseek, YouTube, Tidal, Qobuz, and HiFi",
@@ -21777,7 +21777,7 @@ def get_version_info():
]
},
{
- "title": "🔍 Cache-Powered Discovery",
+ "title": "Cache-Powered Discovery",
"description": "Five new discover sections mined from your metadata cache — zero API calls",
"features": [
"• Undiscovered Albums: albums by your most-played artists that aren't in your library",
@@ -21789,7 +21789,7 @@ def get_version_info():
]
},
{
- "title": "🎶 Genre Deep Dive Modal",
+ "title": "Genre Deep Dive Modal",
"description": "Tap any genre pill to explore artists, tracks, and albums in that genre",
"features": [
"• Artists section with scaled avatars — top artist gets largest, 'In Library' badges",
@@ -21802,7 +21802,7 @@ def get_version_info():
]
},
{
- "title": "💾 Database Storage Visualization",
+ "title": "Database Storage Visualization",
"description": "See how your database space is distributed across tables",
"features": [
"• Donut chart on Stats page showing storage breakdown by table",
@@ -21812,7 +21812,7 @@ def get_version_info():
]
},
{
- "title": "⚡ Library Page Performance",
+ "title": "Library Page Performance",
"description": "Library artist grid loads significantly faster with smoother animations",
"features": [
"• innerHTML batch rendering replaces per-card DOM manipulation — near-instant grid population",
@@ -21822,7 +21822,7 @@ def get_version_info():
]
},
{
- "title": "🔄 Per-Artist Enrichment Rings",
+ "title": "Per-Artist Enrichment Rings",
"description": "See metadata coverage for each artist on their detail page",
"features": [
"• SVG ring indicators for all 9 enrichment services below the album/EP/singles bars",
@@ -21832,7 +21832,7 @@ def get_version_info():
]
},
{
- "title": "📱 Mobile Responsive Overhaul",
+ "title": "Mobile Responsive Overhaul",
"description": "Comprehensive mobile layout fixes across all pages",
"features": [
"• Stats, Automations, Hydrabase, Issues, Help pages now fully mobile responsive",
@@ -21842,7 +21842,7 @@ def get_version_info():
]
},
{
- "title": "🎵 Album Split Fix (Navidrome)",
+ "title": "Album Split Fix (Navidrome)",
"description": "Prevent deluxe/standard editions from splitting into separate albums",
"features": [
"• MusicBrainz release cache key normalized — strips edition suffixes (Deluxe, Remastered, etc.)",
@@ -21852,7 +21852,7 @@ def get_version_info():
]
},
{
- "title": "🏷️ Picard-Style Album Tagging",
+ "title": "Picard-Style Album Tagging",
"description": "All tracks in an album now get the same MusicBrainz release ID automatically",
"features": [
"• Pre-flight MB release lookup before album tracks start downloading",
@@ -21862,7 +21862,7 @@ def get_version_info():
]
},
{
- "title": "🛠️ Enrichment & Repair Fixes",
+ "title": "Enrichment & Repair Fixes",
"description": "Critical fixes for background workers and maintenance jobs",
"features": [
"• All 9 enrichment workers: error status items no longer auto-retry in infinite loops",
@@ -21873,7 +21873,7 @@ def get_version_info():
]
},
{
- "title": "🔗 Automation Signal Chain Fix",
+ "title": "Automation Signal Chain Fix",
"description": "Event-triggered automations now receive playlist context properly",
"features": [
"• playlist_id forwarded from events to action handlers (fixes silent 'No playlist specified')",
@@ -21883,7 +21883,7 @@ def get_version_info():
]
},
{
- "title": "🎨 Unified Glass UI Redesign",
+ "title": "Unified Glass UI Redesign",
"description": "Consistent visual style across all cards, modals, and buttons",
"features": [
"• Dashboard tool cards, service cards, and stat cards: unified glass style",
@@ -21894,7 +21894,7 @@ def get_version_info():
]
},
{
- "title": "🎧 Scrobbling to Last.fm & ListenBrainz",
+ "title": "Scrobbling to Last.fm & ListenBrainz",
"description": "Automatically scrobble your plays from Plex, Jellyfin, or Navidrome",
"features": [
"• Listen on your media server — SoulSync automatically scrobbles to Last.fm and/or ListenBrainz",
@@ -21903,7 +21903,7 @@ def get_version_info():
]
},
{
- "title": "🧠 Personalized Discovery + Listening Stats",
+ "title": "Personalized Discovery + Listening Stats",
"description": "Discovery playlists use your listening history, plus a full stats dashboard",
"features": [
"• Release Radar, Discovery Weekly, and Because You Listen To: personalized by play history",
@@ -21913,7 +21913,7 @@ def get_version_info():
]
},
{
- "title": "❓ Interactive Help System",
+ "title": "Interactive Help System",
"description": "Full contextual help platform accessible from the floating ? button",
"features": [
"• 200+ contextual help entries — click any UI element to learn what it does",
@@ -21929,7 +21929,7 @@ def get_version_info():
]
},
{
- "title": "🎤 Rich Artist Profiles",
+ "title": "Rich Artist Profiles",
"description": "Full-bleed hero section on the Artists page with deep metadata",
"features": [
"• Large portrait image with blurred background, glassmorphic design",
@@ -21939,7 +21939,7 @@ def get_version_info():
]
},
{
- "title": "📚 Enhanced Library Manager",
+ "title": "Enhanced Library Manager",
"description": "Inline metadata editing and tag writing from the library view",
"features": [
"• Toggle between Standard and Enhanced view on any artist detail page",
@@ -21950,7 +21950,7 @@ def get_version_info():
]
},
{
- "title": "🏷️ In Library Badges + Search Improvements",
+ "title": "In Library Badges + Search Improvements",
"description": "Know what you already own before downloading",
"features": [
"• 'In Library' badges on enhanced search album and track results",
@@ -21960,7 +21960,7 @@ def get_version_info():
]
},
{
- "title": "🎵 FLAC Bit Depth + Quality Filter",
+ "title": "FLAC Bit Depth + Quality Filter",
"description": "Finer control over audio quality preferences",
"features": [
"• Quality profile enforces 16-bit vs 24-bit FLAC preference",
@@ -21970,7 +21970,7 @@ def get_version_info():
]
},
{
- "title": "🔧 Enrichment Worker Improvements",
+ "title": "Enrichment Worker Improvements",
"description": "Better name matching and quieter logs across all 8+ workers",
"features": [
"• Dash-suffix normalization: 'Title - Remix' now matches 'Title (Remix)' across all workers",
@@ -21981,7 +21981,7 @@ def get_version_info():
]
},
{
- "title": "🔒 Launch PIN Lock Screen",
+ "title": "Launch PIN Lock Screen",
"description": "Protect SoulSync access with a PIN on every page load",
"features": [
"• Toggle in Settings → Advanced → Security to require PIN on launch",
@@ -21992,7 +21992,7 @@ def get_version_info():
]
},
{
- "title": "🎵 Stream Source Setting",
+ "title": "Stream Source Setting",
"description": "Choose where track previews come from — independent of download source",
"features": [
"• New dropdown in Settings → Downloads: YouTube (instant, default) or Active Download Source",
@@ -22001,7 +22001,7 @@ def get_version_info():
]
},
{
- "title": "🔧 YouTube Download Fix",
+ "title": "YouTube Download Fix",
"description": "Fixed 'Requested format not available' errors affecting all YouTube downloads",
"features": [
"• Removed stale player_client and HLS/DASH skip overrides that blocked audio formats",
@@ -22010,7 +22010,7 @@ def get_version_info():
]
},
{
- "title": "📊 Accurate Album Completion Badges",
+ "title": "Accurate Album Completion Badges",
"description": "Album completion now uses exact track counts instead of percentage rounding",
"features": [
"• Exact match: 'Complete' only when all tracks are present — no more 90% rounding",
@@ -22020,7 +22020,7 @@ def get_version_info():
]
},
{
- "title": "👥 Collaborative Album Handling",
+ "title": "Collaborative Album Handling",
"description": "Smart folder naming and matching for albums with multiple artists",
"features": [
"• New setting: Collaborative Album Artist — use first listed artist or all combined",
@@ -22030,7 +22030,7 @@ def get_version_info():
]
},
{
- "title": "🔄 Per-Artist Library Sync",
+ "title": "Per-Artist Library Sync",
"description": "Validate and clean up individual artist library entries",
"features": [
"• New 'Sync' button on enhanced library view",
@@ -22040,7 +22040,7 @@ def get_version_info():
]
},
{
- "title": "🛠️ Stability & Bug Fixes",
+ "title": "Stability & Bug Fixes",
"description": "Various fixes for crashes, data integrity, and UX",
"features": [
"• Enrichment worker pause state persists across restarts",
@@ -22067,7 +22067,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔊 Lossy Codec Expansion + Retroactive Converter",
+ "title": "Lossy Codec Expansion + Retroactive Converter",
"description": "Opus and AAC support for post-download conversion, plus a repair job for existing files",
"features": [
"• Lossy copy now supports MP3, Opus, and AAC (M4A) — configurable codec and bitrate",
@@ -22079,7 +22079,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📥 Smarter Staging Import",
+ "title": "Smarter Staging Import",
"description": "Tag-first matching and auto-grouping for the import workflow",
"features": [
"• Tags take priority over filename parsing — no more '08' as artist name",
@@ -22089,7 +22089,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎨 Library Artist Hero Redesign",
+ "title": "Library Artist Hero Redesign",
"description": "Expanded artist detail section with Last.fm integration",
"features": [
"• Horizontal service badge row with hover lift animations",
@@ -22100,7 +22100,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🌐 Hydrabase Search & Routing",
+ "title": "Hydrabase Search & Routing",
"description": "Hydrabase shows as a search tab with proper ID routing",
"features": [
"• Hydrabase appears as a source tab on enhanced search when connected",
@@ -22110,7 +22110,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔧 Orphan File Detector + MusicBrainz Fixes",
+ "title": "Orphan File Detector + MusicBrainz Fixes",
"description": "Better orphan detection and album version matching",
"features": [
"• Orphan detector: normalized tag matching strips feat./parentheticals to reduce false positives",
@@ -22120,7 +22120,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📅 Release Year Collection",
+ "title": "Release Year Collection",
"description": "Post-processing now collects release year from all metadata sources",
"features": [
"• Year extracted from MusicBrainz, Deezer, Tidal, Qobuz during post-processing",
@@ -22129,7 +22129,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔍 Multi-Source Search Tabs",
+ "title": "Multi-Source Search Tabs",
"description": "View search results from Spotify, iTunes, and Deezer side by side",
"features": [
"• Enhanced search now fires parallel queries against all available metadata sources",
@@ -22141,7 +22141,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "👥 Per-Profile Service Credentials",
+ "title": "Per-Profile Service Credentials",
"description": "Each profile can connect their own Spotify, Tidal, and media server library",
"features": [
"• Non-admin profiles can enter their own Spotify credentials and authenticate their own account",
@@ -22153,7 +22153,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "⚙️ Modern Settings Redesign",
+ "title": "Modern Settings Redesign",
"description": "Settings page rebuilt with tabbed single-column layout",
"features": [
"• Horizontal tab bar: Connections, Downloads, Library, Appearance, Advanced",
@@ -22164,7 +22164,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔄 Hybrid N-Source Download Priority",
+ "title": "Hybrid N-Source Download Priority",
"description": "Hybrid mode now supports all 5 download sources with drag-to-reorder priority",
"features": [
"• Enable/disable any combination of Soulseek, YouTube, Tidal, Qobuz, and HiFi",
@@ -22175,7 +22175,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🚀 Automation Hub Pipelines",
+ "title": "Automation Hub Pipelines",
"description": "One-click deployment of multi-automation pipelines",
"features": [
"• 11 pre-built pipelines: Release Radar, Discovery Weekly, Playlist Auto-Sync, Nightly Operations, and more",
@@ -22186,7 +22186,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📂 Staging Folder Pre-Download Check",
+ "title": "Staging Folder Pre-Download Check",
"description": "Check your staging folder for existing files before downloading",
"features": [
"• Before searching Soulseek/YouTube, checks the staging folder for a matching file",
@@ -22196,7 +22196,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🛡️ Library Safety & Repair Fixes",
+ "title": "Library Safety & Repair Fixes",
"description": "Critical fixes for library maintenance jobs and safety guards",
"features": [
"• Mass orphan safety guard — 'witness me' confirmation required when >50% of files flagged as orphans",
@@ -22213,7 +22213,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎵 Deezer Metadata Source",
+ "title": "Deezer Metadata Source",
"description": "Deezer added as a configurable free metadata fallback alongside iTunes/Apple Music",
"features": [
"• New setting to choose between iTunes and Deezer as your fallback metadata source — switch anytime from Settings",
@@ -22225,7 +22225,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📜 Library History",
+ "title": "Library History",
"description": "Persistent record of every download and server import — viewable from the dashboard",
"features": [
"• History button next to Recent Activity opens a modal with Downloads and Server Imports tabs",
@@ -22236,7 +22236,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔬 MusicBrainz MBID Mismatch Repair",
+ "title": "MusicBrainz MBID Mismatch Repair",
"description": "New repair job to detect and fix wrong MusicBrainz recording IDs on library tracks",
"features": [
"• Detects tracks where the stored MusicBrainz recording ID resolves to a different title than expected",
@@ -22245,7 +22245,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎵 HiFi Download Source",
+ "title": "HiFi Download Source",
"description": "Free lossless downloads via public hifi-api instances — no account or subscription required",
"features": [
"• New download mode alongside Soulseek, YouTube, Tidal, and Qobuz — select HiFi Only or use in hybrid mode",
@@ -22256,7 +22256,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔗 Spotify Link (No API Credentials)",
+ "title": "Spotify Link (No API Credentials)",
"description": "Scrape Spotify playlists and albums by URL without needing Spotify API credentials",
"features": [
"• New Spotify Link tab on the playlist sync page — paste any public Spotify playlist or album URL",
@@ -22266,7 +22266,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔧 Library Maintenance Suite",
+ "title": "Library Maintenance Suite",
"description": "Full-featured library repair system with 9 automated jobs, fix actions, and rich findings UI",
"features": [
"• 9 repair jobs: track number mismatch, dead files, duplicates, metadata gaps, album completeness, missing cover art, AcoustID scanner, orphan files, fake lossless detection",
@@ -22279,7 +22279,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🏷️ Post-Processing Enhancements",
+ "title": "Post-Processing Enhancements",
"description": "Granular control over post-processing and richer file tagging",
"features": [
"• Granular toggles for each post-processing step — enable/disable metadata services, cover art, and lyrics individually",
@@ -22288,7 +22288,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🧠 Per-Profile ListenBrainz",
+ "title": "Per-Profile ListenBrainz",
"description": "Each profile can connect their own ListenBrainz account for personalized playlists",
"features": [
"• Personal settings modal with ListenBrainz connect/disconnect flow",
@@ -22298,7 +22298,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "⬆️ Quality Enhance",
+ "title": "Quality Enhance",
"description": "Upgrade existing library tracks to higher quality versions",
"features": [
"• Quality enhance button on library tracks — find and download a higher quality version",
@@ -22307,7 +22307,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📀 Hi-Res FLAC Downsampling",
+ "title": "Hi-Res FLAC Downsampling",
"description": "Automatically convert 24-bit hi-res downloads to 16-bit/44.1kHz CD quality",
"features": [
"• New toggle in Settings → Post-Download Conversion: downsample hi-res FLAC to CD quality after download",
@@ -22319,7 +22319,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🐛 Recent Bug Fixes & Improvements",
+ "title": "Recent Bug Fixes & Improvements",
"description": "Stability fixes, UX improvements, and edge case handling",
"features": [
"• Fix $year template variable empty for playlist/sync downloads — album metadata now backfilled from Spotify API",
@@ -22351,7 +22351,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📋 Library Issue Reporting",
+ "title": "Library Issue Reporting",
"description": "Report and track issues for tracks, albums, and artists directly from the library",
"features": [
"• Report issues on any library item — tracks, albums, or artists — with category, priority, and notes",
@@ -22362,7 +22362,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📁 Album File Reorganization",
+ "title": "Album File Reorganization",
"description": "Reorganize album files on disk from the Enhanced Library Manager",
"features": [
"• Move and rename album files to match your configured folder template",
@@ -22372,7 +22372,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📖 Interactive REST API Docs",
+ "title": "Interactive REST API Docs",
"description": "Full API documentation with a built-in endpoint tester",
"features": [
"• Comprehensive docs for all API endpoints organized by category",
@@ -22382,7 +22382,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "👁️ Watchlist Improvements",
+ "title": "Watchlist Improvements",
"description": "Smarter cross-provider matching, manual artist linking, and scan timestamp fixes",
"features": [
"• Cross-provider artist matching now uses fuzzy name comparison instead of blindly taking the first result",
@@ -22394,7 +22394,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔊 AcoustID Verification Fix",
+ "title": "AcoustID Verification Fix",
"description": "More accurate audio file verification with broader title normalization",
"features": [
"• Strip ALL parentheticals in title normalization — fixes false mismatches for parody, soundtrack, and featured artist suffixes",
@@ -22402,7 +22402,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎧 Deezer Playlist Sync",
+ "title": "Deezer Playlist Sync",
"description": "Full Deezer integration for playlist sync alongside Spotify, Tidal, and YouTube",
"features": [
"• Import and sync Deezer playlists with full track matching and discovery",
@@ -22412,7 +22412,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🚀 Discovery Page Improvements",
+ "title": "Discovery Page Improvements",
"description": "Better playlist generation, caching, and iTunes parity",
"features": [
"• iTunes discovery playlists now produce quality results — synthetic popularity scoring replaces broken 0-popularity tiering",
@@ -22426,7 +22426,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🛡️ Rate Limit Detection Fix",
+ "title": "Rate Limit Detection Fix",
"description": "Rate limit handling completely overhauled — escalating bans, no more rate limit loops",
"features": [
"• Fixed rate limits going undetected in get_album, get_artist, and batch artist enrichment",
@@ -22439,7 +22439,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📦 Download & Matching Fixes",
+ "title": "Download & Matching Fixes",
"description": "Accuracy improvements for album downloads and track matching",
"features": [
"• Album download pre-flight search finds complete album folders before track-by-track downloading",
@@ -22450,7 +22450,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔒 Security & Config",
+ "title": "Security & Config",
"description": "Encryption at rest and config improvements",
"features": [
"• Sensitive config values (API keys, passwords, tokens) encrypted at rest with Fernet",
@@ -22459,7 +22459,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🐛 Recent Bug Fixes",
+ "title": "Recent Bug Fixes",
"description": "Stability and UX fixes",
"features": [
"• Fix sync stuck at 80% — serialize datetime in SyncResult for WebSocket emit",
@@ -22473,7 +22473,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎵 Tidal & Qobuz Enrichment Workers",
+ "title": "Tidal & Qobuz Enrichment Workers",
"description": "Two new background enrichment workers for Tidal and Qobuz metadata",
"features": [
"• Tidal worker enriches artists, albums, and tracks with Tidal IDs, thumbnails, and metadata",
@@ -22487,7 +22487,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎧 Full Qobuz Support",
+ "title": "Full Qobuz Support",
"description": "Qobuz added as a first-class download source alongside Tidal and Soulseek",
"features": [
"• Search, browse, and download from Qobuz with quality selection up to Hi-Res 24-bit/192kHz",
@@ -22497,7 +22497,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔀 Hybrid Mode Redesign",
+ "title": "Hybrid Mode Redesign",
"description": "Overhauled download source selection and priority system",
"features": [
"• Redesigned hybrid mode with drag-and-drop source priority ordering",
@@ -22507,7 +22507,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🛡️ Spotify Rate Limit Protection",
+ "title": "Spotify Rate Limit Protection",
"description": "Smart detection and handling of Spotify API rate limits with escalating bans",
"features": [
"• Automatic detection of long rate limit bans (Retry-After > 60s) from Spotify",
@@ -22522,7 +22522,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "👤 Profile Permissions & Page Access Control",
+ "title": "Profile Permissions & Page Access Control",
"description": "Granular admin controls over what each profile can see and do",
"features": [
"• Admin can control which sidebar pages each profile can access",
@@ -22533,7 +22533,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🎵 Now Playing Overhaul",
+ "title": "Now Playing Overhaul",
"description": "Redesigned media player with expanded Now Playing modal and smart radio",
"features": [
"• Expanded Now Playing modal — click the sidebar player to open a full-screen experience",
@@ -22546,7 +22546,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "📚 Enhanced Library Manager",
+ "title": "Enhanced Library Manager",
"description": "Professional-grade library management with tag writing and server sync",
"features": [
"• Toggle between Standard and Enhanced views on any artist's discography",
@@ -22563,7 +22563,7 @@ _OLD_V2_NOTES = r"""
"usage_note": "Open any artist's detail page and click 'Enhanced' in the view toggle to access the library manager."
},
{
- "title": "🎶 Last.fm & Genius Enrichment Workers",
+ "title": "Last.fm & Genius Enrichment Workers",
"description": "Background enrichment workers for Last.fm and Genius metadata",
"features": [
"• Last.fm worker enriches artists, albums, and tracks with listener counts, play counts, tags, and bios",
@@ -22574,7 +22574,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "✨ UI & Visual Overhaul",
+ "title": "UI & Visual Overhaul",
"description": "Per-page particle animations, sidebar visualizer, watchlist redesign, and design refresh",
"features": [
"• Per-page particle animations with unique themes for each page",
@@ -22590,7 +22590,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🔧 Tidal Download Improvements",
+ "title": "Tidal Download Improvements",
"description": "Stability and accuracy fixes for Tidal downloads",
"features": [
"• Tidal download validation — detect and clean up unplayable hi-res stubs",
@@ -22600,7 +22600,7 @@ _OLD_V2_NOTES = r"""
]
},
{
- "title": "🐛 Bug Fixes & Stability",
+ "title": "Bug Fixes & Stability",
"description": "Reliability improvements across the board",
"features": [
"• Fix Genius search blindly matching wrong artists — all bad matches auto-reset",
@@ -22618,7 +22618,7 @@ _OLD_V2_NOTES = r"""
def _simple_monitor_task():
"""The actual monitoring task that runs in the background thread.
Search cleanup and download cleanup are now handled by system automations."""
- print("🔄 Simple background monitor started")
+ print("Simple background monitor started")
while True:
try:
@@ -22639,12 +22639,12 @@ def _simple_monitor_task():
if current_time - data['first_attempt'] > 60
]
for key in stale_keys:
- print(f"🧹 Cleaning up stale retry attempt: {key}")
+ print(f"Cleaning up stale retry attempt: {key}")
del _download_retry_attempts[key]
time.sleep(1)
except Exception as e:
- print(f"❌ Simple monitor error: {e}")
+ print(f"Simple monitor error: {e}")
time.sleep(10)
def start_simple_background_monitor():
@@ -22663,7 +22663,7 @@ def _sanitize_track_data_for_processing(track_data):
Preserves album dict to retain full metadata (images, id, etc.) and normalizes artist field.
"""
if not isinstance(track_data, dict):
- print(f"⚠️ [Sanitize] Unexpected track data type: {type(track_data)}")
+ print(f"[Sanitize] Unexpected track data type: {type(track_data)}")
return track_data
# Create a copy to avoid modifying original data
@@ -22688,7 +22688,7 @@ def _sanitize_track_data_for_processing(track_data):
processed_artists.append(str(artist))
sanitized['artists'] = processed_artists
else:
- print(f"⚠️ [Sanitize] Unexpected artists format: {type(raw_artists)}")
+ print(f"[Sanitize] Unexpected artists format: {type(raw_artists)}")
sanitized['artists'] = [str(raw_artists)] if raw_artists else []
return sanitized
@@ -22711,7 +22711,7 @@ def check_and_recover_stuck_flags():
time_stuck = current_time - wishlist_auto_processing_timestamp
if time_stuck > stuck_timeout:
stuck_minutes = time_stuck / 60
- print(f"⚠️ [Stuck Detection] Wishlist auto-processing flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING")
+ print(f"[Stuck Detection] Wishlist auto-processing flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING")
with wishlist_timer_lock:
wishlist_auto_processing = False
wishlist_auto_processing_timestamp = 0
@@ -22723,7 +22723,7 @@ def check_and_recover_stuck_flags():
time_stuck = current_time - watchlist_auto_scanning_timestamp
if time_stuck > stuck_timeout:
stuck_minutes = time_stuck / 60
- print(f"⚠️ [Stuck Detection] Watchlist auto-scanning flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING")
+ print(f"[Stuck Detection] Watchlist auto-scanning flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING")
with watchlist_timer_lock:
watchlist_auto_scanning = False
watchlist_auto_scanning_timestamp = 0
@@ -22748,7 +22748,7 @@ def is_wishlist_actually_processing():
# If more than 15 minutes, flag is stuck - auto-recover and return False
if time_since_start > 900: # 15 minutes
stuck_minutes = time_since_start / 60
- print(f"⚠️ [Stuck Detection] Wishlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering")
+ print(f"[Stuck Detection] Wishlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering")
check_and_recover_stuck_flags()
return False
@@ -22771,7 +22771,7 @@ def is_watchlist_actually_scanning():
# If more than 15 minutes, flag is stuck - auto-recover and return False
if time_since_start > 900: # 15 minutes
stuck_minutes = time_since_start / 60
- print(f"⚠️ [Stuck Detection] Watchlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering")
+ print(f"[Stuck Detection] Watchlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering")
check_and_recover_stuck_flags()
return False
@@ -22818,13 +22818,13 @@ def _process_wishlist_automatically(automation_id=None):
"""Main automatic processing logic that runs in background thread."""
global wishlist_auto_processing, wishlist_auto_processing_timestamp
- print("🤖 [Auto-Wishlist] Timer triggered - starting automatic wishlist processing...")
+ print("[Auto-Wishlist] Timer triggered - starting automatic wishlist processing...")
try:
# CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock
# This prevents deadlock and handles stuck flags (2-hour timeout)
if is_wishlist_actually_processing():
- print("⚠️ [Auto-Wishlist] Already processing (verified with stuck detection), skipping.")
+ print("[Auto-Wishlist] Already processing (verified with stuck detection), skipping.")
return
# Check conditions and set flag
@@ -22833,7 +22833,7 @@ def _process_wishlist_automatically(automation_id=None):
with wishlist_timer_lock:
# Re-check inside lock to handle race conditions
if wishlist_auto_processing:
- print("⚠️ [Auto-Wishlist] Already processing (race condition check), skipping.")
+ print("[Auto-Wishlist] Already processing (race condition check), skipping.")
should_skip_already_running = True
else:
@@ -22841,7 +22841,7 @@ def _process_wishlist_automatically(automation_id=None):
import time
wishlist_auto_processing = True
wishlist_auto_processing_timestamp = time.time()
- print(f"🔒 [Auto-Wishlist] Flag set at timestamp {wishlist_auto_processing_timestamp}")
+ print(f"[Auto-Wishlist] Flag set at timestamp {wishlist_auto_processing_timestamp}")
if should_skip_already_running:
return
@@ -22855,7 +22855,7 @@ def _process_wishlist_automatically(automation_id=None):
database = get_database()
all_profiles = database.get_all_profiles()
count = sum(wishlist_service.get_wishlist_count(profile_id=p['id']) for p in all_profiles)
- print(f"🔍 [Auto-Wishlist] Wishlist count check: {count} tracks found across {len(all_profiles)} profiles")
+ print(f"[Auto-Wishlist] Wishlist count check: {count} tracks found across {len(all_profiles)} profiles")
_update_automation_progress(automation_id, progress=10, phase='Checking wishlist',
log_line=f'{count} tracks across {len(all_profiles)} profiles', log_type='info')
if count == 0:
@@ -22865,7 +22865,7 @@ def _process_wishlist_automatically(automation_id=None):
wishlist_auto_processing_timestamp = 0
return
- print(f"🎵 [Auto-Wishlist] Found {count} tracks in wishlist, starting automatic processing...")
+ print(f"[Auto-Wishlist] Found {count} tracks in wishlist, starting automatic processing...")
# Check if wishlist processing is already active (auto or manual)
playlist_id = "wishlist"
@@ -22875,7 +22875,7 @@ def _process_wishlist_automatically(automation_id=None):
# Check for both auto ('wishlist') and manual ('wishlist_manual') batches
if (batch_playlist_id in ['wishlist', 'wishlist_manual'] and
batch_data.get('phase') not in ['complete', 'error', 'cancelled']):
- print(f"⚠️ Wishlist processing already active in another batch ({batch_playlist_id}), skipping automatic start")
+ print(f"Wishlist processing already active in another batch ({batch_playlist_id}), skipping automatic start")
with wishlist_timer_lock:
wishlist_auto_processing = False
return
@@ -22885,15 +22885,15 @@ def _process_wishlist_automatically(automation_id=None):
from database.music_database import MusicDatabase
db = MusicDatabase()
- print("🧹 [Auto-Wishlist] Cleaning duplicate tracks before processing...")
+ print("[Auto-Wishlist] Cleaning duplicate tracks before processing...")
for p in all_profiles:
duplicates_removed = db.remove_wishlist_duplicates(profile_id=p['id'])
if duplicates_removed > 0:
- print(f"🧹 [Auto-Wishlist] Removed {duplicates_removed} duplicate tracks from profile {p['id']}")
+ print(f"[Auto-Wishlist] Removed {duplicates_removed} duplicate tracks from profile {p['id']}")
# CLEANUP: Remove tracks from wishlist that already exist in library
# This prevents wasting bandwidth on tracks we already have
- print("🧼 [Auto-Wishlist] Checking wishlist against library for already-owned tracks...")
+ print("[Auto-Wishlist] Checking wishlist against library for already-owned tracks...")
cleanup_tracks = []
for p in all_profiles:
cleanup_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id']))
@@ -22938,12 +22938,12 @@ def _process_wishlist_automatically(automation_id=None):
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
if removed:
cleanup_removed += 1
- print(f"🧼 [Auto-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}")
+ print(f"[Auto-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}")
except Exception as remove_error:
- print(f"⚠️ [Auto-Wishlist] Error removing track from wishlist: {remove_error}")
+ print(f"[Auto-Wishlist] Error removing track from wishlist: {remove_error}")
if cleanup_removed > 0:
- print(f"✅ [Auto-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist")
+ print(f"[Auto-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist")
_update_automation_progress(automation_id, progress=25, phase='Cleaned up duplicates',
log_line=f'Removed {cleanup_removed} already-owned tracks', log_type='success')
else:
@@ -22955,7 +22955,7 @@ def _process_wishlist_automatically(automation_id=None):
for p in all_profiles:
raw_wishlist_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id']))
if not raw_wishlist_tracks:
- print("⚠️ No tracks returned from wishlist service.")
+ print("No tracks returned from wishlist service.")
with wishlist_timer_lock:
wishlist_auto_processing = False
wishlist_auto_processing_timestamp = 0
@@ -22979,8 +22979,8 @@ def _process_wishlist_automatically(automation_id=None):
seen_track_ids_sanitation.add(spotify_track_id)
if duplicates_found > 0:
- print(f"⚠️ [Auto-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization")
- print(f"🔧 [Auto-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service")
+ print(f"[Auto-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization")
+ print(f"[Auto-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service")
# CYCLE FILTERING: Get current cycle and filter tracks by category
with db._get_connection() as conn:
@@ -23018,8 +23018,8 @@ def _process_wishlist_automatically(automation_id=None):
# No ID - can't deduplicate safely, always add
filtered_tracks.append(track)
- print(f"🔄 [Auto-Wishlist] Current cycle: {current_cycle}")
- print(f"📊 [Auto-Wishlist] Filtered {len(filtered_tracks)}/{len(wishlist_tracks)} tracks for '{current_cycle}' category")
+ print(f"[Auto-Wishlist] Current cycle: {current_cycle}")
+ print(f"[Auto-Wishlist] Filtered {len(filtered_tracks)}/{len(wishlist_tracks)} tracks for '{current_cycle}' category")
_update_automation_progress(automation_id, progress=40, phase=f'Processing {current_cycle}',
log_line=f'Cycle: {current_cycle} — {len(filtered_tracks)} tracks to process', log_type='info')
@@ -23036,7 +23036,7 @@ def _process_wishlist_automatically(automation_id=None):
VALUES ('wishlist_cycle', ?, CURRENT_TIMESTAMP)
""", (next_cycle,))
conn.commit()
- print(f"🔄 [Auto-Wishlist] Cycle toggled: {current_cycle} → {next_cycle}")
+ print(f"[Auto-Wishlist] Cycle toggled: {current_cycle} → {next_cycle}")
with wishlist_timer_lock:
wishlist_auto_processing = False
@@ -23079,7 +23079,7 @@ def _process_wishlist_automatically(automation_id=None):
'profile_id': 1
}
- print(f"🚀 Starting automatic wishlist batch {batch_id} with {len(wishlist_tracks)} tracks")
+ print(f"Starting automatic wishlist batch {batch_id} with {len(wishlist_tracks)} tracks")
_update_automation_progress(automation_id, progress=50, phase=f'Downloading {len(wishlist_tracks)} tracks',
log_line=f'Started batch: {len(wishlist_tracks)} {current_cycle}', log_type='success')
@@ -23089,7 +23089,7 @@ def _process_wishlist_automatically(automation_id=None):
# Don't mark auto_processing as False here - let completion handler do it
except Exception as e:
- print(f"❌ Error in automatic wishlist processing: {e}")
+ print(f"Error in automatic wishlist processing: {e}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id, log_line=f'Error: {str(e)}', log_type='error')
@@ -23104,7 +23104,7 @@ def _process_wishlist_automatically(automation_id=None):
# ===============================
def _db_update_progress_callback(current_item, processed, total, percentage):
- print(f"📊 [DB Progress] {current_item} - {processed}/{total} ({percentage:.1f}%)")
+ print(f"[DB Progress] {current_item} - {processed}/{total} ({percentage:.1f}%)")
with db_update_lock:
db_update_state.update({
"current_item": current_item,
@@ -23117,7 +23117,7 @@ def _db_update_progress_callback(current_item, processed, total, percentage):
current_item=current_item)
def _db_update_phase_callback(phase):
- print(f"🔄 [DB Phase] {phase}")
+ print(f"[DB Phase] {phase}")
with db_update_lock:
db_update_state["phase"] = phase
_update_automation_progress(_db_update_automation_id, phase=phase)
@@ -23172,7 +23172,7 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
summary = f"{total_tracks} tracks, {total_albums} albums, {total_artists} artists processed"
if removed_artists > 0 or removed_albums > 0:
summary += f" | {removed_artists} artists, {removed_albums} albums removed"
- add_activity_item("✅", "Database Update Complete", summary, "Now")
+ add_activity_item("", "Database Update Complete", summary, "Now")
try:
if automation_engine:
@@ -23189,17 +23189,17 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
inv_db = get_database()
cleared = inv_db.invalidate_sync_match_cache()
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:
pass
# WISHLIST CLEANUP: Automatically clean up wishlist after database update
try:
- print("📋 [DB Update] Database update completed, starting automatic wishlist cleanup...")
+ print("[DB Update] Database update completed, starting automatic wishlist cleanup...")
# Run cleanup in background to avoid blocking the UI
missing_download_executor.submit(_automatic_wishlist_cleanup_after_db_update)
except Exception as cleanup_error:
- print(f"⚠️ [DB Update] Error starting automatic wishlist cleanup: {cleanup_error}")
+ print(f"[DB Update] Error starting automatic wishlist cleanup: {cleanup_error}")
def _db_update_error_callback(error_message):
global _db_update_automation_id
@@ -23214,7 +23214,7 @@ def _db_update_error_callback(error_message):
_db_update_automation_id = None
# Add activity for database update error
- add_activity_item("❌", "Database Update Failed", error_message, "Now")
+ add_activity_item("", "Database Update Failed", error_message, "Now")
_workers_paused_by_scan = set() # Track which workers WE paused (don't resume manually-paused ones)
@@ -23233,7 +23233,7 @@ def _pause_workers_for_scan():
w.pause()
_workers_paused_by_scan.add(name)
if _workers_paused_by_scan:
- print(f"⏸️ Paused {len(_workers_paused_by_scan)} workers during database scan: {', '.join(_workers_paused_by_scan)}")
+ print(f"Paused {len(_workers_paused_by_scan)} workers during database scan: {', '.join(_workers_paused_by_scan)}")
def _resume_workers_after_scan():
"""Resume only the workers that WE paused (don't resume manually-paused ones)."""
@@ -23250,7 +23250,7 @@ def _resume_workers_after_scan():
w.resume()
resumed += 1
if resumed:
- print(f"▶️ Resumed {resumed} workers after database scan")
+ print(f"Resumed {resumed} workers after database scan")
_workers_paused_by_scan = set()
def _run_db_update_task(full_refresh, server_type):
@@ -23515,7 +23515,7 @@ def set_wishlist_cycle():
""", (cycle,))
conn.commit()
- print(f"✅ Wishlist cycle set to: {cycle}")
+ print(f"Wishlist cycle set to: {cycle}")
return jsonify({"success": True, "cycle": cycle})
except Exception as e:
@@ -23602,7 +23602,7 @@ def set_discovery_lookback_period():
conn.commit()
- print(f"✅ Discovery lookback period set to: {period}")
+ print(f"Discovery lookback period set to: {period}")
return jsonify({"success": True, "period": period})
except Exception as e:
@@ -23682,9 +23682,9 @@ def get_wishlist_tracks():
db = MusicDatabase()
duplicates_removed = db.remove_wishlist_duplicates(profile_id=get_current_profile_id())
if duplicates_removed > 0:
- print(f"🧹 Cleaned {duplicates_removed} duplicate tracks from wishlist")
+ print(f"Cleaned {duplicates_removed} duplicate tracks from wishlist")
else:
- print(f"⏸️ Skipping wishlist duplicate cleanup - download in progress")
+ print(f"Skipping wishlist duplicate cleanup - download in progress")
wishlist_service = get_wishlist_service()
raw_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=get_current_profile_id())
@@ -23707,7 +23707,7 @@ def get_wishlist_tracks():
seen_track_ids_sanitation.add(spotify_track_id)
if duplicates_found > 0:
- print(f"⚠️ [API-Wishlist-Tracks] Found and removed {duplicates_found} duplicate tracks during sanitization")
+ print(f"[API-Wishlist-Tracks] Found and removed {duplicates_found} duplicate tracks during sanitization")
# FILTER by category if specified
if category:
@@ -23736,7 +23736,7 @@ def get_wishlist_tracks():
# Count total in category (quick scan — no heavy processing, just classification)
total_in_category = sum(1 for t in sanitized_tracks if _classify_wishlist_track(t) == category)
- print(f"📊 Wishlist filter: {len(filtered_tracks)}/{total_in_category} tracks in '{category}' category (limit: {limit or 'none'})")
+ print(f"Wishlist filter: {len(filtered_tracks)}/{total_in_category} tracks in '{category}' category (limit: {limit or 'none'})")
return jsonify({"tracks": filtered_tracks, "category": category, "total": total_in_category})
# Apply limit to non-filtered results
@@ -23776,16 +23776,16 @@ def start_wishlist_missing_downloads():
# CRITICAL: Clean duplicates BEFORE fetching tracks to prevent count mismatches
# This prevents the "11 tracks shown but 12 counted" bug
- print("🧹 [Manual-Wishlist] Cleaning duplicate tracks before download...")
+ print("[Manual-Wishlist] Cleaning duplicate tracks before download...")
db = MusicDatabase()
manual_profile_id = get_current_profile_id()
duplicates_removed = db.remove_wishlist_duplicates(profile_id=manual_profile_id)
if duplicates_removed > 0:
- print(f"🧹 [Manual-Wishlist] Removed {duplicates_removed} duplicate tracks")
+ print(f"[Manual-Wishlist] Removed {duplicates_removed} duplicate tracks")
# CLEANUP: Remove tracks from wishlist that already exist in library
# This prevents wasting bandwidth on tracks we already have
- print("🧼 [Manual-Wishlist] Checking wishlist against library for already-owned tracks...")
+ print("[Manual-Wishlist] Checking wishlist against library for already-owned tracks...")
cleanup_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=manual_profile_id)
cleanup_removed = 0
@@ -23832,12 +23832,12 @@ def start_wishlist_missing_downloads():
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
if removed:
cleanup_removed += 1
- print(f"🧼 [Manual-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}")
+ print(f"[Manual-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}")
except Exception as remove_error:
- print(f"⚠️ [Manual-Wishlist] Error removing track from wishlist: {remove_error}")
+ print(f"[Manual-Wishlist] Error removing track from wishlist: {remove_error}")
if cleanup_removed > 0:
- print(f"✅ [Manual-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist")
+ print(f"[Manual-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist")
# Get wishlist tracks formatted for download modal (after cleanup)
raw_wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=manual_profile_id)
@@ -23862,8 +23862,8 @@ def start_wishlist_missing_downloads():
seen_track_ids_sanitation.add(spotify_track_id)
if duplicates_found > 0:
- print(f"⚠️ [Manual-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization")
- print(f"🔧 [Manual-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service")
+ print(f"[Manual-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization")
+ print(f"[Manual-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service")
# FILTER BY TRACK IDs if specified (prioritized - prevents race conditions)
if track_ids:
@@ -23887,7 +23887,7 @@ def start_wishlist_missing_downloads():
seen_track_ids.add(tid)
wishlist_tracks = filtered_tracks
- print(f"🎯 [Manual-Wishlist] Filtered to {len(wishlist_tracks)} specific tracks by ID (preserving frontend display order)")
+ print(f"[Manual-Wishlist] Filtered to {len(wishlist_tracks)} specific tracks by ID (preserving frontend display order)")
# FILTER BY CATEGORY if specified and no track_ids (backward compatibility)
elif category:
@@ -23938,14 +23938,14 @@ def start_wishlist_missing_downloads():
filtered_tracks.append(track)
wishlist_tracks = filtered_tracks
- print(f"🔍 [Manual-Wishlist] Filtered to {len(wishlist_tracks)} tracks for category: {category}")
+ print(f"[Manual-Wishlist] Filtered to {len(wishlist_tracks)} tracks for category: {category}")
# Stamp original index on each track so task indices match frontend row order
for i, track in enumerate(wishlist_tracks):
track['_original_index'] = i
# Add activity for wishlist download start
- add_activity_item("📥", "Wishlist Download Started", f"{len(wishlist_tracks)} tracks", "Now")
+ add_activity_item("", "Wishlist Download Started", f"{len(wishlist_tracks)} tracks", "Now")
batch_id = str(uuid.uuid4())
@@ -24016,7 +24016,7 @@ def cleanup_wishlist():
db = MusicDatabase()
active_server = config_manager.get_active_media_server()
- print("📋 [Wishlist Cleanup] Starting wishlist cleanup process...")
+ print("[Wishlist Cleanup] Starting wishlist cleanup process...")
# Get wishlist tracks for current profile
cleanup_profile_id = get_current_profile_id()
@@ -24024,7 +24024,7 @@ def cleanup_wishlist():
if not wishlist_tracks:
return jsonify({"success": True, "message": "No tracks in wishlist to clean up", "removed_count": 0})
- print(f"📋 [Wishlist Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
+ print(f"[Wishlist Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
removed_count = 0
processed_count = 0
@@ -24040,7 +24040,7 @@ def cleanup_wishlist():
if not track_name or not artists or not spotify_track_id:
continue
- print(f"📋 [Wishlist Cleanup] Checking track {processed_count}/{len(wishlist_tracks)}: '{track_name}'")
+ print(f"[Wishlist Cleanup] Checking track {processed_count}/{len(wishlist_tracks)}: '{track_name}'")
# Check each artist
found_in_db = False
@@ -24063,11 +24063,11 @@ def cleanup_wishlist():
if db_track and confidence >= 0.7:
found_in_db = True
- print(f"📋 [Wishlist Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
+ print(f"[Wishlist Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
break
except Exception as db_error:
- print(f"⚠️ [Wishlist Cleanup] Error checking database for track '{track_name}': {db_error}")
+ print(f"[Wishlist Cleanup] Error checking database for track '{track_name}': {db_error}")
continue
# If found in database, remove from wishlist
@@ -24076,13 +24076,13 @@ def cleanup_wishlist():
removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
if removed:
removed_count += 1
- print(f"✅ [Wishlist Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
+ print(f"[Wishlist Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
else:
- print(f"⚠️ [Wishlist Cleanup] Failed to remove track from wishlist: '{track_name}' ({spotify_track_id})")
+ print(f"[Wishlist Cleanup] Failed to remove track from wishlist: '{track_name}' ({spotify_track_id})")
except Exception as remove_error:
- print(f"❌ [Wishlist Cleanup] Error removing track from wishlist: {remove_error}")
+ print(f"[Wishlist Cleanup] Error removing track from wishlist: {remove_error}")
- print(f"📋 [Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist")
+ print(f"[Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist")
return jsonify({
"success": True,
@@ -24304,20 +24304,20 @@ def add_album_track_to_wishlist():
)
if success:
- print(f"✅ Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist")
+ print(f"Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist")
return jsonify({
"success": True,
"message": f"Added '{track.get('name')}' to wishlist"
})
else:
- print(f"❌ Failed to add track '{track.get('name')}' to wishlist")
+ print(f"Failed to add track '{track.get('name')}' to wishlist")
return jsonify({
"success": False,
"error": "Failed to add track to wishlist"
})
except Exception as e:
- print(f"❌ Error adding track to wishlist: {e}")
+ print(f"Error adding track to wishlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -24343,7 +24343,7 @@ def start_database_update():
# Add activity for database update start
update_type = "Full" if full_refresh else "Incremental"
server_name = active_server.capitalize()
- add_activity_item("🗄️", "Database Update", f"Starting {update_type.lower()} update from {server_name}...", "Now")
+ add_activity_item("", "Database Update", f"Starting {update_type.lower()} update from {server_name}...", "Now")
# Submit the worker function to the executor
db_update_executor.submit(_run_db_update_task, full_refresh, active_server)
@@ -24356,7 +24356,7 @@ def get_database_update_status():
with db_update_lock:
# Debug: Log current state occasionally
if db_update_state["status"] == "running":
- print(f"📊 [Status Check] {db_update_state['processed']}/{db_update_state['total']} ({db_update_state['progress']:.1f}%) - {db_update_state['phase']}")
+ print(f"[Status Check] {db_update_state['processed']}/{db_update_state['total']} ({db_update_state['progress']:.1f}%) - {db_update_state['phase']}")
return jsonify(db_update_state)
@app.route('/api/database/update/stop', methods=['POST'])
@@ -25087,7 +25087,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
quality_scanner_state["results"] = []
quality_scanner_state["error_message"] = ""
- print(f"🔍 [Quality Scanner] Starting scan with scope: {scope}")
+ print(f"[Quality Scanner] Starting scan with scope: {scope}")
# Get database instance
db = MusicDatabase()
@@ -25112,7 +25112,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
tier_num = QUALITY_TIERS[tier_name]['tier']
min_acceptable_tier = min(min_acceptable_tier, tier_num)
- print(f"🎵 [Quality Scanner] Minimum acceptable tier: {min_acceptable_tier}")
+ print(f"[Quality Scanner] Minimum acceptable tier: {min_acceptable_tier}")
# Get tracks to scan based on scope
with quality_scanner_lock:
@@ -25126,12 +25126,12 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
quality_scanner_state["status"] = "finished"
quality_scanner_state["phase"] = "No watchlist artists found"
quality_scanner_state["error_message"] = "Please add artists to watchlist first"
- print(f"⚠️ [Quality Scanner] No watchlist artists found")
+ print(f"[Quality Scanner] No watchlist artists found")
return
# Get artist names from watchlist
artist_names = [artist.artist_name for artist in watchlist_artists]
- print(f"📋 [Quality Scanner] Scanning {len(artist_names)} watchlist artists")
+ print(f"[Quality Scanner] Scanning {len(artist_names)} watchlist artists")
# Get all tracks for these artists by name
conn = db._get_connection()
@@ -25161,7 +25161,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
conn.close()
total_tracks = len(tracks_to_scan)
- print(f"📊 [Quality Scanner] Found {total_tracks} tracks to scan")
+ print(f"[Quality Scanner] Found {total_tracks} tracks to scan")
with quality_scanner_lock:
quality_scanner_state["total"] = total_tracks
@@ -25173,7 +25173,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
quality_scanner_state["status"] = "error"
quality_scanner_state["phase"] = "Spotify not authenticated"
quality_scanner_state["error_message"] = "Please authenticate with Spotify first"
- print(f"❌ [Quality Scanner] Spotify not authenticated")
+ print(f"[Quality Scanner] Spotify not authenticated")
return
wishlist_service = get_wishlist_service()
@@ -25182,7 +25182,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
for idx, track_row in enumerate(tracks_to_scan, 1):
# Check for stop request
if quality_scanner_state.get('status') != 'running':
- print(f"🛑 [Quality Scanner] Stop requested, halting at track {idx}/{total_tracks}")
+ print(f"[Quality Scanner] Stop requested, halting at track {idx}/{total_tracks}")
break
try:
@@ -25208,7 +25208,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
with quality_scanner_lock:
quality_scanner_state["low_quality"] += 1
- print(f"🔍 [Quality Scanner] Low quality: {artist_name} - {title} ({tier_name}, {file_path})")
+ print(f"[Quality Scanner] Low quality: {artist_name} - {title} ({tier_name}, {file_path})")
# Attempt to match to Spotify using matching_engine
matched = False
@@ -25223,7 +25223,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
})()
search_queries = matching_engine.generate_download_queries(temp_track)
- print(f"🔍 [Quality Scanner] Generated {len(search_queries)} search queries for {artist_name} - {title}")
+ print(f"[Quality Scanner] Generated {len(search_queries)} search queries for {artist_name} - {title}")
# Find best match using confidence scoring
best_match = None
@@ -25267,31 +25267,31 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
elif _at == 'ep':
combined_confidence += 0.01
- print(f"🔍 [Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})")
+ print(f"[Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})")
# Update best match if this is better
if combined_confidence > best_confidence and combined_confidence >= min_confidence:
best_confidence = combined_confidence
best_match = spotify_track
- print(f"✅ [Quality Scanner] New best match: {spotify_track.artists[0]} - {spotify_track.name} (confidence: {combined_confidence:.3f})")
+ print(f"[Quality Scanner] New best match: {spotify_track.artists[0]} - {spotify_track.name} (confidence: {combined_confidence:.3f})")
except Exception as e:
- print(f"❌ [Quality Scanner] Error scoring result: {e}")
+ print(f"[Quality Scanner] Error scoring result: {e}")
continue
# If we found a very high confidence match, stop searching
if best_confidence >= 0.9:
- print(f"🎯 [Quality Scanner] High confidence match found ({best_confidence:.3f}), stopping search")
+ print(f"[Quality Scanner] High confidence match found ({best_confidence:.3f}), stopping search")
break
except Exception as e:
- print(f"❌ [Quality Scanner] Error searching with query '{search_query}': {e}")
+ print(f"[Quality Scanner] Error searching with query '{search_query}': {e}")
continue
# Process best match
if best_match:
matched = True
- print(f"✅ [Quality Scanner] Final match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})")
+ print(f"[Quality Scanner] Final match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})")
# Build full Spotify track data for wishlist
matched_track_data = {
@@ -25331,14 +25331,14 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
if success:
with quality_scanner_lock:
quality_scanner_state["matched"] += 1
- print(f"✅ [Quality Scanner] Matched and added to wishlist: {artist_name} - {title}")
+ print(f"[Quality Scanner] Matched and added to wishlist: {artist_name} - {title}")
else:
- print(f"⚠️ [Quality Scanner] Failed to add to wishlist: {artist_name} - {title}")
+ print(f"[Quality Scanner] Failed to add to wishlist: {artist_name} - {title}")
else:
- print(f"⚠️ [Quality Scanner] No suitable match found (best confidence: {best_confidence:.3f}, required: {min_confidence:.3f})")
+ print(f"[Quality Scanner] No suitable match found (best confidence: {best_confidence:.3f}, required: {min_confidence:.3f})")
except Exception as matching_error:
- print(f"❌ [Quality Scanner] Matching error for {artist_name} - {title}: {matching_error}")
+ print(f"[Quality Scanner] Matching error for {artist_name} - {title}: {matching_error}")
# Store result
result_entry = {
@@ -25357,10 +25357,10 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
quality_scanner_state["results"].append(result_entry)
if not matched:
- print(f"⚠️ [Quality Scanner] No Spotify match found for: {artist_name} - {title}")
+ print(f"[Quality Scanner] No Spotify match found for: {artist_name} - {title}")
except Exception as track_error:
- print(f"❌ [Quality Scanner] Error processing track: {track_error}")
+ print(f"[Quality Scanner] Error processing track: {track_error}")
continue
# Scan complete (don't overwrite if already stopped by user)
@@ -25371,11 +25371,11 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
if not was_stopped:
quality_scanner_state["phase"] = "Scan complete"
- print(f"✅ [Quality Scanner] Scan {'stopped' if was_stopped else 'complete'}: {quality_scanner_state['processed']} processed, "
+ print(f"[Quality Scanner] Scan {'stopped' if was_stopped else 'complete'}: {quality_scanner_state['processed']} processed, "
f"{quality_scanner_state['low_quality']} low quality, {quality_scanner_state['matched']} matched to Spotify")
# Add activity
- add_activity_item("🔍", "Quality Scan Complete",
+ add_activity_item("", "Quality Scan Complete",
f"{quality_scanner_state['matched']} tracks added to wishlist", "Now")
try:
@@ -25389,7 +25389,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
pass
except Exception as e:
- print(f"❌ [Quality Scanner] Critical error: {e}")
+ print(f"[Quality Scanner] Critical error: {e}")
import traceback
traceback.print_exc()
@@ -25418,7 +25418,7 @@ def _run_duplicate_cleaner():
duplicate_cleaner_state["space_freed"] = 0
duplicate_cleaner_state["error_message"] = ""
- print(f"🧹 [Duplicate Cleaner] Starting duplicate scan...")
+ print(f"[Duplicate Cleaner] Starting duplicate scan...")
# Get Transfer folder path from config
transfer_folder = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
@@ -25427,13 +25427,13 @@ def _run_duplicate_cleaner():
duplicate_cleaner_state["status"] = "error"
duplicate_cleaner_state["phase"] = "Transfer folder not configured or does not exist"
duplicate_cleaner_state["error_message"] = "Please configure Transfer folder in settings"
- print(f"❌ [Duplicate Cleaner] Transfer folder not found: {transfer_folder}")
+ print(f"[Duplicate Cleaner] Transfer folder not found: {transfer_folder}")
return
# Create deleted folder if it doesn't exist
deleted_folder = os.path.join(transfer_folder, 'deleted')
os.makedirs(deleted_folder, exist_ok=True)
- print(f"📁 [Duplicate Cleaner] Deleted folder: {deleted_folder}")
+ print(f"[Duplicate Cleaner] Deleted folder: {deleted_folder}")
# Phase 1: Count total files for progress tracking
with duplicate_cleaner_lock:
@@ -25446,7 +25446,7 @@ def _run_duplicate_cleaner():
dirs.remove('deleted')
total_files += len(files)
- print(f"📊 [Duplicate Cleaner] Found {total_files} total files to scan")
+ print(f"[Duplicate Cleaner] Found {total_files} total files to scan")
with duplicate_cleaner_lock:
duplicate_cleaner_state["total_files"] = total_files
@@ -25513,7 +25513,7 @@ def _run_duplicate_cleaner():
continue
duplicates_found += len(file_versions) - 1 # Count all but the one we keep
- print(f"🔍 [Duplicate Cleaner] Found {len(file_versions)} versions of '{filename}' in {directory}")
+ print(f"[Duplicate Cleaner] Found {len(file_versions)} versions of '{filename}' in {directory}")
# Sort by priority: best format first, then largest size
def sort_key(f):
@@ -25525,7 +25525,7 @@ def _run_duplicate_cleaner():
# Keep the first one (best quality), delete the rest
best_version = sorted_versions[0]
- print(f"✅ [Duplicate Cleaner] Keeping: {os.path.basename(best_version['full_path'])} "
+ print(f"[Duplicate Cleaner] Keeping: {os.path.basename(best_version['full_path'])} "
f"({best_version['extension']}, {best_version['size']} bytes)")
for duplicate_file in sorted_versions[1:]:
@@ -25544,7 +25544,7 @@ def _run_duplicate_cleaner():
deleted_count += 1
space_freed += duplicate_file['size']
- print(f"🗑️ [Duplicate Cleaner] Moved to deleted: {os.path.basename(duplicate_file['full_path'])} "
+ print(f"[Duplicate Cleaner] Moved to deleted: {os.path.basename(duplicate_file['full_path'])} "
f"({duplicate_file['extension']}, {duplicate_file['size']} bytes)")
# Update stats
@@ -25554,7 +25554,7 @@ def _run_duplicate_cleaner():
duplicate_cleaner_state["duplicates_found"] = duplicates_found
except Exception as e:
- print(f"❌ [Duplicate Cleaner] Error moving file {duplicate_file['full_path']}: {e}")
+ print(f"[Duplicate Cleaner] Error moving file {duplicate_file['full_path']}: {e}")
continue
# Scan complete
@@ -25564,12 +25564,12 @@ def _run_duplicate_cleaner():
duplicate_cleaner_state["phase"] = "Cleaning complete"
space_mb = space_freed / (1024 * 1024)
- print(f"✅ [Duplicate Cleaner] Scan complete: {files_scanned} files scanned, "
+ print(f"[Duplicate Cleaner] Scan complete: {files_scanned} files scanned, "
f"{duplicates_found} duplicates found, {deleted_count} files moved to deleted folder, "
f"{space_mb:.2f} MB freed")
# Add activity
- add_activity_item("🧹", "Duplicate Cleaner Complete",
+ add_activity_item("", "Duplicate Cleaner Complete",
f"{deleted_count} files removed, {space_mb:.1f} MB freed", "Now")
try:
@@ -25583,7 +25583,7 @@ def _run_duplicate_cleaner():
pass
except Exception as e:
- print(f"❌ [Duplicate Cleaner] Critical error: {e}")
+ print(f"[Duplicate Cleaner] Critical error: {e}")
import traceback
traceback.print_exc()
@@ -25602,7 +25602,7 @@ def start_quality_scan():
data = request.get_json() or {}
scope = data.get('scope', 'watchlist') # 'watchlist' or 'all'
- print(f"🔍 [Quality Scanner API] Starting scan with scope: {scope}")
+ print(f"[Quality Scanner API] Starting scan with scope: {scope}")
# Reset state
quality_scanner_state["status"] = "running"
@@ -25620,7 +25620,7 @@ def start_quality_scan():
scan_profile_id = get_current_profile_id()
quality_scanner_executor.submit(_run_quality_scanner, scope, scan_profile_id)
- add_activity_item("🔍", "Quality Scan Started", f"Scanning {scope} tracks", "Now")
+ add_activity_item("", "Quality Scan Started", f"Scanning {scope} tracks", "Now")
return jsonify({"success": True, "message": "Quality scan started"})
@@ -25648,7 +25648,7 @@ def start_duplicate_cleaner():
if duplicate_cleaner_state["status"] == "running":
return jsonify({"success": False, "error": "A scan is already in progress"}), 409
- print(f"🧹 [Duplicate Cleaner API] Starting duplicate cleaner...")
+ print(f"[Duplicate Cleaner API] Starting duplicate cleaner...")
# Reset state
duplicate_cleaner_state["status"] = "running"
@@ -25664,7 +25664,7 @@ def start_duplicate_cleaner():
# Submit worker
duplicate_cleaner_executor.submit(_run_duplicate_cleaner)
- add_activity_item("🧹", "Duplicate Cleaner Started", "Scanning Transfer folder", "Now")
+ add_activity_item("", "Duplicate Cleaner Started", "Scanning Transfer folder", "Now")
return jsonify({"success": True, "message": "Duplicate cleaner started"})
@@ -25739,7 +25739,7 @@ def search_retag_albums():
})
return jsonify({"success": True, "albums": albums})
except Exception as e:
- print(f"❌ [Retag] Album search error: {e}")
+ print(f"[Retag] Album search error: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/retag/execute', methods=['POST'])
@@ -25896,15 +25896,15 @@ def get_valid_candidates(results, spotify_track, query):
# Sort by confidence (best match first)
scored.sort(key=lambda x: x.confidence, reverse=True)
best = scored[0]
- print(f"🎵 [{source_label}] {len(scored)}/{len(results)} candidates passed validation "
+ print(f"[{source_label}] {len(scored)}/{len(results)} candidates passed validation "
f"(best: {best.confidence:.2f} '{best.artist} - {best.title}')")
return scored
else:
if results[0].username == 'youtube':
- print(f"⚠️ [{source_label}] No streaming results passed validation — falling through to filename matching")
+ print(f"[{source_label}] No streaming results passed validation — falling through to filename matching")
# YouTube artist data is unreliable, allow fallback to filename-based matching
else:
- print(f"⚠️ [{source_label}] No streaming results passed validation (threshold: 0.60, artist gate: 0.40) — rejecting all candidates")
+ print(f"[{source_label}] No streaming results passed validation (threshold: 0.60, artist gate: 0.40) — rejecting all candidates")
return [] # Tidal/Qobuz/HiFi/Deezer have structured metadata; don't fall back to filename matching
# Uses the existing, powerful matching engine for scoring (Soulseek P2P results)
@@ -25918,7 +25918,7 @@ def get_valid_candidates(results, spotify_track, query):
if is_streaming_source:
source_label = initial_candidates[0].username.title()
- print(f"🎵 [{source_label}] Skipping quality filter - streaming source handles quality internally")
+ print(f"[{source_label}] Skipping quality filter - streaming source handles quality internally")
quality_filtered_candidates = initial_candidates
else:
# Filter by user's quality profile before artist verification (Soulseek only)
@@ -25930,7 +25930,7 @@ def get_valid_candidates(results, spotify_track, query):
# and no results match, we should fail the download rather than force a fallback.
# The quality filter already has its own fallback logic controlled by the user's settings.
if not quality_filtered_candidates:
- print(f"⚠️ [Quality Filter] No candidates match quality profile - download will fail per user preferences")
+ print(f"[Quality Filter] No candidates match quality profile - download will fail per user preferences")
return []
verified_candidates = []
@@ -25989,18 +25989,18 @@ def _recover_worker_slot(batch_id, task_id):
This prevents permanent worker slot leaks that cause modal to show wrong worker counts.
"""
try:
- print(f"🚨 [Worker Recovery] Attempting to recover worker slot for batch {batch_id}, task {task_id}")
+ print(f"[Worker Recovery] Attempting to recover worker slot for batch {batch_id}, task {task_id}")
# Acquire lock with timeout to prevent deadlock
lock_acquired = tasks_lock.acquire(timeout=3.0)
if not lock_acquired:
- print(f"💀 [Worker Recovery] FATAL: Could not acquire lock for recovery - worker slot LEAKED")
+ print(f"[Worker Recovery] FATAL: Could not acquire lock for recovery - worker slot LEAKED")
return False
try:
# Verify batch still exists
if batch_id not in download_batches:
- print(f"⚠️ [Worker Recovery] Batch {batch_id} not found - nothing to recover")
+ print(f"[Worker Recovery] Batch {batch_id} not found - nothing to recover")
return True
batch = download_batches[batch_id]
@@ -26010,11 +26010,11 @@ def _recover_worker_slot(batch_id, task_id):
if old_active > 0:
batch['active_count'] -= 1
new_active = batch['active_count']
- print(f"✅ [Worker Recovery] Recovered worker slot - Active count: {old_active} → {new_active}")
+ print(f"[Worker Recovery] Recovered worker slot - Active count: {old_active} → {new_active}")
# Try to start next worker if queue isn't empty
if batch['queue_index'] < len(batch['queue']) and new_active < batch['max_concurrent']:
- print(f"🔄 [Worker Recovery] Attempting to start replacement worker")
+ print(f"[Worker Recovery] Attempting to start replacement worker")
# Release lock temporarily to avoid deadlock in _start_next_batch_of_downloads
tasks_lock.release()
try:
@@ -26025,14 +26025,14 @@ def _recover_worker_slot(batch_id, task_id):
return True
else:
- print(f"⚠️ [Worker Recovery] Active count already 0 - no recovery needed")
+ print(f"[Worker Recovery] Active count already 0 - no recovery needed")
return True
finally:
tasks_lock.release()
except Exception as recovery_error:
- print(f"💀 [Worker Recovery] FATAL ERROR in recovery: {recovery_error}")
+ print(f"[Worker Recovery] FATAL ERROR in recovery: {recovery_error}")
return False
def _get_batch_lock(batch_id):
@@ -26051,7 +26051,7 @@ def _start_next_batch_of_downloads(batch_id):
with batch_lock:
# Prevent starting new tasks if shutting down
if IS_SHUTTING_DOWN:
- print(f"🛑 [Batch Manager] Server shutting down - skipping new tasks for batch {batch_id}")
+ print(f"[Batch Manager] Server shutting down - skipping new tasks for batch {batch_id}")
return
with tasks_lock:
@@ -26064,7 +26064,7 @@ def _start_next_batch_of_downloads(batch_id):
queue_index = batch['queue_index']
active_count = batch['active_count']
- print(f"🔍 [Batch Lock] Starting workers for {batch_id}: active={active_count}, max={max_concurrent}, queue_pos={queue_index}/{len(queue)}")
+ print(f"[Batch Lock] Starting workers for {batch_id}: active={active_count}, max={max_concurrent}, queue_pos={queue_index}/{len(queue)}")
# Start downloads up to the concurrent limit
while active_count < max_concurrent and queue_index < len(queue):
@@ -26074,7 +26074,7 @@ def _start_next_batch_of_downloads(batch_id):
if task_id in download_tasks:
current_status = download_tasks[task_id]['status']
if current_status == 'cancelled':
- print(f"⏭️ [Batch Lock] Skipping cancelled task {task_id} (queue position {queue_index + 1})")
+ print(f"[Batch Lock] Skipping cancelled task {task_id} (queue position {queue_index + 1})")
download_batches[batch_id]['queue_index'] += 1
queue_index += 1
continue # Skip to next task without consuming worker slot
@@ -26083,9 +26083,9 @@ def _start_next_batch_of_downloads(batch_id):
# Must be done INSIDE the lock to prevent race conditions with status polling
download_tasks[task_id]['status'] = 'searching'
download_tasks[task_id]['status_change_time'] = time.time()
- print(f"🔧 [Batch Manager] Set task {task_id} status to 'searching'")
+ print(f"[Batch Manager] Set task {task_id} status to 'searching'")
else:
- print(f"⚠️ [Batch Lock] Task {task_id} not found in download_tasks - skipping")
+ print(f"[Batch Lock] Task {task_id} not found in download_tasks - skipping")
download_batches[batch_id]['queue_index'] += 1
queue_index += 1
continue
@@ -26099,26 +26099,26 @@ def _start_next_batch_of_downloads(batch_id):
download_batches[batch_id]['active_count'] += 1
download_batches[batch_id]['queue_index'] += 1
- print(f"🔄 [Batch Lock] Started download {queue_index + 1}/{len(queue)} - Active: {active_count + 1}/{max_concurrent}")
+ print(f"[Batch Lock] Started download {queue_index + 1}/{len(queue)} - Active: {active_count + 1}/{max_concurrent}")
# Update local counters for next iteration
active_count += 1
queue_index += 1
except Exception as submit_error:
- print(f"❌ [Batch Lock] CRITICAL: Failed to submit task {task_id} to executor: {submit_error}")
- print(f"🚨 [Batch Lock] Worker slot NOT consumed - preventing ghost worker")
+ print(f"[Batch Lock] CRITICAL: Failed to submit task {task_id} to executor: {submit_error}")
+ print(f"[Batch Lock] Worker slot NOT consumed - preventing ghost worker")
# Reset task status since worker never started
if task_id in download_tasks:
download_tasks[task_id]['status'] = 'failed'
- print(f"🔧 [Batch Lock] Set task {task_id} status to 'failed' due to submit failure")
+ print(f"[Batch Lock] Set task {task_id} status to 'failed' due to submit failure")
# Don't increment counters - no worker was actually started
# This prevents the "ghost worker" issue where active_count is incremented but no actual worker runs
break # Stop trying to start more workers if executor is failing
- print(f"✅ [Batch Lock] Finished starting workers for {batch_id}: final_active={download_batches[batch_id]['active_count']}, max={max_concurrent}")
+ print(f"[Batch Lock] Finished starting workers for {batch_id}: final_active={download_batches[batch_id]['active_count']}, max={max_concurrent}")
def _get_track_artist_name(track_info):
"""Extract artist name from track info, handling different data formats (replicating sync.py)"""
@@ -26224,11 +26224,11 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
from core.wishlist_service import get_wishlist_service
from datetime import datetime
- print(f"🔍 [Wishlist Processing] Starting wishlist processing for batch {batch_id}")
+ print(f"[Wishlist Processing] Starting wishlist processing for batch {batch_id}")
with tasks_lock:
if batch_id not in download_batches:
- print(f"⚠️ [Wishlist Processing] Batch {batch_id} not found")
+ print(f"[Wishlist Processing] Batch {batch_id} not found")
return {'tracks_added': 0, 'errors': 0}
batch = download_batches[batch_id]
@@ -26236,13 +26236,13 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
# Wing It mode — skip wishlist entirely for failed tracks
if batch.get('wing_it'):
failed_count = len(batch.get('permanently_failed_tracks', []))
- print(f"⚡ [Wing It] Skipping wishlist for {failed_count} failed tracks (wing it mode)")
+ print(f"[Wing It] Skipping wishlist for {failed_count} failed tracks (wing it mode)")
return {'tracks_added': 0, 'errors': 0}
permanently_failed_tracks = batch.get('permanently_failed_tracks', [])
cancelled_tracks = batch.get('cancelled_tracks', set())
# STEP 0: Remove completed tracks from wishlist (THIS WAS MISSING!)
- print(f"🔍 [Wishlist Processing] Checking completed tracks for wishlist removal")
+ print(f"[Wishlist Processing] Checking completed tracks for wishlist removal")
for task_id in batch.get('queue', []):
if task_id in download_tasks:
task = download_tasks[task_id]
@@ -26252,12 +26252,12 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
context = {'track_info': track_info, 'original_search_result': track_info}
_check_and_remove_from_wishlist(context)
except Exception as e:
- print(f"⚠️ [Wishlist Processing] Error removing completed track from wishlist: {e}")
+ print(f"[Wishlist Processing] Error removing completed track from wishlist: {e}")
# STEP 1: Add cancelled tracks that were missing to permanently_failed_tracks (replicating sync.py)
# This matches sync.py's logic for adding cancelled missing tracks to the failed list
if cancelled_tracks:
- print(f"🔍 [Wishlist Processing] Processing {len(cancelled_tracks)} cancelled tracks")
+ print(f"[Wishlist Processing] Processing {len(cancelled_tracks)} cancelled tracks")
# Process cancelled tracks with safeguard to prevent infinite loops
processed_count = 0
@@ -26291,9 +26291,9 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
if not any(t.get('table_index') == track_index for t in permanently_failed_tracks):
permanently_failed_tracks.append(cancelled_track_info)
processed_count += 1
- print(f"🚫 [Wishlist Processing] Added cancelled missing track {cancelled_track_info['track_name']} to failed list for wishlist")
+ print(f"[Wishlist Processing] Added cancelled missing track {cancelled_track_info['track_name']} to failed list for wishlist")
- print(f"🔍 [Wishlist Processing] Processed {processed_count} cancelled tracks")
+ print(f"[Wishlist Processing] Processed {processed_count} cancelled tracks")
# STEP 1.5: Recover any failed/not_found tasks not captured in permanently_failed_tracks.
# Stuck detection (in _on_download_completed, _check_batch_completion_v2, and the Safety Valve)
@@ -26319,14 +26319,14 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
'candidates': task.get('cached_candidates', [])
}
permanently_failed_tracks.append(recovered_track_info)
- print(f"📋 [Wishlist Processing] Recovered uncaptured failed track for wishlist: {recovered_track_info['track_name']}")
+ print(f"[Wishlist Processing] Recovered uncaptured failed track for wishlist: {recovered_track_info['track_name']}")
# STEP 2: Add permanently failed tracks to wishlist (exact sync.py logic)
failed_count = len(permanently_failed_tracks)
wishlist_added_count = 0
error_count = 0
- print(f"🔍 [Wishlist Processing] Processing {failed_count} failed tracks for wishlist")
+ print(f"[Wishlist Processing] Processing {failed_count} failed tracks for wishlist")
if permanently_failed_tracks:
try:
@@ -26345,7 +26345,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
for i, failed_track_info in enumerate(permanently_failed_tracks[:max_failed_tracks]):
try:
track_name = failed_track_info.get('track_name', f'Track {i+1}')
- print(f"🔍 [Wishlist Processing] Adding track {i+1}/{max_failed_tracks}: {track_name}")
+ print(f"[Wishlist Processing] Adding track {i+1}/{max_failed_tracks}: {track_name}")
success = wishlist_service.add_failed_track_from_modal(
track_info=failed_track_info,
@@ -26355,7 +26355,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
)
if success:
wishlist_added_count += 1
- print(f"✅ [Wishlist Processing] Added {track_name} to wishlist")
+ print(f"[Wishlist Processing] Added {track_name} to wishlist")
try:
if automation_engine:
automation_engine.emit('wishlist_item_added', {
@@ -26366,17 +26366,17 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
except Exception:
pass
else:
- print(f"⚠️ [Wishlist Processing] Failed to add {track_name} to wishlist")
+ print(f"[Wishlist Processing] Failed to add {track_name} to wishlist")
except Exception as e:
error_count += 1
- print(f"❌ [Wishlist Processing] Exception adding track to wishlist: {e}")
+ print(f"[Wishlist Processing] Exception adding track to wishlist: {e}")
- print(f"✨ [Wishlist Processing] Added {wishlist_added_count}/{failed_count} failed tracks to wishlist (errors: {error_count})")
+ print(f"[Wishlist Processing] Added {wishlist_added_count}/{failed_count} failed tracks to wishlist (errors: {error_count})")
except Exception as e:
error_count = len(permanently_failed_tracks)
- print(f"❌ [Wishlist Processing] Critical error adding failed tracks to wishlist: {e}")
+ print(f"[Wishlist Processing] Critical error adding failed tracks to wishlist: {e}")
import traceback
traceback.print_exc()
else:
@@ -26395,26 +26395,26 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
download_batches[batch_id]['wishlist_processing_complete'] = True
# Phase already set to 'complete' in _on_download_completed
- print(f"✅ [Wishlist Processing] Completed wishlist processing for batch {batch_id}")
+ print(f"[Wishlist Processing] Completed wishlist processing for batch {batch_id}")
# Auto-cleanup: Clear completed downloads from slskd
try:
- logger.info(f"🧹 [Auto-Cleanup] Clearing completed downloads from slskd after batch {batch_id}")
+ logger.info(f"[Auto-Cleanup] Clearing completed downloads from slskd after batch {batch_id}")
run_async(soulseek_client.clear_all_completed_downloads())
- logger.info(f"✅ [Auto-Cleanup] Completed downloads cleared from slskd")
+ logger.info(f"[Auto-Cleanup] Completed downloads cleared from slskd")
except Exception as cleanup_error:
- logger.warning(f"⚠️ [Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}")
+ logger.warning(f"[Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}")
# Sweep empty directories left behind by this batch's downloads
try:
_sweep_empty_download_directories()
except Exception as sweep_error:
- logger.warning(f"⚠️ [Auto-Cleanup] Failed to sweep empty directories: {sweep_error}")
+ logger.warning(f"[Auto-Cleanup] Failed to sweep empty directories: {sweep_error}")
return completion_summary
except Exception as e:
- print(f"❌ [Wishlist Processing] CRITICAL ERROR in wishlist processing: {e}")
+ print(f"[Wishlist Processing] CRITICAL ERROR in wishlist processing: {e}")
import traceback
traceback.print_exc()
@@ -26432,7 +26432,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
}
download_batches[batch_id]['wishlist_processing_complete'] = True
except Exception as lock_error:
- print(f"❌ [Wishlist Processing] Failed to update batch after error: {lock_error}")
+ print(f"[Wishlist Processing] Failed to update batch after error: {lock_error}")
return {'tracks_added': 0, 'errors': 1, 'total_failed': 0}
@@ -26444,7 +26444,7 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id):
global wishlist_auto_processing
try:
- print(f"🤖 [Auto-Wishlist] Processing completion for auto-initiated batch {batch_id}")
+ print(f"[Auto-Wishlist] Processing completion for auto-initiated batch {batch_id}")
# Run standard wishlist processing
completion_summary = _process_failed_tracks_to_wishlist_exact(batch_id)
@@ -26452,11 +26452,11 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id):
# Log auto-processing completion
tracks_added = completion_summary.get('tracks_added', 0)
total_failed = completion_summary.get('total_failed', 0)
- print(f"🎉 [Auto-Wishlist] Background processing complete: {tracks_added} added to wishlist, {total_failed} failed")
+ print(f"[Auto-Wishlist] Background processing complete: {tracks_added} added to wishlist, {total_failed} failed")
# Add activity for wishlist processing
if tracks_added > 0:
- add_activity_item("⭐", "Wishlist Updated", f"{tracks_added} failed tracks added to wishlist", "Now")
+ add_activity_item("", "Wishlist Updated", f"{tracks_added} failed tracks added to wishlist", "Now")
# TOGGLE CYCLE: Switch to next category for next run
try:
@@ -26478,9 +26478,9 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id):
""", (next_cycle,))
conn.commit()
- print(f"🔄 [Auto-Wishlist] Cycle toggled after completion: {current_cycle} → {next_cycle}")
+ print(f"[Auto-Wishlist] Cycle toggled after completion: {current_cycle} → {next_cycle}")
except Exception as cycle_error:
- print(f"⚠️ [Auto-Wishlist] Error toggling cycle: {cycle_error}")
+ print(f"[Auto-Wishlist] Error toggling cycle: {cycle_error}")
# Mark auto-processing as complete and reset timestamp
with wishlist_timer_lock:
@@ -26500,7 +26500,7 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id):
return completion_summary
except Exception as e:
- print(f"❌ [Auto-Wishlist] Error in auto-completion processing: {e}")
+ print(f"[Auto-Wishlist] Error in auto-completion processing: {e}")
import traceback
traceback.print_exc()
@@ -26515,7 +26515,7 @@ def _on_download_completed(batch_id, task_id, success=True):
"""Called when a download completes to start the next one in queue"""
with tasks_lock:
if batch_id not in download_batches:
- print(f"⚠️ [Batch Manager] Batch {batch_id} not found for completed task {task_id}")
+ print(f"[Batch Manager] Batch {batch_id} not found for completed task {task_id}")
return
# Guard against double-calling: track which tasks have already been completed
@@ -26528,7 +26528,7 @@ def _on_download_completed(batch_id, task_id, success=True):
completed_tasks = download_batches[batch_id].setdefault('_completed_task_ids', set())
_is_duplicate_completion = task_id in completed_tasks
if _is_duplicate_completion:
- print(f"⚠️ [Batch Manager] Task {task_id} already completed — skipping decrement, still checking batch completion")
+ print(f"[Batch Manager] Task {task_id} already completed — skipping decrement, still checking batch completion")
# Set terminal status so the monitor loop stops re-processing this task
if task_id in download_tasks and download_tasks[task_id].get('status') in ('downloading', 'queued'):
download_tasks[task_id]['status'] = 'completed'
@@ -26561,16 +26561,16 @@ def _on_download_completed(batch_id, task_id, success=True):
if task_status == 'cancelled':
download_batches[batch_id]['cancelled_tracks'].add(task.get('track_index', 0))
- print(f"🚫 [Batch Manager] Added cancelled track to batch tracking: {track_info['track_name']}")
- add_activity_item("🚫", "Download Cancelled", f"'{track_info['track_name']}'", "Now")
+ print(f"[Batch Manager] Added cancelled track to batch tracking: {track_info['track_name']}")
+ add_activity_item("", "Download Cancelled", f"'{track_info['track_name']}'", "Now")
elif task_status in ('failed', 'not_found'):
download_batches[batch_id]['permanently_failed_tracks'].append(track_info)
if task_status == 'not_found':
- print(f"🔇 [Batch Manager] Added not-found track to batch tracking: {track_info['track_name']}")
- add_activity_item("🔇", "Not Found", f"'{track_info['track_name']}'", "Now")
+ print(f"[Batch Manager] Added not-found track to batch tracking: {track_info['track_name']}")
+ add_activity_item("", "Not Found", f"'{track_info['track_name']}'", "Now")
else:
- print(f"❌ [Batch Manager] Added failed track to batch tracking: {track_info['track_name']}")
- add_activity_item("❌", "Download Failed", f"'{track_info['track_name']}'", "Now")
+ print(f"[Batch Manager] Added failed track to batch tracking: {track_info['track_name']}")
+ add_activity_item("", "Download Failed", f"'{track_info['track_name']}'", "Now")
try:
if automation_engine:
@@ -26587,7 +26587,7 @@ def _on_download_completed(batch_id, task_id, success=True):
try:
task = download_tasks[task_id]
track_info = task.get('track_info', {})
- print(f"📋 [Batch Manager] Successful download - checking wishlist removal for task {task_id}")
+ print(f"[Batch Manager] Successful download - checking wishlist removal for task {task_id}")
# Add activity for successful download
track_name = track_info.get('name', 'Unknown Track')
@@ -26602,7 +26602,7 @@ def _on_download_completed(batch_id, task_id, success=True):
else:
artist_name = 'Unknown Artist'
- add_activity_item("📥", "Download Complete", f"'{track_name}' by {artist_name}", "Now")
+ add_activity_item("", "Download Complete", f"'{track_name}' by {artist_name}", "Now")
# Try to remove from wishlist using track info
if track_info:
@@ -26613,18 +26613,18 @@ def _on_download_completed(batch_id, task_id, success=True):
}
_check_and_remove_from_wishlist(context)
except Exception as wishlist_error:
- print(f"⚠️ [Batch Manager] Error checking wishlist removal for successful download: {wishlist_error}")
+ print(f"[Batch Manager] Error checking wishlist removal for successful download: {wishlist_error}")
# Decrement active count
old_active = download_batches[batch_id]['active_count']
download_batches[batch_id]['active_count'] -= 1
new_active = download_batches[batch_id]['active_count']
- print(f"🔄 [Batch Manager] Task {task_id} completed ({'success' if success else 'failed/cancelled'}). Active workers: {old_active} → {new_active}/{download_batches[batch_id]['max_concurrent']}")
+ print(f"[Batch Manager] Task {task_id} completed ({'success' if success else 'failed/cancelled'}). Active workers: {old_active} → {new_active}/{download_batches[batch_id]['max_concurrent']}")
# ENHANCED: Always check batch completion after any task completes (including duplicate calls)
# This ensures completion is detected even when mixing normal downloads with cancelled tasks
- print(f"🔍 [Batch Manager] Checking batch completion after task {task_id} completed")
+ print(f"[Batch Manager] Checking batch completion after task {task_id} completed")
# FIXED: Check if batch is truly complete (all tasks finished, not just workers freed)
batch = download_batches[batch_id]
@@ -26665,19 +26665,19 @@ def _on_download_completed(batch_id, task_id, success=True):
finished_count += 1
else:
# Task ID in queue but not in download_tasks - treat as completed to prevent blocking
- print(f"⚠️ [Orphaned Task] Task {task_id} in queue but not in download_tasks - counting as finished")
+ print(f"[Orphaned Task] Task {task_id} in queue but not in download_tasks - counting as finished")
finished_count += 1
all_tasks_truly_finished = finished_count >= len(queue)
has_retrying_tasks = retrying_count > 0
if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks:
- print(f"🎉 [Batch Manager] Batch {batch_id} truly complete - all {finished_count}/{len(queue)} tasks finished - processing failed tracks to wishlist")
+ print(f"[Batch Manager] Batch {batch_id} truly complete - all {finished_count}/{len(queue)} tasks finished - processing failed tracks to wishlist")
elif all_tasks_started and no_active_workers and has_retrying_tasks:
- print(f"🔄 [Batch Manager] Batch {batch_id}: all workers free but {retrying_count} tasks retrying - continuing monitoring")
+ print(f"[Batch Manager] Batch {batch_id}: all workers free but {retrying_count} tasks retrying - continuing monitoring")
elif all_tasks_started and no_active_workers:
# This used to incorrectly mark batch as complete!
- print(f"📊 [Batch Manager] Batch {batch_id}: all workers free but only {finished_count}/{len(queue)} tasks finished - continuing monitoring")
+ print(f"[Batch Manager] Batch {batch_id}: all workers free but only {finished_count}/{len(queue)} tasks finished - continuing monitoring")
if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks:
@@ -26697,7 +26697,7 @@ def _on_download_completed(batch_id, task_id, success=True):
playlist_name = batch.get('playlist_name', 'Unknown Playlist')
failed_count = len(batch.get('permanently_failed_tracks', []))
successful_downloads = finished_count - failed_count
- add_activity_item("✅", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now")
+ add_activity_item("", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now")
# Emit batch_complete event for automation engine (only if something downloaded)
if successful_downloads > 0:
@@ -26718,30 +26718,30 @@ def _on_download_completed(batch_id, task_id, success=True):
url_hash = playlist_id.replace('youtube_', '')
if url_hash in youtube_playlist_states:
youtube_playlist_states[url_hash]['phase'] = 'download_complete'
- print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase")
+ print(f"Updated YouTube playlist {url_hash} to download_complete phase")
# Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist
if playlist_id and playlist_id.startswith('tidal_'):
tidal_playlist_id = playlist_id.replace('tidal_', '')
if tidal_playlist_id in tidal_discovery_states:
tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete'
- print(f"📋 Updated Tidal playlist {tidal_playlist_id} to download_complete phase")
+ print(f"Updated Tidal playlist {tidal_playlist_id} to download_complete phase")
# Update Deezer playlist phase to 'download_complete' if this is a Deezer playlist
if playlist_id and playlist_id.startswith('deezer_'):
deezer_playlist_id = playlist_id.replace('deezer_', '')
if deezer_playlist_id in deezer_discovery_states:
deezer_discovery_states[deezer_playlist_id]['phase'] = 'download_complete'
- print(f"📋 Updated Deezer playlist {deezer_playlist_id} to download_complete phase")
+ print(f"Updated Deezer playlist {deezer_playlist_id} to download_complete phase")
# Update Spotify Public playlist phase to 'download_complete' if this is a Spotify Public playlist
if playlist_id and playlist_id.startswith('spotify_public_'):
spotify_public_url_hash = playlist_id.replace('spotify_public_', '')
if spotify_public_url_hash in spotify_public_discovery_states:
spotify_public_discovery_states[spotify_public_url_hash]['phase'] = 'download_complete'
- print(f"📋 Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase")
+ print(f"Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase")
- print(f"🎉 [Batch Manager] Batch {batch_id} complete - stopping monitor")
+ print(f"[Batch Manager] Batch {batch_id} complete - stopping monitor")
download_monitor.stop_monitoring(batch_id)
# REPAIR: Scan all album folders from this batch for track number issues
@@ -26759,12 +26759,12 @@ def _on_download_completed(batch_id, task_id, success=True):
# For manual batches, use standard wishlist processing
missing_download_executor.submit(_process_failed_tracks_to_wishlist_exact, batch_id)
else:
- print(f"✅ [Batch Manager] Batch {batch_id} already marked complete - skipping duplicate processing")
+ print(f"[Batch Manager] Batch {batch_id} already marked complete - skipping duplicate processing")
return # Don't start next batch if we're done
# Start next downloads in queue
- print(f"🔄 [Batch Manager] Starting next batch for {batch_id}")
+ print(f"[Batch Manager] Starting next batch for {batch_id}")
_start_next_batch_of_downloads(batch_id)
def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
@@ -26799,7 +26799,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
batch_artist_context = download_batches[batch_id].get('artist_context')
if force_download_all:
- print(f"🔄 [Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing")
+ print(f"[Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing")
# PREFLIGHT: Pre-populate MusicBrainz release cache for album downloads.
# This ensures ALL tracks in the album use the same release MBID during
@@ -26824,12 +26824,12 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
# Also cache the full release detail for tag extraction
with _mb_release_detail_cache_lock:
_mb_release_detail_cache[release_mbid] = release
- print(f"🏷️ [Preflight] Pre-cached MB release for '{album_name_pf}': "
+ print(f"[Preflight] Pre-cached MB release for '{album_name_pf}': "
f"'{release.get('title', '')}' ({release_mbid[:8]}...)")
else:
- print(f"⚠️ [Preflight] No MB release found for '{album_name_pf}' — per-track lookup will be used")
+ print(f"[Preflight] No MB release found for '{album_name_pf}' — per-track lookup will be used")
except Exception as pf_err:
- print(f"⚠️ [Preflight] MB release preflight failed: {pf_err}")
+ print(f"[Preflight] MB release preflight failed: {pf_err}")
# ALBUM FAST PATH: If this is an album download, try to find the album in the DB first
# and match tracks within it — faster and more accurate than N global searches
@@ -26850,11 +26850,11 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
db_album_tracks = db.get_tracks_by_album(db_album.id)
for t in db_album_tracks:
album_tracks_map[t.title.lower().strip()] = t
- print(f"📀 [Album Analysis] Found album '{db_album.title}' in DB with {len(db_album_tracks)} tracks (confidence: {album_confidence:.2f})")
+ print(f"[Album Analysis] Found album '{db_album.title}' in DB with {len(db_album_tracks)} tracks (confidence: {album_confidence:.2f})")
else:
- print(f"📀 [Album Analysis] Album '{album_name}' not found in DB — falling back to per-track search")
+ print(f"[Album Analysis] Album '{album_name}' not found in DB — falling back to per-track search")
except Exception as album_err:
- print(f"⚠️ [Album Analysis] Album lookup error: {album_err} — falling back to per-track search")
+ print(f"[Album Analysis] Album lookup error: {album_err} — falling back to per-track search")
for i, track_data in enumerate(tracks_json):
# Use original table index if provided (for partial track selection),
@@ -26866,7 +26866,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
# Skip database check if force download is enabled
if force_download_all:
- print(f"🔄 [Force Download] Skipping database check for '{track_name}' - treating as missing")
+ print(f"[Force Download] Skipping database check for '{track_name}' - treating as missing")
found, confidence = False, 0.0
elif album_tracks_map:
# Album-scoped matching: check against known album tracks first
@@ -26924,7 +26924,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
try:
_check_and_remove_track_from_wishlist_by_metadata(track_data)
except Exception as wishlist_error:
- print(f"⚠️ [Analysis] Error checking wishlist removal for found track: {wishlist_error}")
+ print(f"[Analysis] Error checking wishlist removal for found track: {wishlist_error}")
with tasks_lock:
if batch_id in download_batches:
@@ -26940,7 +26940,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
missing_tracks = [res for res in missing_tracks if not _is_explicit_blocked(res.get('track', {}))]
skipped = before_count - len(missing_tracks)
if skipped > 0:
- print(f"🚫 [Content Filter] Filtered out {skipped} explicit track(s) from download queue")
+ print(f"[Content Filter] Filtered out {skipped} explicit track(s) from download queue")
with tasks_lock:
if batch_id in download_batches:
@@ -26948,7 +26948,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
# PHASE 2: TRANSITION TO DOWNLOAD (if necessary)
if not missing_tracks:
- print(f"✅ Analysis for batch {batch_id} complete. No missing tracks.")
+ print(f"Analysis for batch {batch_id} complete. No missing tracks.")
# Record sync history — all tracks found, nothing to download
tracks_found = sum(1 for r in analysis_results if r.get('found'))
@@ -26999,32 +26999,32 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
url_hash = playlist_id.replace('youtube_', '')
if url_hash in youtube_playlist_states:
youtube_playlist_states[url_hash]['phase'] = 'download_complete'
- print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase (no missing tracks)")
+ print(f"Updated YouTube playlist {url_hash} to download_complete phase (no missing tracks)")
# Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist
if playlist_id.startswith('tidal_'):
tidal_playlist_id = playlist_id.replace('tidal_', '')
if tidal_playlist_id in tidal_discovery_states:
tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete'
- print(f"📋 Updated Tidal playlist {tidal_playlist_id} to download_complete phase (no missing tracks)")
+ print(f"Updated Tidal playlist {tidal_playlist_id} to download_complete phase (no missing tracks)")
# Update Deezer playlist phase to 'download_complete' if this is a Deezer playlist
if playlist_id.startswith('deezer_'):
deezer_playlist_id = playlist_id.replace('deezer_', '')
if deezer_playlist_id in deezer_discovery_states:
deezer_discovery_states[deezer_playlist_id]['phase'] = 'download_complete'
- print(f"📋 Updated Deezer playlist {deezer_playlist_id} to download_complete phase (no missing tracks)")
+ print(f"Updated Deezer playlist {deezer_playlist_id} to download_complete phase (no missing tracks)")
# Update Spotify Public playlist phase to 'download_complete' if this is a Spotify Public playlist
if playlist_id.startswith('spotify_public_'):
spotify_public_url_hash = playlist_id.replace('spotify_public_', '')
if spotify_public_url_hash in spotify_public_discovery_states:
spotify_public_discovery_states[spotify_public_url_hash]['phase'] = 'download_complete'
- print(f"📋 Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase (no missing tracks)")
+ print(f"Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase (no missing tracks)")
# Handle auto-initiated wishlist completion even when no missing tracks
if is_auto_batch and playlist_id == 'wishlist':
- print("🤖 [Auto-Wishlist] No missing tracks found - calling auto-completion handler to toggle cycle and reschedule")
+ print("[Auto-Wishlist] No missing tracks found - calling auto-completion handler to toggle cycle and reschedule")
missing_download_executor.submit(_process_failed_tracks_to_wishlist_exact_with_auto_completion, batch_id)
return
@@ -27058,7 +27058,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
try:
_sr = source_reuse_logger
_sr.info(f"[Album Pre-flight] Searching for '{artist_name} {album_name}'")
- print(f"🔎 [Album Pre-flight] Searching Soulseek for complete album: '{artist_name} - {album_name}'")
+ print(f"[Album Pre-flight] Searching Soulseek for complete album: '{artist_name} - {album_name}'")
slsk = soulseek_client.soulseek if hasattr(soulseek_client, 'soulseek') else soulseek_client
@@ -27097,7 +27097,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
_sr.info(f"[Album Pre-flight] Best album result: {best_album.username}:{best_album.album_path} "
f"({best_album.track_count} tracks, quality={best_album.dominant_quality})")
- print(f"📀 [Album Pre-flight] Found album folder: {best_album.username} — "
+ print(f"[Album Pre-flight] Found album folder: {best_album.username} — "
f"{best_album.track_count} tracks ({best_album.dominant_quality})")
# Browse the user's folder to get all tracks (may have more than search returned)
@@ -27113,7 +27113,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
}
preflight_tracks = folder_tracks
_sr.info(f"[Album Pre-flight] Browsed folder: {len(folder_tracks)} audio tracks available")
- print(f"✅ [Album Pre-flight] Cached {len(folder_tracks)} tracks from {best_album.username} for source reuse")
+ print(f"[Album Pre-flight] Cached {len(folder_tracks)} tracks from {best_album.username} for source reuse")
else:
_sr.info(f"[Album Pre-flight] Browse returned files but no audio tracks")
else:
@@ -27124,16 +27124,16 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
'folder_path': best_album.album_path
}
preflight_tracks = best_album.tracks
- print(f"✅ [Album Pre-flight] Using {len(best_album.tracks)} tracks from search results (browse unavailable)")
+ print(f"[Album Pre-flight] Using {len(best_album.tracks)} tracks from search results (browse unavailable)")
else:
_sr.info(f"[Album Pre-flight] No album results passed quality filter")
- print(f"⚠️ [Album Pre-flight] No album results matched quality preferences")
+ print(f"[Album Pre-flight] No album results matched quality preferences")
else:
_sr.info(f"[Album Pre-flight] Search returned no album results (got {len(track_results)} individual tracks)")
- print(f"⚠️ [Album Pre-flight] No complete album folders found, falling back to track-by-track search")
+ print(f"[Album Pre-flight] No complete album folders found, falling back to track-by-track search")
except Exception as preflight_err:
- print(f"⚠️ [Album Pre-flight] Search failed (non-fatal, falling back to track-by-track): {preflight_err}")
+ print(f"[Album Pre-flight] Search failed (non-fatal, falling back to track-by-track): {preflight_err}")
source_reuse_logger.info(f"[Album Pre-flight] Exception: {preflight_err}")
with tasks_lock:
@@ -27146,7 +27146,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
download_batches[batch_id]['last_good_source'] = preflight_source
download_batches[batch_id]['source_folder_tracks'] = preflight_tracks
download_batches[batch_id]['failed_sources'] = set()
- print(f"📦 [Album Pre-flight] Pre-loaded source reuse data on batch {batch_id}")
+ print(f"[Album Pre-flight] Pre-loaded source reuse data on batch {batch_id}")
# Compute total_discs for multi-disc album subfolder support
# Use ALL tracks (tracks_json), not just missing ones, to correctly detect multi-disc
@@ -27155,7 +27155,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
total_discs = max((t.get('disc_number', 1) for t in tracks_json), default=1)
batch_album_context['total_discs'] = total_discs
if total_discs > 1:
- print(f"💿 [Multi-Disc] Detected {total_discs} discs for album '{batch_album_context.get('name')}'")
+ print(f"[Multi-Disc] Detected {total_discs} discs for album '{batch_album_context.get('name')}'")
# Pre-compute per-album data for wishlist tracks (grouped by album ID)
# Wishlist tracks aren't batch_is_album but each track has disc_number in spotify_data
@@ -27216,7 +27216,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
else:
_fallback_name = t.get('artist', '')
wishlist_album_artist_map[album_id] = {'name': _fallback_name or 'Unknown Artist'}
- print(f"🔗 [Wishlist Album Grouping] Album '{_wl_album.get('name', album_id)}' → artist: '{wishlist_album_artist_map[album_id].get('name', '?')}'")
+ print(f"[Wishlist Album Grouping] Album '{_wl_album.get('name', album_id)}' → artist: '{wishlist_album_artist_map[album_id].get('name', '?')}'")
@@ -27229,7 +27229,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
track_info['_explicit_album_context'] = batch_album_context
track_info['_explicit_artist_context'] = batch_artist_context
track_info['_is_explicit_album_download'] = True
- print(f"🎵 [Task Creation] Added explicit album context for: {track_info.get('name')}")
+ print(f"[Task Creation] Added explicit album context for: {track_info.get('name')}")
# SPECIAL WISHLIST HANDLING: Inject album context if available to force grouping
elif playlist_id == 'wishlist':
@@ -27288,16 +27288,16 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
track_info['_explicit_album_context'] = album_ctx
track_info['_explicit_artist_context'] = artist_ctx
track_info['_is_explicit_album_download'] = True
- print(f"🎵 [Wishlist] Added album context for: '{track_info.get('name')}' -> '{album_ctx['name']}'")
+ print(f"[Wishlist] Added album context for: '{track_info.get('name')}' -> '{album_ctx['name']}'")
# Add playlist folder mode flag for sync page playlists
if batch_playlist_folder_mode:
track_info['_playlist_folder_mode'] = True
track_info['_playlist_name'] = batch_playlist_name
- print(f"📁 [Task Creation] Added playlist folder mode for: {track_info.get('name')} → {batch_playlist_name}")
+ print(f"[Task Creation] Added playlist folder mode for: {track_info.get('name')} → {batch_playlist_name}")
else:
- print(f"🔍 [Debug] Task Creation - playlist folder mode NOT enabled for: {track_info.get('name')}")
+ print(f"[Debug] Task Creation - playlist folder mode NOT enabled for: {track_info.get('name')}")
download_tasks[task_id] = {
'status': 'pending', 'track_info': track_info,
@@ -27313,7 +27313,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
_start_next_batch_of_downloads(batch_id)
except Exception as e:
- print(f"❌ Master worker for batch {batch_id} failed: {e}")
+ print(f"Master worker for batch {batch_id} failed: {e}")
import traceback
traceback.print_exc()
@@ -27329,11 +27329,11 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
url_hash = playlist_id.replace('youtube_', '')
if url_hash in youtube_playlist_states:
youtube_playlist_states[url_hash]['phase'] = 'discovered'
- print(f"📋 Reset YouTube playlist {url_hash} to discovered phase (error)")
+ print(f"Reset YouTube playlist {url_hash} to discovered phase (error)")
# Handle auto-initiated wishlist errors - reset flag
if is_auto_batch and playlist_id == 'wishlist':
- print("❌ [Auto-Wishlist] Master worker error - resetting auto-processing flag")
+ print("[Auto-Wishlist] Master worker error - resetting auto-processing flag")
global wishlist_auto_processing, wishlist_auto_processing_timestamp
with wishlist_timer_lock:
wishlist_auto_processing = False
@@ -27345,21 +27345,21 @@ def _run_post_processing_worker(task_id, batch_id):
after successful file verification and processing. This matches sync.py's reliability.
"""
try:
- print(f"🔧 [Post-Processing] Starting verification for task {task_id}")
+ print(f"[Post-Processing] Starting verification for task {task_id}")
# Retrieve task details from global state
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Post-Processing] Task {task_id} not found in download_tasks")
+ print(f"[Post-Processing] Task {task_id} not found in download_tasks")
return
task = download_tasks[task_id].copy()
# Check if task was cancelled or already completed during post-processing
if task['status'] == 'cancelled':
- print(f"❌ [Post-Processing] Task {task_id} was cancelled, skipping verification")
+ print(f"[Post-Processing] Task {task_id} was cancelled, skipping verification")
return
if task['status'] == 'completed' or task.get('stream_processed'):
- print(f"✅ [Post-Processing] Task {task_id} already completed by stream processor, skipping verification")
+ print(f"[Post-Processing] Task {task_id} already completed by stream processor, skipping verification")
return
# Extract file information for verification
@@ -27368,7 +27368,7 @@ def _run_post_processing_worker(task_id, batch_id):
task_username = task.get('username') or track_info.get('username')
if not task_filename or not task_username:
- print(f"❌ [Post-Processing] Missing filename or username for task {task_id}")
+ print(f"[Post-Processing] Missing filename or username for task {task_id}")
with tasks_lock:
if task_id in download_tasks:
download_tasks[task_id]['status'] = 'failed'
@@ -27384,39 +27384,39 @@ def _run_post_processing_worker(task_id, batch_id):
context_key = _make_context_key(task_username, task_filename)
expected_final_filename = None
- print(f"🔍 [Post-Processing] Looking up context with key: {context_key}")
+ print(f"[Post-Processing] Looking up context with key: {context_key}")
with matched_context_lock:
context = matched_downloads_context.get(context_key)
# Debug: Show all available context keys
available_keys = list(matched_downloads_context.keys())
- print(f"🔍 [Post-Processing] Available context keys: {available_keys[:10]}...") # Show first 10 keys
+ print(f"[Post-Processing] Available context keys: {available_keys[:10]}...") # Show first 10 keys
if context:
- print(f"✅ [Post-Processing] Found context for key: {context_key}")
+ print(f"[Post-Processing] Found context for key: {context_key}")
try:
original_search = context.get("original_search_result", {})
- print(f"🔍 [Post-Processing] original_search keys: {list(original_search.keys())}")
+ print(f"[Post-Processing] original_search keys: {list(original_search.keys())}")
spotify_clean_title = original_search.get('spotify_clean_title')
track_number = original_search.get('track_number')
- print(f"🔍 [Post-Processing] spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}")
+ print(f"[Post-Processing] spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}")
if spotify_clean_title and track_number:
# Generate expected final filename that stream processor would create
# Pattern: f"{track_number:02d} - {clean_title}.flac"
sanitized_title = spotify_clean_title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
expected_final_filename = f"{track_number:02d} - {sanitized_title}.flac"
- print(f"🎯 [Post-Processing] Generated expected final filename: {expected_final_filename}")
+ print(f"[Post-Processing] Generated expected final filename: {expected_final_filename}")
else:
- print(f"❌ [Post-Processing] Missing required data - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}")
+ print(f"[Post-Processing] Missing required data - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}")
except Exception as e:
- print(f"⚠️ [Post-Processing] Error generating expected filename: {e}")
+ print(f"[Post-Processing] Error generating expected filename: {e}")
import traceback
traceback.print_exc()
else:
- print(f"❌ [Post-Processing] No context found for key: {context_key}")
+ print(f"[Post-Processing] No context found for key: {context_key}")
# Try fuzzy matching with similar keys containing the filename
# SAFETY: Constrain to same Soulseek username to prevent cross-album
# metadata contamination during mass downloads (e.g., two albums both
@@ -27428,35 +27428,35 @@ def _run_post_processing_worker(task_id, batch_id):
# Use the first similar key found
fuzzy_key = similar_keys[0]
context = matched_downloads_context.get(fuzzy_key)
- print(f"✅ [Post-Processing] Found context using fuzzy key matching: {fuzzy_key}")
+ print(f"[Post-Processing] Found context using fuzzy key matching: {fuzzy_key}")
# Generate expected final filename using the found context
try:
original_search = context.get("original_search_result", {})
- print(f"🔍 [Post-Processing] fuzzy context original_search keys: {list(original_search.keys())}")
+ print(f"[Post-Processing] fuzzy context original_search keys: {list(original_search.keys())}")
spotify_clean_title = original_search.get('spotify_clean_title')
track_number = original_search.get('track_number')
- print(f"🔍 [Post-Processing] fuzzy context spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}")
+ print(f"[Post-Processing] fuzzy context spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}")
if spotify_clean_title and track_number:
# Generate expected final filename that stream processor would create
# Pattern: f"{track_number:02d} - {clean_title}.flac"
sanitized_title = spotify_clean_title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
expected_final_filename = f"{track_number:02d} - {sanitized_title}.flac"
- print(f"🎯 [Post-Processing] Generated expected final filename from fuzzy match: {expected_final_filename}")
+ print(f"[Post-Processing] Generated expected final filename from fuzzy match: {expected_final_filename}")
else:
- print(f"❌ [Post-Processing] Missing required data from fuzzy match - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}")
+ print(f"[Post-Processing] Missing required data from fuzzy match - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}")
except Exception as e:
- print(f"⚠️ [Post-Processing] Error generating expected filename from fuzzy match: {e}")
+ print(f"[Post-Processing] Error generating expected filename from fuzzy match: {e}")
import traceback
traceback.print_exc()
else:
- print(f"🔍 [Post-Processing] No similar keys found containing '{task_basename}'")
+ print(f"[Post-Processing] No similar keys found containing '{task_basename}'")
# Show a sample of what keys actually exist for debugging
sample_keys = list(matched_downloads_context.keys())[:5]
- print(f"🔍 [Post-Processing] Sample of existing keys: {sample_keys}")
+ print(f"[Post-Processing] Sample of existing keys: {sample_keys}")
# RESILIENT FILE-FINDING LOOP: Try up to 3 times with delays
found_file = None
@@ -27465,7 +27465,7 @@ def _run_post_processing_worker(task_id, batch_id):
# CRITICAL FIX: For YouTube downloads, the filename in task is 'id||title' (metadata),
# but the actual file on disk is 'Title.mp3'. We must ask the client for the real path.
if (task.get('username') == 'youtube' or '||' in str(task_filename)) and not found_file:
- logger.info(f"🔧 [Post-Processing] Detected YouTube download task: {task_id}")
+ logger.info(f"[Post-Processing] Detected YouTube download task: {task_id}")
try:
# Query the download orchestrator for the status which contains the real file path
# CRITICAL FIX: Use the actual download_id designated by the client, not the internal task_id
@@ -27499,63 +27499,63 @@ def _run_post_processing_worker(task_id, batch_id):
# runs with CWD as project root, not download dir.
found_file = real_path
- logger.info(f"✅ [Post-Processing] Resolved actual YouTube filename: {found_file} (Location: {file_location})")
+ logger.info(f"[Post-Processing] Resolved actual YouTube filename: {found_file} (Location: {file_location})")
else:
- logger.warning(f"⚠️ [Post-Processing] YouTube status reported path but file missing: {real_path}")
+ logger.warning(f"[Post-Processing] YouTube status reported path but file missing: {real_path}")
else:
- logger.warning(f"⚠️ [Post-Processing] YouTube status returned no file_path for task {task_id}")
+ logger.warning(f"[Post-Processing] YouTube status returned no file_path for task {task_id}")
except Exception as e:
- logger.error(f"⚠️ [Post-Processing] Failed to retrieve YouTube task status: {e}")
+ logger.error(f"[Post-Processing] Failed to retrieve YouTube task status: {e}")
_file_search_max_retries = 5
for retry_count in range(_file_search_max_retries):
# If we already resolved the file (e.g. via YouTube status), skip searching
if found_file:
- print(f"🎯 [Post-Processing] Skipping search loop, file already resolved: {found_file}")
+ print(f"[Post-Processing] Skipping search loop, file already resolved: {found_file}")
break
# Check if stream processor already completed this task while we were waiting
with tasks_lock:
if task_id in download_tasks:
if download_tasks[task_id].get('stream_processed') or download_tasks[task_id]['status'] == 'completed':
- print(f"✅ [Post-Processing] Task {task_id} was completed by stream processor during file search - done")
+ print(f"[Post-Processing] Task {task_id} was completed by stream processor during file search - done")
return
- print(f"🔍 [Post-Processing] Attempt {retry_count + 1}/{_file_search_max_retries} to find file")
- print(f"🔍 [Post-Processing] Original filename: {task_basename}")
+ print(f"[Post-Processing] Attempt {retry_count + 1}/{_file_search_max_retries} to find file")
+ print(f"[Post-Processing] Original filename: {task_basename}")
if expected_final_filename:
- print(f"🔍 [Post-Processing] Expected final filename: {expected_final_filename}")
+ print(f"[Post-Processing] Expected final filename: {expected_final_filename}")
else:
- print(f"⚠️ [Post-Processing] No expected final filename available")
+ print(f"[Post-Processing] No expected final filename available")
# Strategy 1: Try with original filename in both downloads and transfer
- print(f"🔍 [Post-Processing] Strategy 1: Searching with original filename...")
+ print(f"[Post-Processing] Strategy 1: Searching with original filename...")
found_file, file_location = _find_completed_file_robust(download_dir, task_filename, transfer_dir)
if found_file:
- print(f"✅ [Post-Processing] Strategy 1 SUCCESS: Found file with original filename in {file_location}: {found_file}")
+ print(f"[Post-Processing] Strategy 1 SUCCESS: Found file with original filename in {file_location}: {found_file}")
else:
- print(f"❌ [Post-Processing] Strategy 1 FAILED: Original filename not found in either location")
+ print(f"[Post-Processing] Strategy 1 FAILED: Original filename not found in either location")
# Strategy 2: If not found and we have an expected final filename, try that in transfer folder
if not found_file and expected_final_filename:
- print(f"🔍 [Post-Processing] Strategy 2: Searching transfer folder with expected final filename...")
+ print(f"[Post-Processing] Strategy 2: Searching transfer folder with expected final filename...")
found_result = _find_completed_file_robust(transfer_dir, expected_final_filename)
if found_result and found_result[0]:
found_file, file_location = found_result[0], 'transfer'
- print(f"✅ [Post-Processing] Strategy 2 SUCCESS: Found file with expected final filename: {found_file}")
+ print(f"[Post-Processing] Strategy 2 SUCCESS: Found file with expected final filename: {found_file}")
else:
- print(f"❌ [Post-Processing] Strategy 2 FAILED: Expected final filename not found in transfer folder")
+ print(f"[Post-Processing] Strategy 2 FAILED: Expected final filename not found in transfer folder")
elif not expected_final_filename:
- print(f"⏭️ [Post-Processing] Strategy 2 SKIPPED: No expected final filename available")
+ print(f"[Post-Processing] Strategy 2 SKIPPED: No expected final filename available")
if found_file:
- print(f"🎯 [Post-Processing] FILE FOUND after {retry_count + 1} attempts in {file_location}: {found_file}")
+ print(f"[Post-Processing] FILE FOUND after {retry_count + 1} attempts in {file_location}: {found_file}")
break
else:
- print(f"❌ [Post-Processing] All search strategies failed on attempt {retry_count + 1}/{_file_search_max_retries}")
+ print(f"[Post-Processing] All search strategies failed on attempt {retry_count + 1}/{_file_search_max_retries}")
if retry_count < _file_search_max_retries - 1: # Don't sleep on final attempt
- print(f"⏳ [Post-Processing] Waiting 5 seconds before next attempt...")
+ print(f"[Post-Processing] Waiting 5 seconds before next attempt...")
time.sleep(5)
if not found_file:
@@ -27565,7 +27565,7 @@ def _run_post_processing_worker(task_id, batch_id):
with tasks_lock:
if task_id in download_tasks:
if download_tasks[task_id].get('stream_processed') or download_tasks[task_id]['status'] == 'completed':
- print(f"✅ [Post-Processing] Task {task_id} was completed by stream processor - not marking as failed")
+ print(f"[Post-Processing] Task {task_id} was completed by stream processor - not marking as failed")
return
download_tasks[task_id]['status'] = 'failed'
download_tasks[task_id]['error_message'] = f'File not found on disk after {_file_search_max_retries} search attempts. Expected: {os.path.basename(task_filename)}'
@@ -27574,7 +27574,7 @@ def _run_post_processing_worker(task_id, batch_id):
# Handle file found in transfer folder - already completed by stream processor
if file_location == 'transfer':
- print(f"🎯 [Post-Processing] File found in transfer folder - already completed by stream processor: {found_file}")
+ print(f"[Post-Processing] File found in transfer folder - already completed by stream processor: {found_file}")
# Check if metadata enhancement was completed
metadata_enhanced = False
@@ -27583,7 +27583,7 @@ def _run_post_processing_worker(task_id, batch_id):
metadata_enhanced = download_tasks[task_id].get('metadata_enhanced', False)
if not metadata_enhanced:
- print(f"⚠️ [Post-Processing] File in transfer folder missing metadata enhancement - completing now")
+ print(f"[Post-Processing] File in transfer folder missing metadata enhancement - completing now")
# Attempt to complete metadata enhancement using context
if context and expected_final_filename:
try:
@@ -27602,13 +27602,13 @@ def _run_post_processing_worker(task_id, batch_id):
# If no track number in context, extract from filename
if track_number == 1 and found_file:
- print(f"⚠️ [Verification] No track_number in context, extracting from filename: {os.path.basename(found_file)}")
+ print(f"[Verification] No track_number in context, extracting from filename: {os.path.basename(found_file)}")
track_number = _extract_track_number_from_filename(found_file)
print(f" -> Extracted track number: {track_number}")
# Ensure track_number is valid
if not isinstance(track_number, int) or track_number < 1:
- print(f"⚠️ [Verification] Invalid track number ({track_number}), defaulting to 1")
+ print(f"[Verification] Invalid track number ({track_number}), defaulting to 1")
track_number = 1
# Get clean track name
@@ -27641,34 +27641,34 @@ def _run_post_processing_worker(task_id, batch_id):
consistent_album_name = _resolve_album_group(spotify_artist, album_info, original_album_ctx)
album_info['album_name'] = consistent_album_name
except Exception as group_err:
- print(f"⚠️ [Verification] Album grouping failed, using raw name: {group_err}")
+ print(f"[Verification] Album grouping failed, using raw name: {group_err}")
else:
- print(f"🎯 [Verification] Explicit album download - preserving Spotify album name: '{album_info['album_name']}'")
+ print(f"[Verification] Explicit album download - preserving Spotify album name: '{album_info['album_name']}'")
- print(f"🎯 [Verification] Created proper album_info - track_number: {track_number}, album: {album_info['album_name']}")
+ print(f"[Verification] Created proper album_info - track_number: {track_number}, album: {album_info['album_name']}")
- print(f"🎵 [Post-Processing] Attempting metadata enhancement for: {found_file}")
- print(f"🔍 [Metadata Input] Verification worker - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
- print(f"🔍 [Metadata Input] Verification worker - album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}")
+ print(f"[Post-Processing] Attempting metadata enhancement for: {found_file}")
+ print(f"[Metadata Input] Verification worker - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
+ print(f"[Metadata Input] Verification worker - album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}")
enhancement_success = _enhance_file_metadata(found_file, context, spotify_artist, album_info)
if enhancement_success:
with tasks_lock:
if task_id in download_tasks:
download_tasks[task_id]['metadata_enhanced'] = True
- print(f"✅ [Post-Processing] Successfully completed metadata enhancement for: {os.path.basename(found_file)}")
+ print(f"[Post-Processing] Successfully completed metadata enhancement for: {os.path.basename(found_file)}")
else:
- print(f"⚠️ [Post-Processing] Metadata enhancement returned False for: {os.path.basename(found_file)}")
+ print(f"[Post-Processing] Metadata enhancement returned False for: {os.path.basename(found_file)}")
else:
- print(f"⚠️ [Post-Processing] Missing spotify_artist or spotify_album in context")
- print(f"⚠️ [Post-Processing] spotify_artist: {spotify_artist is not None}, spotify_album: {spotify_album is not None}")
+ print(f"[Post-Processing] Missing spotify_artist or spotify_album in context")
+ print(f"[Post-Processing] spotify_artist: {spotify_artist is not None}, spotify_album: {spotify_album is not None}")
except Exception as enhancement_error:
import traceback
- print(f"❌ [Post-Processing] Error during metadata enhancement: {enhancement_error}\n{traceback.format_exc()}")
+ print(f"[Post-Processing] Error during metadata enhancement: {enhancement_error}\n{traceback.format_exc()}")
else:
- print(f"⚠️ [Post-Processing] Cannot complete metadata enhancement - missing context or expected filename")
+ print(f"[Post-Processing] Cannot complete metadata enhancement - missing context or expected filename")
else:
- print(f"✅ [Post-Processing] File already has metadata enhancement completed")
+ print(f"[Post-Processing] File already has metadata enhancement completed")
with tasks_lock:
if task_id in download_tasks:
@@ -27679,7 +27679,7 @@ def _run_post_processing_worker(task_id, batch_id):
with matched_context_lock:
if context_key in matched_downloads_context:
del matched_downloads_context[context_key]
- print(f"🗑️ [Verification] Cleaned up context after successful verification: {context_key}")
+ print(f"[Verification] Cleaned up context after successful verification: {context_key}")
_on_download_completed(batch_id, task_id, success=True)
return
@@ -27694,12 +27694,12 @@ def _run_post_processing_worker(task_id, batch_id):
context = matched_downloads_context.get(context_key)
if context:
- print(f"🎯 [Post-Processing] Found matched context, running full post-processing for: {context_key}")
+ print(f"[Post-Processing] Found matched context, running full post-processing for: {context_key}")
# Run the existing post-processing logic with verification
_post_process_matched_download_with_verification(context_key, context, found_file, task_id, batch_id)
else:
# No matched context - just mark as completed since file exists
- print(f"📁 [Post-Processing] No matched context, marking as completed: {os.path.basename(found_file)}")
+ print(f"[Post-Processing] No matched context, marking as completed: {os.path.basename(found_file)}")
with tasks_lock:
if task_id in download_tasks:
track_info = download_tasks[task_id].get('track_info')
@@ -27709,13 +27709,13 @@ def _run_post_processing_worker(task_id, batch_id):
with matched_context_lock:
if context_key in matched_downloads_context:
del matched_downloads_context[context_key]
- print(f"🗑️ [Verification] Cleaned up leftover context: {context_key}")
+ print(f"[Verification] Cleaned up leftover context: {context_key}")
# Call completion callback since there's no other post-processing to handle it
_on_download_completed(batch_id, task_id, success=True)
except Exception as processing_error:
- print(f"❌ [Post-Processing] Processing failed for task {task_id}: {processing_error}")
+ print(f"[Post-Processing] Processing failed for task {task_id}: {processing_error}")
with tasks_lock:
if task_id in download_tasks:
download_tasks[task_id]['status'] = 'failed'
@@ -27723,7 +27723,7 @@ def _run_post_processing_worker(task_id, batch_id):
_on_download_completed(batch_id, task_id, success=False)
except Exception as e:
- print(f"❌ [Post-Processing] Critical error in post-processing worker for task {task_id}: {e}")
+ print(f"[Post-Processing] Critical error in post-processing worker for task {task_id}: {e}")
with tasks_lock:
if task_id in download_tasks:
download_tasks[task_id]['status'] = 'failed'
@@ -27740,33 +27740,33 @@ def _download_track_worker(task_id, batch_id=None):
# Retrieve task details from global state
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Modal Worker] Task {task_id} not found in download_tasks")
+ print(f"[Modal Worker] Task {task_id} not found in download_tasks")
return
task = download_tasks[task_id].copy()
# Cancellation Checkpoint 1: Before doing anything
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Modal Worker] Task {task_id} was deleted before starting")
+ print(f"[Modal Worker] Task {task_id} was deleted before starting")
return
if download_tasks[task_id]['status'] == 'cancelled':
- print(f"❌ [Modal Worker] Task {task_id} cancelled before starting")
+ print(f"[Modal Worker] Task {task_id} cancelled before starting")
# V2 FIX: Don't call _on_download_completed for cancelled V2 tasks
# V2 system handles worker slot freeing in atomic cancel function
task_playlist_id = download_tasks[task_id].get('playlist_id')
if task_playlist_id:
- print(f"⏭️ [Modal Worker] V2 task {task_id} cancelled - worker slot already freed by V2 system")
+ print(f"[Modal Worker] V2 task {task_id} cancelled - worker slot already freed by V2 system")
return # V2 system already handled worker slot management
elif batch_id:
# Legacy system - use old completion callback
- print(f"⏭️ [Modal Worker] Legacy task {task_id} cancelled - using legacy completion callback")
+ print(f"[Modal Worker] Legacy task {task_id} cancelled - using legacy completion callback")
_on_download_completed(batch_id, task_id, success=False)
return
track_data = task['track_info']
track_name = track_data.get('name', 'Unknown Track')
- print(f"🎯 [Modal Worker] Task {task_id} starting search for track: '{track_name}'")
+ print(f"[Modal Worker] Task {task_id} starting search for track: '{track_name}'")
# Recreate a SpotifyTrack object for the matching engine
# Handle both string format and Spotify API format for artists
@@ -27797,7 +27797,7 @@ def _download_track_worker(task_id, batch_id=None):
duration_ms=track_data.get('duration_ms', 0),
popularity=track_data.get('popularity', 0)
)
- print(f"📥 [Modal Worker] Starting download task for: {track.name} by {track.artists[0] if track.artists else 'Unknown'}")
+ print(f"[Modal Worker] Starting download task for: {track.name} by {track.artists[0] if track.artists else 'Unknown'}")
# === SOURCE REUSE: Check batch's last good source before searching ===
if _try_source_reuse(task_id, batch_id, track):
@@ -27871,8 +27871,8 @@ def _download_track_worker(task_id, batch_id=None):
seen.add(query.lower())
search_queries = unique_queries
- print(f"🔍 [Modal Worker] Generated {len(search_queries)} smart search queries for '{track.name}': {search_queries}")
- print(f"🔍 [Modal Worker] About to start search loop for task {task_id} (track: '{track.name}')")
+ print(f"[Modal Worker] Generated {len(search_queries)} smart search queries for '{track.name}': {search_queries}")
+ print(f"[Modal Worker] About to start search loop for task {task_id} (track: '{track.name}')")
# 2. Sequential Query Search (matches GUI's start_search_worker_parallel logic)
search_diagnostics = [] # Track what happened per query for detailed error messages
@@ -27881,29 +27881,29 @@ def _download_track_worker(task_id, batch_id=None):
# Cancellation check before each query
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Modal Worker] Task {task_id} was deleted during query {query_index + 1}")
+ print(f"[Modal Worker] Task {task_id} was deleted during query {query_index + 1}")
return
if download_tasks[task_id]['status'] == 'cancelled':
- print(f"❌ [Modal Worker] Task {task_id} cancelled during query {query_index + 1}")
+ print(f"[Modal Worker] Task {task_id} cancelled during query {query_index + 1}")
# Don't call _on_download_completed for cancelled tasks as it can stop monitoring
return
download_tasks[task_id]['current_query_index'] = query_index
- print(f"🔍 [Modal Worker] Query {query_index + 1}/{len(search_queries)}: '{query}'")
- print(f"🔍 [DEBUG] About to call soulseek search for task {task_id}")
+ print(f"[Modal Worker] Query {query_index + 1}/{len(search_queries)}: '{query}'")
+ print(f"[DEBUG] About to call soulseek search for task {task_id}")
try:
# Perform search with timeout
tracks_result, _ = run_async(soulseek_client.search(query, timeout=30))
- print(f"🔍 [DEBUG] Search completed for task {task_id}, got {len(tracks_result) if tracks_result else 0} results")
+ print(f"[DEBUG] Search completed for task {task_id}, got {len(tracks_result) if tracks_result else 0} results")
# CRITICAL: Check cancellation immediately after search returns
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Modal Worker] Task {task_id} was deleted after search returned")
+ print(f"[Modal Worker] Task {task_id} was deleted after search returned")
return
if download_tasks[task_id]['status'] == 'cancelled':
- print(f"❌ [Modal Worker] Task {task_id} cancelled after search returned - ignoring results")
+ print(f"[Modal Worker] Task {task_id} cancelled after search returned - ignoring results")
# Don't call _on_download_completed for cancelled tasks as it can stop monitoring
# The cancellation endpoint already handles batch management properly
return
@@ -27913,15 +27913,15 @@ def _download_track_worker(task_id, batch_id=None):
# Validate candidates using GUI's get_valid_candidates logic
candidates = get_valid_candidates(tracks_result, track, query)
if candidates:
- print(f"✅ [Modal Worker] Found {len(candidates)} valid candidates for query '{query}'")
+ print(f"[Modal Worker] Found {len(candidates)} valid candidates for query '{query}'")
# CRITICAL: Check cancellation before processing candidates
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Modal Worker] Task {task_id} was deleted before processing candidates")
+ print(f"[Modal Worker] Task {task_id} was deleted before processing candidates")
return
if download_tasks[task_id]['status'] == 'cancelled':
- print(f"❌ [Modal Worker] Task {task_id} cancelled before processing candidates")
+ print(f"[Modal Worker] Task {task_id} cancelled before processing candidates")
# Don't call _on_download_completed for cancelled tasks as it can stop monitoring
return
# Store candidates for retry fallback (like GUI)
@@ -27932,7 +27932,7 @@ def _download_track_worker(task_id, batch_id=None):
if success:
# Download initiated successfully - let the download monitoring system handle completion
if batch_id:
- print(f"✅ [Modal Worker] Download initiated successfully for task {task_id} - monitoring will handle completion")
+ print(f"[Modal Worker] Download initiated successfully for task {task_id} - monitoring will handle completion")
# Store this source for batch reuse
with tasks_lock:
used_filename = download_tasks.get(task_id, {}).get('filename')
@@ -27949,7 +27949,7 @@ def _download_track_worker(task_id, batch_id=None):
search_diagnostics.append(f'"{query}": no results found')
except Exception as e:
- print(f"⚠️ [Modal Worker] Search failed for query '{query}': {e}")
+ print(f"[Modal Worker] Search failed for query '{query}': {e}")
search_diagnostics.append(f'"{query}": search error — {e}')
continue
@@ -27981,7 +27981,7 @@ def _download_track_worker(task_id, batch_id=None):
# harmless (streaming sources return fast).
remaining_sources = [s for s in hybrid_order[1:] if s in source_clients and source_clients[s]]
if remaining_sources:
- print(f"🔄 [Hybrid Fallback] Primary source had no valid matches. Trying fallback sources: {remaining_sources}")
+ print(f"[Hybrid Fallback] Primary source had no valid matches. Trying fallback sources: {remaining_sources}")
for fallback_source in remaining_sources:
fb_client = source_clients[fallback_source]
@@ -27991,27 +27991,27 @@ def _download_track_worker(task_id, batch_id=None):
# Use first 2 queries only for speed
for fb_query in search_queries[:2]:
try:
- print(f"🔄 [Hybrid Fallback] Trying {fallback_source}: '{fb_query}'")
+ print(f"[Hybrid Fallback] Trying {fallback_source}: '{fb_query}'")
fb_results, _ = run_async(fb_client.search(fb_query, timeout=20))
if not fb_results:
continue
fb_candidates = get_valid_candidates(fb_results, track, fb_query)
if fb_candidates:
- print(f"✅ [Hybrid Fallback] {fallback_source} found {len(fb_candidates)} valid candidates!")
+ print(f"[Hybrid Fallback] {fallback_source} found {len(fb_candidates)} valid candidates!")
success = _attempt_download_with_candidates(task_id, fb_candidates, track, batch_id)
if success:
return
except Exception as e:
- print(f"⚠️ [Hybrid Fallback] {fallback_source} search failed: {e}")
+ print(f"[Hybrid Fallback] {fallback_source} search failed: {e}")
continue
- print(f"⏭️ [Hybrid Fallback] {fallback_source} returned no valid candidates")
+ print(f"[Hybrid Fallback] {fallback_source} returned no valid candidates")
except Exception as e:
- print(f"⚠️ [Hybrid Fallback] Error in fallback logic: {e}")
+ print(f"[Hybrid Fallback] Error in fallback logic: {e}")
# If we get here, all search queries and hybrid fallbacks failed
- print(f"❌ [Modal Worker] No valid candidates found for '{track.name}' after trying all {len(search_queries)} queries.")
+ print(f"[Modal Worker] No valid candidates found for '{track.name}' after trying all {len(search_queries)} queries.")
with tasks_lock:
if task_id in download_tasks:
download_tasks[task_id]['status'] = 'not_found'
@@ -28026,12 +28026,12 @@ def _download_track_worker(task_id, batch_id=None):
try:
_on_download_completed(batch_id, task_id, success=False)
except Exception as completion_error:
- print(f"❌ Error in batch completion callback for {task_id}: {completion_error}")
+ print(f"Error in batch completion callback for {task_id}: {completion_error}")
except Exception as e:
import traceback
track_name_safe = locals().get('track_name', 'unknown') # Safe fallback for track_name
- print(f"❌ CRITICAL ERROR in download task for '{track_name_safe}' (task_id: {task_id}): {e}")
+ print(f"CRITICAL ERROR in download task for '{track_name_safe}' (task_id: {task_id}): {e}")
traceback.print_exc()
# Update task status safely with timeout
@@ -28042,27 +28042,27 @@ def _download_track_worker(task_id, batch_id=None):
if task_id in download_tasks:
download_tasks[task_id]['status'] = 'failed'
download_tasks[task_id]['error_message'] = f'Unexpected error during download: {type(e).__name__}: {e}'
- print(f"🔧 [Exception Recovery] Set task {task_id} status to 'failed'")
+ print(f"[Exception Recovery] Set task {task_id} status to 'failed'")
finally:
tasks_lock.release()
else:
- print(f"⚠️ [Exception Recovery] Could not acquire lock to update task {task_id} status")
+ print(f"[Exception Recovery] Could not acquire lock to update task {task_id} status")
except Exception as status_error:
- print(f"❌ Error updating task status in exception handler: {status_error}")
+ print(f"Error updating task status in exception handler: {status_error}")
# Notify batch manager that this task completed (failed) - THREAD SAFE with RECOVERY
if batch_id:
try:
_on_download_completed(batch_id, task_id, success=False)
- print(f"✅ [Exception Recovery] Successfully freed worker slot for task {task_id}")
+ print(f"[Exception Recovery] Successfully freed worker slot for task {task_id}")
except Exception as completion_error:
- print(f"❌ [Exception Recovery] Error in batch completion callback for {task_id}: {completion_error}")
+ print(f"[Exception Recovery] Error in batch completion callback for {task_id}: {completion_error}")
# CRITICAL: If batch completion fails, we need to manually recover the worker slot
try:
- print(f"🚨 [Exception Recovery] Attempting manual worker slot recovery for batch {batch_id}")
+ print(f"[Exception Recovery] Attempting manual worker slot recovery for batch {batch_id}")
_recover_worker_slot(batch_id, task_id)
except Exception as recovery_error:
- print(f"💀 [Exception Recovery] FATAL: Could not recover worker slot: {recovery_error}")
+ print(f"[Exception Recovery] FATAL: Could not recover worker slot: {recovery_error}")
def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None):
"""
@@ -28083,10 +28083,10 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
# Check cancellation before each attempt
with tasks_lock:
if task_id not in download_tasks:
- print(f"❌ [Modal Worker] Task {task_id} was deleted during candidate {candidate_index + 1}")
+ print(f"[Modal Worker] Task {task_id} was deleted during candidate {candidate_index + 1}")
return False
if download_tasks[task_id]['status'] == 'cancelled':
- print(f"❌ [Modal Worker] Task {task_id} cancelled during candidate {candidate_index + 1}")
+ print(f"[Modal Worker] Task {task_id} cancelled during candidate {candidate_index + 1}")
# Don't call _on_download_completed for cancelled tasks as it can stop monitoring
return False
download_tasks[task_id]['current_candidate_index'] = candidate_index
@@ -28094,14 +28094,14 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
# Create source key to avoid duplicate attempts (like GUI)
source_key = f"{candidate.username}_{candidate.filename}"
if source_key in used_sources:
- print(f"⏭️ [Modal Worker] Skipping already tried source: {source_key}")
+ print(f"[Modal Worker] Skipping already tried source: {source_key}")
continue
# Blacklist check — skip sources the user has flagged as bad matches
try:
_bl_db = get_database()
if _bl_db.is_blacklisted(candidate.username, candidate.filename):
- print(f"⛔ [Modal Worker] Skipping blacklisted source: {source_key}")
+ print(f"[Modal Worker] Skipping blacklisted source: {source_key}")
continue
except Exception:
pass
@@ -28111,9 +28111,9 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
with tasks_lock:
if task_id in download_tasks:
download_tasks[task_id]['used_sources'].add(source_key)
- print(f"🚫 [Modal Worker] Marked source as used before download attempt: {source_key}")
+ print(f"[Modal Worker] Marked source as used before download attempt: {source_key}")
- print(f"🎯 [Modal Worker] Trying candidate {candidate_index + 1}/{len(candidates)}: {candidate.filename} (Confidence: {candidate.confidence:.2f})")
+ print(f"[Modal Worker] Trying candidate {candidate_index + 1}/{len(candidates)}: {candidate.filename} (Confidence: {candidate.confidence:.2f})")
try:
# Update task status to downloading
@@ -28161,7 +28161,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
'album_type': explicit_album.get('album_type', 'album'),
'artists': explicit_album.get('artists', [{'name': spotify_artist_context.get('name', '')}])
}
- print(f"🎵 [Explicit Context] Using real album data: '{spotify_album_context['name']}' ({spotify_album_context['album_type']}, {spotify_album_context['total_discs']} disc(s))")
+ print(f"[Explicit Context] Using real album data: '{spotify_album_context['name']}' ({spotify_album_context['album_type']}, {spotify_album_context['total_discs']} disc(s))")
else:
# Fallback to generic context for playlists/wishlists
# Extract album metadata from track_info if available (discovery enriches tracks with full album objects)
@@ -28199,7 +28199,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
size = download_payload.get('size', 0)
if not username or not filename:
- print(f"❌ [Modal Worker] Invalid candidate data: missing username or filename")
+ print(f"[Modal Worker] Invalid candidate data: missing username or filename")
continue
# PROTECTION: Check if there's already an active download for this task
@@ -28209,12 +28209,12 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
current_download_id = download_tasks[task_id].get('download_id')
if current_download_id:
- print(f"⚠️ [Modal Worker] Task {task_id} already has active download {current_download_id} - skipping new download attempt")
- print(f"🔄 [Modal Worker] This prevents race condition where multiple retries start overlapping downloads")
+ print(f"[Modal Worker] Task {task_id} already has active download {current_download_id} - skipping new download attempt")
+ print(f"[Modal Worker] This prevents race condition where multiple retries start overlapping downloads")
continue
# Initiate download
- print(f"🚀 [Modal Worker] Starting download: {username} / {os.path.basename(filename)}")
+ print(f"[Modal Worker] Starting download: {username} / {os.path.basename(filename)}")
download_id = run_async(soulseek_client.download(username, filename, size))
if download_id:
@@ -28233,7 +28233,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
enhanced_payload['spotify_clean_artist'] = track.artists[0] if track.artists else enhanced_payload.get('artist', '')
# Preserve all artists for metadata tagging
enhanced_payload['artists'] = [{'name': artist} for artist in track.artists] if track.artists else []
- print(f"✨ [Context] Using clean Spotify metadata - Album: '{track.album}', Title: '{track.name}'")
+ print(f"[Context] Using clean Spotify metadata - Album: '{track.album}', Title: '{track.name}'")
# Get track_number and disc_number — prefer track data we already have,
# fall back to detailed API call only if needed
@@ -28246,14 +28246,14 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
enhanced_payload['track_number'] = tn
enhanced_payload['disc_number'] = dn
got_track_number = True
- print(f"🔢 [Context] Added track_number from track_info: {tn}, disc_number: {dn}")
+ print(f"[Context] Added track_number from track_info: {tn}, disc_number: {dn}")
# 2. Try the track object itself (from album tracks response)
if not got_track_number and hasattr(track, 'track_number') and track.track_number:
enhanced_payload['track_number'] = track.track_number
enhanced_payload['disc_number'] = getattr(track, 'disc_number', 1) or 1
got_track_number = True
- print(f"🔢 [Context] Added track_number from track object: {track.track_number}, disc_number: {enhanced_payload['disc_number']}")
+ print(f"[Context] Added track_number from track object: {track.track_number}, disc_number: {enhanced_payload['disc_number']}")
# 3. Last resort — fetch from metadata source API
if not got_track_number and hasattr(track, 'id') and track.id:
@@ -28263,7 +28263,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
enhanced_payload['track_number'] = detailed_track['track_number']
enhanced_payload['disc_number'] = detailed_track.get('disc_number', 1)
got_track_number = True
- print(f"🔢 [Context] Added track_number from API: {detailed_track['track_number']}, disc_number: {enhanced_payload['disc_number']}")
+ print(f"[Context] Added track_number from API: {detailed_track['track_number']}, disc_number: {enhanced_payload['disc_number']}")
# Backfill album metadata from detailed track when context
# has incomplete data (missing release_date, total_tracks, etc.)
@@ -28271,7 +28271,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
dt_album = detailed_track['album']
if not spotify_album_context.get('release_date') and dt_album.get('release_date'):
spotify_album_context['release_date'] = dt_album['release_date']
- print(f"📅 [Context] Backfilled release_date from API: {dt_album['release_date']}")
+ print(f"[Context] Backfilled release_date from API: {dt_album['release_date']}")
if not spotify_album_context.get('album_type') and dt_album.get('album_type'):
spotify_album_context['album_type'] = dt_album['album_type']
if not spotify_album_context.get('total_tracks') and dt_album.get('total_tracks'):
@@ -28281,18 +28281,18 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
if not spotify_album_context.get('image_url') and dt_album.get('images'):
spotify_album_context['image_url'] = dt_album['images'][0].get('url', '')
except Exception as e:
- print(f"⚠️ [Context] API track details failed: {e}")
+ print(f"[Context] API track details failed: {e}")
if not got_track_number:
enhanced_payload.setdefault('track_number', 0)
enhanced_payload.setdefault('disc_number', 1)
- print(f"⚠️ [Context] No track_number found from any source")
+ print(f"[Context] No track_number found from any source")
# Determine if this should be treated as album download
# First check if we have explicit album context from artist page
if has_explicit_context:
is_album_context = True
- print(f"✅ [Context] Using explicit album context flag from artist page")
+ print(f"[Context] Using explicit album context flag from artist page")
else:
# Fall back to guessing based on clean data
is_album_context = (
@@ -28311,7 +28311,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
enhanced_payload['artists'] = [{'name': enhanced_payload['artist']}]
enhanced_payload['track_number'] = track_info.get('track_number', 1) # Fallback when no clean Spotify data
is_album_context = False
- print(f"⚠️ [Context] Using fallback data - no clean Spotify metadata available, track_number={enhanced_payload['track_number']}")
+ print(f"[Context] Using fallback data - no clean Spotify metadata available, track_number={enhanced_payload['track_number']}")
matched_downloads_context[context_key] = {
"spotify_artist": spotify_artist_context,
@@ -28325,21 +28325,21 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
"_download_username": username, # Source username for AcoustID skip logic
}
- print(f"🎯 [Context] Set is_album_download: {is_album_context} (has clean data: {has_clean_spotify_data})")
- print(f"🔍 [Debug] Context creation - track_info: {track_info is not None}, playlist_folder_mode: {track_info.get('_playlist_folder_mode', False) if track_info else False}")
+ print(f"[Context] Set is_album_download: {is_album_context} (has clean data: {has_clean_spotify_data})")
+ print(f"[Debug] Context creation - track_info: {track_info is not None}, playlist_folder_mode: {track_info.get('_playlist_folder_mode', False) if track_info else False}")
# Update task with successful download info
with tasks_lock:
if task_id in download_tasks:
# PHASE 3: Final cancellation check after download started (GUI PARITY)
if download_tasks[task_id]['status'] == 'cancelled':
- print(f"🚫 [Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download")
+ print(f"[Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download")
# Try to cancel the download immediately
try:
run_async(soulseek_client.cancel_download(download_id, username, remove=True))
- print(f"✅ Successfully cancelled active download {download_id}")
+ print(f"Successfully cancelled active download {download_id}")
except Exception as cancel_error:
- print(f"⚠️ Warning: Failed to cancel active download {download_id}: {cancel_error}")
+ print(f"Warning: Failed to cancel active download {download_id}: {cancel_error}")
# Free worker slot
if batch_id:
@@ -28353,10 +28353,10 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
download_tasks[task_id]['username'] = username
download_tasks[task_id]['filename'] = filename
- print(f"✅ [Modal Worker] Download started successfully for '{filename}'. Download ID: {download_id}")
+ print(f"[Modal Worker] Download started successfully for '{filename}'. Download ID: {download_id}")
return True # Success!
else:
- print(f"❌ [Modal Worker] Failed to start download for '{filename}'")
+ print(f"[Modal Worker] Failed to start download for '{filename}'")
# Reset status back to searching for next attempt
with tasks_lock:
if task_id in download_tasks:
@@ -28365,7 +28365,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
except Exception as e:
import traceback
- print(f"❌ [Modal Worker] Error attempting download for '{candidate.filename}': {e}")
+ print(f"[Modal Worker] Error attempting download for '{candidate.filename}': {e}")
traceback.print_exc()
# Reset status back to searching for next attempt
with tasks_lock:
@@ -28374,7 +28374,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
continue
# All candidates failed
- print(f"❌ [Modal Worker] All {len(candidates)} candidates failed for '{track.name}'")
+ print(f"[Modal Worker] All {len(candidates)} candidates failed for '{track.name}'")
return False
# ── Staging folder match cache (per-batch, avoids re-scanning for every track) ──
@@ -28415,7 +28415,7 @@ def _get_staging_file_cache(batch_id):
'extension': ext,
})
- print(f"📂 [Staging] Scanned {len(files)} audio files in staging folder")
+ print(f"[Staging] Scanned {len(files)} audio files in staging folder")
with _staging_cache_lock:
_staging_cache[batch_id] = files
return files
@@ -28483,7 +28483,7 @@ def _try_staging_match(task_id, batch_id, track):
if not best_match or best_score < 0.75:
return False
- print(f"📂 [Staging] Match found for '{track_title}' by '{track_artist}': "
+ print(f"[Staging] Match found for '{track_title}' by '{track_artist}': "
f"{os.path.basename(best_match['full_path'])} (score: {best_score:.2f})")
# Copy the file to the transfer folder
@@ -28500,7 +28500,7 @@ def _try_staging_match(task_id, batch_id, track):
import shutil
shutil.copy2(best_match['full_path'], dest_path)
- print(f"📂 [Staging] Copied to transfer: {dest_path}")
+ print(f"[Staging] Copied to transfer: {dest_path}")
# Mark task as completed with staging context
with tasks_lock:
@@ -28533,7 +28533,7 @@ def _try_staging_match(task_id, batch_id, track):
return True
except Exception as e:
- print(f"❌ [Staging] Failed to use staging file: {e}")
+ print(f"[Staging] Failed to use staging file: {e}")
return False
@@ -28749,13 +28749,13 @@ def start_playlist_missing_downloads(playlist_id):
missing_tracks = [t for t in missing_tracks if not _is_explicit_blocked(t.get('track', t))]
skipped = before_count - len(missing_tracks)
if skipped > 0:
- print(f"🚫 [Content Filter] Filtered out {skipped} explicit track(s) from playlist download")
+ print(f"[Content Filter] Filtered out {skipped} explicit track(s) from playlist download")
if not missing_tracks:
return jsonify({"success": False, "error": "All tracks were filtered by explicit content setting"}), 400
# Add activity for playlist download missing start
playlist_name = data.get('playlist_name', f'Playlist {playlist_id}')
- add_activity_item("📥", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now")
+ add_activity_item("", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now")
try:
batch_id = str(uuid.uuid4())
@@ -28810,7 +28810,7 @@ def start_playlist_missing_downloads(playlist_id):
return jsonify({"success": True, "batch_id": batch_id, "message": f"Queued {len(missing_tracks)} downloads for processing."})
except Exception as e:
- print(f"❌ Error starting missing downloads: {e}")
+ print(f"Error starting missing downloads: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/active-processes', methods=['GET'])
@@ -28868,7 +28868,7 @@ def get_active_processes():
"download_process_id": state.get('download_process_id') # batch_id for download modal rehydration
})
- print(f"📊 Active processes check: {len([p for p in active_processes if p['type'] == 'batch'])} download batches, {len([p for p in active_processes if p['type'] == 'youtube_playlist'])} YouTube playlists")
+ print(f"Active processes check: {len([p for p in active_processes if p['type'] == 'batch'])} download batches, {len([p for p in active_processes if p['type'] == 'youtube_playlist'])} YouTube playlists")
return jsonify({"active_processes": active_processes})
def _build_batch_status_data(batch_id, batch, live_transfers_lookup):
@@ -28918,13 +28918,13 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup):
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
found_file, file_location = _find_completed_file_robust(download_dir, task_filename, transfer_dir)
if found_file:
- print(f"✅ [Safety Valve] Task {task_id} stuck but file found in {file_location} — routing to post-processing")
+ print(f"[Safety Valve] Task {task_id} stuck but file found in {file_location} — routing to post-processing")
task['status'] = 'post_processing'
task['status_change_time'] = current_time
missing_download_executor.submit(_run_post_processing_worker, task_id, batch_id)
recovered = True
except Exception as e:
- print(f"⚠️ [Safety Valve] Error checking for completed file: {e}")
+ print(f"[Safety Valve] Error checking for completed file: {e}")
if not recovered:
if stuck_state == 'searching':
@@ -28970,7 +28970,7 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup):
elif 'Failed' in state_str or 'Errored' in state_str or 'Rejected' in state_str or 'TimedOut' in state_str:
# UNIFIED ERROR HANDLING: Let monitor handle errors for consistency
# Monitor will detect errored state and trigger retry within 5 seconds
- print(f"🔍 Task {task_id} API shows error state: {state_str} - letting monitor handle retry")
+ print(f"Task {task_id} API shows error state: {state_str} - letting monitor handle retry")
# Keep task in current status (downloading/queued) so monitor can detect error
# Don't mark as failed here - let the unified retry system handle it
@@ -28986,13 +28986,13 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup):
if expected_size > 0 and transferred < expected_size:
# State says complete but bytes don't match — keep current status
task_status['status'] = task['status']
- print(f"⚠️ Task {task_id} state says complete but bytes incomplete ({transferred}/{expected_size})")
+ print(f"Task {task_id} state says complete but bytes incomplete ({transferred}/{expected_size})")
# NEW VERIFICATION WORKFLOW: Use intermediate post_processing status
# Only set this status once to prevent multiple worker submissions
elif task['status'] != 'post_processing':
task_status['status'] = 'post_processing'
task['status'] = 'post_processing'
- print(f"🔄 Task {task_id} API reports 'Succeeded' - starting post-processing verification")
+ print(f"Task {task_id} API reports 'Succeeded' - starting post-processing verification")
# Submit post-processing worker to verify file and complete the task
missing_download_executor.submit(_run_post_processing_worker, task_id, batch_id)
@@ -29000,7 +29000,7 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup):
# FIXED: Always require verification workflow - no bypass for stream processed tasks
# Stream processing only handles metadata, not file verification
task_status['status'] = 'post_processing'
- print(f"🔄 Task {task_id} waiting for verification worker to complete")
+ print(f"Task {task_id} waiting for verification worker to complete")
elif 'InProgress' in state_str:
task_status['status'] = 'downloading'
else:
@@ -29094,7 +29094,7 @@ def get_batched_download_statuses():
)
except Exception as batch_error:
# Don't fail entire request if one batch has issues
- print(f"❌ Error processing batch {batch_id}: {batch_error}")
+ print(f"Error processing batch {batch_id}: {batch_error}")
response["batches"][batch_id] = {"error": str(batch_error)}
# Add metadata for debugging/monitoring
@@ -29123,12 +29123,12 @@ def get_batched_download_statuses():
response["debug_info"] = debug_info
- print(f"📊 [Batched Status] Returning status for {len(response['batches'])} batches")
+ print(f"[Batched Status] Returning status for {len(response['batches'])} batches")
# Log worker discrepancies for debugging
discrepancies = [bid for bid, info in debug_info.items() if info.get("worker_discrepancy")]
if discrepancies:
- print(f"⚠️ [Batched Status] Worker count discrepancies in batches: {discrepancies}")
+ print(f"[Batched Status] Worker count discrepancies in batches: {discrepancies}")
return jsonify(response)
@@ -29160,7 +29160,7 @@ def cancel_download_task():
current_status = task.get('status', 'unknown')
download_id = task.get('download_id')
username = task.get('username')
- print(f"🔍 [Cancel Debug] Task {task_id} - Current status: '{current_status}', download_id: {download_id}, username: {username}")
+ print(f"[Cancel Debug] Task {task_id} - Current status: '{current_status}', download_id: {download_id}, username: {username}")
# Immediately mark as cancelled to prevent race conditions
task['status'] = 'cancelled'
@@ -29180,8 +29180,8 @@ def cancel_download_task():
# Free worker slot if there are active workers and task was actively running
# This is more reliable than checking task status which can be inconsistent
if active_count > 0 and current_status in ['pending', 'searching', 'downloading', 'queued']:
- print(f"🔄 [Cancel] Task {task_id} (status: {current_status}) - freeing worker slot for batch {batch_id}")
- print(f"🔄 [Cancel] Active count before: {active_count}")
+ print(f"[Cancel] Task {task_id} (status: {current_status}) - freeing worker slot for batch {batch_id}")
+ print(f"[Cancel] Active count before: {active_count}")
# Use the completion callback with error handling
_on_download_completed(batch_id, task_id, success=False)
@@ -29189,35 +29189,35 @@ def cancel_download_task():
# Verify slot was actually freed
new_active = download_batches[batch_id]['active_count']
- print(f"🔄 [Cancel] Active count after: {new_active}")
+ print(f"[Cancel] Active count after: {new_active}")
elif active_count == 0:
- print(f"🚫 [Cancel] Task {task_id} - no active workers to free")
+ print(f"[Cancel] Task {task_id} - no active workers to free")
else:
- print(f"🚫 [Cancel] Task {task_id} (status: {current_status}) - not actively running, no slot to free")
+ print(f"[Cancel] Task {task_id} (status: {current_status}) - not actively running, no slot to free")
else:
- print(f"🚫 [Cancel] Task {task_id} - batch {batch_id} not found")
+ print(f"[Cancel] Task {task_id} - batch {batch_id} not found")
except Exception as slot_error:
- print(f"❌ [Cancel] Error managing worker slot for {task_id}: {slot_error}")
+ print(f"[Cancel] Error managing worker slot for {task_id}: {slot_error}")
# Attempt emergency recovery if normal completion failed
if not worker_slot_freed:
try:
- print(f"🚨 [Cancel] Attempting emergency worker slot recovery")
+ print(f"[Cancel] Attempting emergency worker slot recovery")
_recover_worker_slot(batch_id, task_id)
except Exception as recovery_error:
- print(f"💀 [Cancel] FATAL: Emergency recovery failed: {recovery_error}")
+ print(f"[Cancel] FATAL: Emergency recovery failed: {recovery_error}")
else:
- print(f"🚫 [Cancel] Task {task_id} cancelled (no batch_id - likely already completed)")
+ print(f"[Cancel] Task {task_id} cancelled (no batch_id - likely already completed)")
# Optionally try to cancel the Soulseek download (don't block worker progression)
if download_id and username:
try:
# This is an async call, so we run it and wait
run_async(soulseek_client.cancel_download(download_id, username, remove=True))
- print(f"✅ Successfully cancelled Soulseek download {download_id} for task {task_id}")
+ print(f"Successfully cancelled Soulseek download {download_id} for task {task_id}")
except Exception as e:
- print(f"⚠️ Warning: Failed to cancel download on slskd, but worker already moved on. Error: {e}")
+ print(f"Warning: Failed to cancel download on slskd, but worker already moved on. Error: {e}")
### NEW LOGIC START: Add cancelled track to wishlist ###
try:
@@ -29288,11 +29288,11 @@ def cancel_download_task():
)
if success:
- print(f"✅ Added cancelled track '{track_info.get('name')}' to wishlist.")
+ print(f"Added cancelled track '{track_info.get('name')}' to wishlist.")
else:
- print(f"❌ Failed to add cancelled track '{track_info.get('name')}' to wishlist.")
+ print(f"Failed to add cancelled track '{track_info.get('name')}' to wishlist.")
except Exception as e:
- print(f"❌ CRITICAL ERROR adding cancelled track to wishlist: {e}")
+ print(f"CRITICAL ERROR adding cancelled track to wishlist: {e}")
### NEW LOGIC END ###
return jsonify({"success": True, "message": "Task cancelled and added to wishlist for retry."})
@@ -29335,7 +29335,7 @@ def _atomic_cancel_task(playlist_id, track_index):
original_status = current_status # Store original status before changing it
batch_id = task.get('batch_id')
- print(f"🎯 [Atomic Cancel] Starting atomic cancel: playlist={playlist_id}, track={track_index}, task={task_id}, status={current_status}")
+ print(f"[Atomic Cancel] Starting atomic cancel: playlist={playlist_id}, track={track_index}, task={task_id}, status={current_status}")
# Mark task as cancelled immediately (within same lock context)
task['status'] = 'cancelled'
@@ -29356,23 +29356,23 @@ def _atomic_cancel_task(playlist_id, track_index):
# Free worker slot if task was consuming one
# More precise check: only free if task was actually running
if active_count > 0 and current_status in ['pending', 'searching', 'downloading', 'queued']:
- print(f"🔄 [Atomic Cancel] Freeing worker slot for {task_id} (was {current_status})")
+ print(f"[Atomic Cancel] Freeing worker slot for {task_id} (was {current_status})")
# CRITICAL: Direct worker slot management to prevent _on_download_completed race
old_active = batch['active_count']
batch['active_count'] = max(0, old_active - 1) # Prevent negative counts
worker_slot_freed = True
- print(f"🔄 [Atomic Cancel] Worker count: {old_active} → {batch['active_count']}")
+ print(f"[Atomic Cancel] Worker count: {old_active} → {batch['active_count']}")
# Try to start next task if available (still within lock)
if (batch['queue_index'] < len(batch['queue']) and
batch['active_count'] < batch['max_concurrent']):
- print(f"🚀 [Atomic Cancel] Starting next task in queue")
+ print(f"[Atomic Cancel] Starting next task in queue")
# Call the existing function to start next downloads
# Note: This will be called outside the lock to prevent deadlock
else:
- print(f"🚫 [Atomic Cancel] No next task to start (queue_index: {batch['queue_index']}/{len(batch['queue'])}, active: {batch['active_count']}/{batch['max_concurrent']})")
+ print(f"[Atomic Cancel] No next task to start (queue_index: {batch['queue_index']}/{len(batch['queue'])}, active: {batch['active_count']}/{batch['max_concurrent']})")
# Build result info
task_info = {
@@ -29385,11 +29385,11 @@ def _atomic_cancel_task(playlist_id, track_index):
'worker_slot_freed': worker_slot_freed
}
- print(f"✅ [Atomic Cancel] Successfully cancelled task {task_id}")
+ print(f"[Atomic Cancel] Successfully cancelled task {task_id}")
return True, "Task cancelled successfully", task_info
except Exception as e:
- print(f"❌ [Atomic Cancel] Error in atomic cancel: {e}")
+ print(f"[Atomic Cancel] Error in atomic cancel: {e}")
import traceback
traceback.print_exc()
return False, f"Internal error: {str(e)}", None
@@ -29432,14 +29432,14 @@ def cancel_task_v2():
try:
_start_next_batch_of_downloads(batch_id)
except Exception as e:
- print(f"⚠️ [Atomic Cancel] Warning: Could not start next downloads: {e}")
+ print(f"[Atomic Cancel] Warning: Could not start next downloads: {e}")
# CRITICAL: Check for batch completion after V2 cancel
# V2 system bypasses _on_download_completed, so we need to check completion manually
try:
_check_batch_completion_v2(batch_id)
except Exception as e:
- print(f"⚠️ [Atomic Cancel] Warning: Could not check batch completion: {e}")
+ print(f"[Atomic Cancel] Warning: Could not check batch completion: {e}")
# Cancel Soulseek download if active (non-blocking)
if task:
@@ -29448,16 +29448,16 @@ def cancel_task_v2():
current_status = task.get('status')
original_status = task_info.get('original_status', current_status) # Get original status from task_info
- print(f"🔍 [Atomic Cancel] Task {task_id} state: status='{current_status}', original_status='{original_status}', download_id='{download_id}', username='{username}'")
- print(f"🔍 [Atomic Cancel] Download ID type: {type(download_id)}, length: {len(str(download_id)) if download_id else 0}")
+ print(f"[Atomic Cancel] Task {task_id} state: status='{current_status}', original_status='{original_status}', download_id='{download_id}', username='{username}'")
+ print(f"[Atomic Cancel] Download ID type: {type(download_id)}, length: {len(str(download_id)) if download_id else 0}")
backslash = '\\'
- print(f"🔍 [Atomic Cancel] Download ID looks like filename: {download_id and ('/' in str(download_id) or backslash in str(download_id))}")
+ print(f"[Atomic Cancel] Download ID looks like filename: {download_id and ('/' in str(download_id) or backslash in str(download_id))}")
if download_id and username:
# Always try to cancel in slskd - doesn't matter what status it was
# If it's not there or already done, the DELETE request will just fail harmlessly
try:
- print(f"🚫 [Atomic Cancel] Attempting to cancel Soulseek download:")
+ print(f"[Atomic Cancel] Attempting to cancel Soulseek download:")
print(f" Username: {username}")
print(f" Download ID: {download_id}")
print(f" Base URL: {soulseek_client.base_url}")
@@ -29468,7 +29468,7 @@ def cancel_task_v2():
real_download_id = None
# Step 1: Always search for real download ID first
- print(f"🔍 [Atomic Cancel] Searching slskd transfers for real download ID")
+ print(f"[Atomic Cancel] Searching slskd transfers for real download ID")
try:
all_transfers = run_async(soulseek_client._make_request('GET', 'transfers/downloads'))
if all_transfers:
@@ -29482,60 +29482,60 @@ def cancel_task_v2():
if (file_filename == download_id or
__import__('os').path.basename(file_filename) == __import__('os').path.basename(str(download_id))):
real_download_id = file_data.get('id')
- print(f"🎯 [Atomic Cancel] Found real download ID: {real_download_id} for file: {file_filename}")
+ print(f"[Atomic Cancel] Found real download ID: {real_download_id} for file: {file_filename}")
break
if real_download_id:
break
if real_download_id:
break
except Exception as search_error:
- print(f"⚠️ [Atomic Cancel] Error searching transfers: {search_error}")
+ print(f"[Atomic Cancel] Error searching transfers: {search_error}")
# Step 2: Try cancellation with real ID if found
if real_download_id:
- print(f"🔄 [Atomic Cancel] Attempting cancel with real ID: {real_download_id}")
+ print(f"[Atomic Cancel] Attempting cancel with real ID: {real_download_id}")
try:
# Use EXACT format from slskd web UI: DELETE /api/v0/transfers/downloads/{username}/{download_id}?remove=false
endpoint = f'transfers/downloads/{username}/{real_download_id}?remove=true'
- print(f"🌐 [Atomic Cancel] Using slskd web UI format: {endpoint}")
+ print(f"[Atomic Cancel] Using slskd web UI format: {endpoint}")
response = run_async(soulseek_client._make_request('DELETE', endpoint))
if response is not None:
- print(f"✅ [Atomic Cancel] Successfully cancelled with slskd web UI format: {real_download_id}")
+ print(f"[Atomic Cancel] Successfully cancelled with slskd web UI format: {real_download_id}")
success = True
else:
- print(f"⚠️ [Atomic Cancel] Web UI format failed, trying alternative formats")
+ print(f"[Atomic Cancel] Web UI format failed, trying alternative formats")
# Fallback: Try without remove parameter
endpoint2 = f'transfers/downloads/{username}/{real_download_id}'
response2 = run_async(soulseek_client._make_request('DELETE', endpoint2))
if response2 is not None:
- print(f"✅ [Atomic Cancel] Successfully cancelled without remove param: {real_download_id}")
+ print(f"[Atomic Cancel] Successfully cancelled without remove param: {real_download_id}")
success = True
else:
# Final fallback: Try simple format (sync.py style)
endpoint3 = f'transfers/downloads/{real_download_id}'
response3 = run_async(soulseek_client._make_request('DELETE', endpoint3))
if response3 is not None:
- print(f"✅ [Atomic Cancel] Successfully cancelled with simple format: {real_download_id}")
+ print(f"[Atomic Cancel] Successfully cancelled with simple format: {real_download_id}")
success = True
else:
- print(f"⚠️ [Atomic Cancel] All DELETE formats failed for real ID: {real_download_id}")
+ print(f"[Atomic Cancel] All DELETE formats failed for real ID: {real_download_id}")
except Exception as cancel_error:
- print(f"⚠️ [Atomic Cancel] Exception cancelling real ID {real_download_id}: {cancel_error}")
+ print(f"[Atomic Cancel] Exception cancelling real ID {real_download_id}: {cancel_error}")
else:
- print(f"⚠️ [Atomic Cancel] Could not find real download ID in slskd transfers")
- print(f"🔄 [Atomic Cancel] This might be a pending download not yet in slskd - relying on status='cancelled' to prevent it")
+ print(f"[Atomic Cancel] Could not find real download ID in slskd transfers")
+ print(f"[Atomic Cancel] This might be a pending download not yet in slskd - relying on status='cancelled' to prevent it")
# For pending downloads, the status='cancelled' will prevent them from starting
success = True # Consider this success since pending downloads are prevented
if not success:
- print(f"❌ [Atomic Cancel] Failed to cancel download in slskd API")
+ print(f"[Atomic Cancel] Failed to cancel download in slskd API")
except Exception as e:
- print(f"⚠️ [Atomic Cancel] Exception cancelling Soulseek download {download_id}: {e}")
+ print(f"[Atomic Cancel] Exception cancelling Soulseek download {download_id}: {e}")
# Print more details about the error
import traceback
- print(f"⚠️ [Atomic Cancel] Cancel error traceback: {traceback.format_exc()}")
+ print(f"[Atomic Cancel] Cancel error traceback: {traceback.format_exc()}")
else:
print(f"ℹ️ [Atomic Cancel] No download_id or username available - skipping slskd cancel")
@@ -29543,7 +29543,7 @@ def cancel_task_v2():
try:
_add_cancelled_task_to_wishlist(task)
except Exception as e:
- print(f"⚠️ [Atomic Cancel] Warning: Could not add to wishlist: {e}")
+ print(f"[Atomic Cancel] Warning: Could not add to wishlist: {e}")
return jsonify({
"success": True,
@@ -29556,7 +29556,7 @@ def cancel_task_v2():
})
except Exception as e:
- print(f"❌ [Cancel V2] Unexpected error: {e}")
+ print(f"[Cancel V2] Unexpected error: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -29571,7 +29571,7 @@ def _check_batch_completion_v2(batch_id):
try:
with tasks_lock:
if batch_id not in download_batches:
- print(f"⚠️ [Completion Check V2] Batch {batch_id} not found")
+ print(f"[Completion Check V2] Batch {batch_id} not found")
return
batch = download_batches[batch_id]
@@ -29611,18 +29611,18 @@ def _check_batch_completion_v2(batch_id):
finished_count += 1
else:
# Task ID in queue but not in download_tasks - treat as completed to prevent blocking
- print(f"⚠️ [Orphaned Task V2] Task {task_id} in queue but not in download_tasks - counting as finished")
+ print(f"[Orphaned Task V2] Task {task_id} in queue but not in download_tasks - counting as finished")
finished_count += 1
all_tasks_truly_finished = finished_count >= len(queue)
has_retrying_tasks = retrying_count > 0
- print(f"🔍 [Completion Check V2] Batch {batch_id}: tasks_started={all_tasks_started}, workers={no_active_workers}, finished={finished_count}/{len(queue)}, retrying={retrying_count}")
+ print(f"[Completion Check V2] Batch {batch_id}: tasks_started={all_tasks_started}, workers={no_active_workers}, finished={finished_count}/{len(queue)}, retrying={retrying_count}")
if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks:
# FIXED: Ensure batch is not already marked as complete to prevent duplicate processing
if batch.get('phase') != 'complete':
- print(f"🎉 [Completion Check V2] Batch {batch_id} is complete - marking as finished")
+ print(f"[Completion Check V2] Batch {batch_id} is complete - marking as finished")
# Check if this is an auto-initiated batch
is_auto_batch = batch.get('auto_initiated', False)
@@ -29635,7 +29635,7 @@ def _check_batch_completion_v2(batch_id):
playlist_name = batch.get('playlist_name', 'Unknown Playlist')
failed_count = len(batch.get('permanently_failed_tracks', []))
successful_downloads = finished_count - failed_count
- add_activity_item("✅", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now")
+ add_activity_item("", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now")
# Emit batch_complete event for automation engine (only if something downloaded)
if successful_downloads > 0:
@@ -29650,7 +29650,7 @@ def _check_batch_completion_v2(batch_id):
except Exception:
pass
else:
- print(f"✅ [Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing")
+ print(f"[Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing")
return True # Already complete
# Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist
@@ -29659,30 +29659,30 @@ def _check_batch_completion_v2(batch_id):
url_hash = playlist_id.replace('youtube_', '')
if url_hash in youtube_playlist_states:
youtube_playlist_states[url_hash]['phase'] = 'download_complete'
- print(f"📋 [Completion Check V2] Updated YouTube playlist {url_hash} to download_complete phase")
+ print(f"[Completion Check V2] Updated YouTube playlist {url_hash} to download_complete phase")
# Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist
if playlist_id and playlist_id.startswith('tidal_'):
tidal_playlist_id = playlist_id.replace('tidal_', '')
if tidal_playlist_id in tidal_discovery_states:
tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete'
- print(f"📋 [Completion Check V2] Updated Tidal playlist {tidal_playlist_id} to download_complete phase")
+ print(f"[Completion Check V2] Updated Tidal playlist {tidal_playlist_id} to download_complete phase")
# Update Deezer playlist phase to 'download_complete' if this is a Deezer playlist
if playlist_id and playlist_id.startswith('deezer_'):
deezer_playlist_id = playlist_id.replace('deezer_', '')
if deezer_playlist_id in deezer_discovery_states:
deezer_discovery_states[deezer_playlist_id]['phase'] = 'download_complete'
- print(f"📋 [Completion Check V2] Updated Deezer playlist {deezer_playlist_id} to download_complete phase")
+ print(f"[Completion Check V2] Updated Deezer playlist {deezer_playlist_id} to download_complete phase")
# Update Spotify Public playlist phase to 'download_complete' if this is a Spotify Public playlist
if playlist_id and playlist_id.startswith('spotify_public_'):
spotify_public_url_hash = playlist_id.replace('spotify_public_', '')
if spotify_public_url_hash in spotify_public_discovery_states:
spotify_public_discovery_states[spotify_public_url_hash]['phase'] = 'download_complete'
- print(f"📋 [Completion Check V2] Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase")
+ print(f"[Completion Check V2] Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase")
- print(f"🎉 [Completion Check V2] Batch {batch_id} complete - stopping monitor")
+ print(f"[Completion Check V2] Batch {batch_id} complete - stopping monitor")
download_monitor.stop_monitoring(batch_id)
# REPAIR: Scan all album folders from this batch for track number issues
@@ -29693,21 +29693,21 @@ def _check_batch_completion_v2(batch_id):
if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks:
# Call wishlist processing outside the lock
if is_auto_batch:
- print(f"🤖 [Completion Check V2] Processing auto-initiated batch completion")
+ print(f"[Completion Check V2] Processing auto-initiated batch completion")
# Use the existing auto-completion function
_process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id)
else:
- print(f"📋 [Completion Check V2] Processing regular batch completion")
+ print(f"[Completion Check V2] Processing regular batch completion")
# Use the regular completion function
_process_failed_tracks_to_wishlist_exact(batch_id)
return True # Batch was completed
else:
- print(f"📊 [Completion Check V2] Batch {batch_id} not yet complete: finished={finished_count}/{len(queue)}, retrying={retrying_count}, workers={batch['active_count']}")
+ print(f"[Completion Check V2] Batch {batch_id} not yet complete: finished={finished_count}/{len(queue)}, retrying={retrying_count}, workers={batch['active_count']}")
return False # Batch still in progress
except Exception as e:
- print(f"❌ [Completion Check V2] Error checking batch completion: {e}")
+ print(f"[Completion Check V2] Error checking batch completion: {e}")
import traceback
traceback.print_exc()
return False
@@ -29783,12 +29783,12 @@ def _add_cancelled_task_to_wishlist(task):
)
if success:
- print(f"✅ [Atomic Cancel] Added '{track_info.get('name')}' to wishlist")
+ print(f"[Atomic Cancel] Added '{track_info.get('name')}' to wishlist")
else:
- print(f"❌ [Atomic Cancel] Failed to add '{track_info.get('name')}' to wishlist")
+ print(f"[Atomic Cancel] Failed to add '{track_info.get('name')}' to wishlist")
except Exception as e:
- print(f"❌ [Atomic Cancel] Critical error adding to wishlist: {e}")
+ print(f"[Atomic Cancel] Critical error adding to wishlist: {e}")
@app.route('/api/playlists//cancel_batch', methods=['POST'])
def cancel_batch(batch_id):
@@ -29816,7 +29816,7 @@ def cancel_batch(batch_id):
with wishlist_timer_lock:
wishlist_auto_processing = False
wishlist_auto_processing_timestamp = 0
- print(f"🔓 [Wishlist Cancel] Reset wishlist auto-processing flag for cancelled auto-batch")
+ print(f"[Wishlist Cancel] Reset wishlist auto-processing flag for cancelled auto-batch")
else:
print(f"ℹ️ [Wishlist Cancel] Manual wishlist batch cancelled (no flag reset needed)")
@@ -29825,7 +29825,7 @@ def cancel_batch(batch_id):
url_hash = playlist_id.replace('youtube_', '')
if url_hash in youtube_playlist_states:
youtube_playlist_states[url_hash]['phase'] = 'discovered'
- print(f"📋 Reset YouTube playlist {url_hash} to discovered phase (batch cancelled)")
+ print(f"Reset YouTube playlist {url_hash} to discovered phase (batch cancelled)")
# Cancel all individual tasks in the batch
cancelled_count = 0
@@ -29838,13 +29838,13 @@ def cancel_batch(batch_id):
# Add activity for batch cancellation
playlist_name = download_batches[batch_id].get('playlist_name', 'Unknown Playlist')
- add_activity_item("🚫", "Batch Cancelled", f"'{playlist_name}' - {cancelled_count} downloads cancelled", "Now")
+ add_activity_item("", "Batch Cancelled", f"'{playlist_name}' - {cancelled_count} downloads cancelled", "Now")
- print(f"✅ Cancelled batch {batch_id} with {cancelled_count} tasks")
+ print(f"Cancelled batch {batch_id} with {cancelled_count} tasks")
return jsonify({"success": True, "cancelled_tasks": cancelled_count})
except Exception as e:
- print(f"❌ Error cancelling batch {batch_id}: {e}")
+ print(f"Error cancelling batch {batch_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# NEW ENDPOINT: Add this function to web_server.py
@@ -29869,7 +29869,7 @@ def cleanup_batch():
# This prevents a race condition where cleanup deletes the batch before
# the wishlist processing thread can access it
if batch.get('wishlist_processing_started') and not batch.get('wishlist_processing_complete'):
- print(f"⏳ [Cleanup] Batch {batch_id} cleanup deferred - wishlist processing in progress")
+ print(f"[Cleanup] Batch {batch_id} cleanup deferred - wishlist processing in progress")
return jsonify({
"success": False,
"error": "Batch cleanup deferred - wishlist processing in progress",
@@ -29887,15 +29887,15 @@ def cleanup_batch():
if task_id in download_tasks:
del download_tasks[task_id]
- print(f"✅ Cleaned up batch '{batch_id}' and its associated tasks from server state.")
+ print(f"Cleaned up batch '{batch_id}' and its associated tasks from server state.")
return jsonify({"success": True, "message": f"Batch {batch_id} cleaned up."})
else:
# It's not an error if the batch is already gone
- print(f"⚠️ Cleanup requested for non-existent batch '{batch_id}'. Already cleaned up?")
+ print(f"Cleanup requested for non-existent batch '{batch_id}'. Already cleaned up?")
return jsonify({"success": True, "message": "Batch already cleaned up."})
except Exception as e:
- print(f"❌ Error during batch cleanup for '{batch_id}': {e}")
+ print(f"Error during batch cleanup for '{batch_id}': {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ===============================
@@ -30738,12 +30738,12 @@ def start_missing_tracks_process(playlist_id):
# Log album context if provided
if is_album_download and album_context and artist_context:
- print(f"🎵 [Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})")
+ print(f"[Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})")
print(f" Release: {album_context.get('release_date', 'Unknown')}, Tracks: {album_context.get('total_tracks', len(tracks))}")
# Log playlist folder mode if enabled
if playlist_folder_mode:
- print(f"📁 [Playlist Folder] Enabled for playlist: '{playlist_name}'")
+ print(f"[Playlist Folder] Enabled for playlist: '{playlist_name}'")
# Limit concurrent analysis processes to prevent resource exhaustion
with tasks_lock:
@@ -30795,7 +30795,7 @@ def start_missing_tracks_process(playlist_id):
youtube_playlist_states[url_hash]['download_process_id'] = batch_id
youtube_playlist_states[url_hash]['phase'] = 'downloading'
youtube_playlist_states[url_hash]['converted_spotify_playlist_id'] = playlist_id
- print(f"🔗 Linked YouTube playlist {url_hash} to download process {batch_id} (converted ID: {playlist_id})")
+ print(f"Linked YouTube playlist {url_hash} to download process {batch_id} (converted ID: {playlist_id})")
# Link Tidal playlist to download process if this is a Tidal playlist
if playlist_id.startswith('tidal_'):
@@ -30804,7 +30804,7 @@ def start_missing_tracks_process(playlist_id):
tidal_discovery_states[tidal_playlist_id]['download_process_id'] = batch_id
tidal_discovery_states[tidal_playlist_id]['phase'] = 'downloading'
tidal_discovery_states[tidal_playlist_id]['converted_spotify_playlist_id'] = playlist_id
- print(f"🔗 Linked Tidal playlist {tidal_playlist_id} to download process {batch_id} (converted ID: {playlist_id})")
+ print(f"Linked Tidal playlist {tidal_playlist_id} to download process {batch_id} (converted ID: {playlist_id})")
# Link Spotify Public playlist to download process if this is a Spotify Public playlist
if playlist_id.startswith('spotify_public_'):
@@ -30813,7 +30813,7 @@ def start_missing_tracks_process(playlist_id):
spotify_public_discovery_states[sp_url_hash]['download_process_id'] = batch_id
spotify_public_discovery_states[sp_url_hash]['phase'] = 'downloading'
spotify_public_discovery_states[sp_url_hash]['converted_spotify_playlist_id'] = playlist_id
- print(f"🔗 Linked Spotify Public playlist {sp_url_hash} to download process {batch_id} (converted ID: {playlist_id})")
+ print(f"Linked Spotify Public playlist {sp_url_hash} to download process {batch_id} (converted ID: {playlist_id})")
# Link Deezer playlist to download process if this is a Deezer playlist
if playlist_id.startswith('deezer_'):
@@ -30822,7 +30822,7 @@ def start_missing_tracks_process(playlist_id):
deezer_discovery_states[deezer_playlist_id]['download_process_id'] = batch_id
deezer_discovery_states[deezer_playlist_id]['phase'] = 'downloading'
deezer_discovery_states[deezer_playlist_id]['converted_spotify_playlist_id'] = playlist_id
- print(f"🔗 Linked Deezer playlist {deezer_playlist_id} to download process {batch_id} (converted ID: {playlist_id})")
+ print(f"Linked Deezer playlist {deezer_playlist_id} to download process {batch_id} (converted ID: {playlist_id})")
# Stamp original index to keep task indices aligned with frontend row order
for i, track in enumerate(tracks):
@@ -30892,7 +30892,7 @@ def start_missing_downloads():
return jsonify({"success": True, "batch_id": batch_id, "message": f"Queued {len(missing_tracks)} downloads for processing."})
except Exception as e:
- print(f"❌ Error starting missing downloads: {e}")
+ print(f"Error starting missing downloads: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ===============================
@@ -30909,7 +30909,7 @@ def _load_sync_status_file():
return data
return {}
except Exception as e:
- print(f"❌ Error loading sync status: {e}")
+ print(f"Error loading sync status: {e}")
return {}
def _save_sync_status_file(sync_statuses):
@@ -30918,7 +30918,7 @@ def _save_sync_status_file(sync_statuses):
database = get_database()
database.set_preference('sync_statuses', json.dumps(sync_statuses))
except Exception as e:
- print(f"❌ Error saving sync status: {e}")
+ print(f"Error saving sync status: {e}")
def _update_and_save_sync_status(playlist_id, playlist_name, playlist_owner, snapshot_id, **kwargs):
"""Updates the sync status for a given playlist and saves to file (same logic as GUI)."""
@@ -30943,10 +30943,10 @@ def _update_and_save_sync_status(playlist_id, playlist_name, playlist_owner, sna
# Save to file
_save_sync_status_file(sync_statuses)
- print(f"🔄 Updated sync status for playlist '{playlist_name}' (ID: {playlist_id})")
+ print(f"Updated sync status for playlist '{playlist_name}' (ID: {playlist_id})")
except Exception as e:
- print(f"❌ Error updating sync status for {playlist_id}: {e}")
+ print(f"Error updating sync status for {playlist_id}: {e}")
@app.route('/api/spotify/playlists', methods=['GET'])
def get_spotify_playlists():
@@ -30967,7 +30967,7 @@ def get_spotify_playlists():
# Handle snapshot_id safely - may not exist in core Playlist class
playlist_snapshot = getattr(p, 'snapshot_id', '')
- print(f"🔍 Processing playlist: {p.name} (ID: {p.id})")
+ print(f"Processing playlist: {p.name} (ID: {p.id})")
print(f" - Playlist snapshot: '{playlist_snapshot}'")
print(f" - Status info: {status_info}")
@@ -31020,9 +31020,9 @@ def get_spotify_playlists():
"sync_status": sync_status,
"snapshot_id": "" # Liked Songs doesn't have a snapshot_id
})
- print(f"🔍 Added virtual 'Liked Songs' playlist with {liked_songs_count} tracks (count only)")
+ print(f"Added virtual 'Liked Songs' playlist with {liked_songs_count} tracks (count only)")
except Exception as liked_error:
- print(f"⚠️ Failed to add Liked Songs playlist: {liked_error}")
+ print(f"Failed to add Liked Songs playlist: {liked_error}")
# Don't fail the entire request if Liked Songs fails
return jsonify(playlist_data)
@@ -31331,7 +31331,7 @@ def search_spotify():
return jsonify({'tracks': {'items': tracks_items}})
except Exception as e:
- print(f"❌ Error searching Spotify: {e}")
+ print(f"Error searching Spotify: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify/search_tracks', methods=['GET'])
@@ -31380,7 +31380,7 @@ def search_spotify_tracks():
return jsonify({'tracks': tracks_dict})
except Exception as e:
- print(f"❌ Error searching Spotify tracks: {e}")
+ print(f"Error searching Spotify tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -31430,7 +31430,7 @@ def search_itunes_tracks():
return jsonify({'tracks': tracks_dict})
except Exception as e:
- print(f"❌ Error searching iTunes tracks: {e}")
+ print(f"Error searching iTunes tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -31472,7 +31472,7 @@ def search_deezer_tracks():
return jsonify({'tracks': tracks_dict})
except Exception as e:
- print(f"❌ Error searching Deezer tracks: {e}")
+ print(f"Error searching Deezer tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -32046,7 +32046,7 @@ def get_tidal_playlists():
playlist_data.append(playlist_dict)
- print(f"🎵 Loaded {len(playlist_data)} Tidal playlists with track data")
+ print(f"Loaded {len(playlist_data)} Tidal playlists with track data")
return jsonify(playlist_data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -32057,7 +32057,7 @@ def get_tidal_playlist_tracks(playlist_id):
if not tidal_client or not tidal_client.is_authenticated():
return jsonify({"error": "Tidal not authenticated."}), 401
try:
- print(f"🎵 Getting full Tidal playlist with tracks for: {playlist_id}")
+ print(f"Getting full Tidal playlist with tracks for: {playlist_id}")
# Fetch this single playlist directly — no need to re-fetch all playlists
full_playlist = tidal_client.get_playlist(playlist_id)
@@ -32067,7 +32067,7 @@ def get_tidal_playlist_tracks(playlist_id):
if not full_playlist.tracks:
return jsonify({"error": "This playlist appears to have no tracks or they cannot be accessed"}), 403
- print(f"🎵 Loaded {len(full_playlist.tracks)} tracks from Tidal playlist: {full_playlist.name}")
+ print(f"Loaded {len(full_playlist.tracks)} tracks from Tidal playlist: {full_playlist.name}")
# Convert playlist to dict (matches sync.py structure)
playlist_dict = {
@@ -32092,7 +32092,7 @@ def get_tidal_playlist_tracks(playlist_id):
return jsonify(playlist_dict)
except Exception as e:
- print(f"❌ Error getting Tidal playlist tracks: {e}")
+ print(f"Error getting Tidal playlist tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -32152,18 +32152,18 @@ def start_tidal_discovery(playlist_id):
tidal_discovery_states[playlist_id] = state
# Add activity for discovery start
- add_activity_item("🔍", "Tidal Discovery Started", f"'{target_playlist.name}' - {len(target_playlist.tracks)} tracks", "Now")
+ add_activity_item("", "Tidal Discovery Started", f"'{target_playlist.name}' - {len(target_playlist.tracks)} tracks", "Now")
# Start discovery worker (capture profile ID while we have Flask context)
state['_profile_id'] = get_current_profile_id()
future = tidal_discovery_executor.submit(_run_tidal_discovery_worker, playlist_id)
state['discovery_future'] = future
- print(f"🔍 Started Spotify discovery for Tidal playlist: {target_playlist.name}")
+ print(f"Started Spotify discovery for Tidal playlist: {target_playlist.name}")
return jsonify({"success": True, "message": "Discovery started"})
except Exception as e:
- print(f"❌ Error starting Tidal discovery: {e}")
+ print(f"Error starting Tidal discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/discovery/status/', methods=['GET'])
@@ -32189,7 +32189,7 @@ def get_tidal_discovery_status(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Tidal discovery status: {e}")
+ print(f"Error getting Tidal discovery status: {e}")
return jsonify({"error": str(e)}), 500
@@ -32219,7 +32219,7 @@ def update_tidal_discovery_match():
old_status = result.get('status')
# Update with user-selected track
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = spotify_track['name']
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
@@ -32247,10 +32247,10 @@ def update_tidal_discovery_match():
result['manual_match'] = True # Flag for tracking
# Update match count if status changed from not found/error
- if old_status != 'found' and old_status != '✅ Found':
+ if old_status != 'found' and old_status != 'Found':
state['spotify_matches'] = state.get('spotify_matches', 0) + 1
- print(f"✅ Manual match updated: tidal - {identifier} - track {track_index}")
+ print(f"Manual match updated: tidal - {identifier} - track {track_index}")
print(f" → {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
@@ -32283,14 +32283,14 @@ def update_tidal_discovery_match():
cache_key[0], cache_key[1], 'spotify', 1.0, matched_data,
original_name, original_artist
)
- print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}")
+ print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}")
except Exception as cache_err:
- print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}")
+ print(f"Error saving manual fix to discovery cache: {cache_err}")
return jsonify({'success': True, 'result': result})
except Exception as e:
- print(f"❌ Error updating Tidal discovery match: {e}")
+ print(f"Error updating Tidal discovery match: {e}")
return jsonify({'error': str(e)}), 500
@@ -32320,11 +32320,11 @@ def get_tidal_playlist_states():
}
states.append(state_info)
- print(f"🎵 Returning {len(states)} stored Tidal playlist states for hydration")
+ print(f"Returning {len(states)} stored Tidal playlist states for hydration")
return jsonify({"states": states})
except Exception as e:
- print(f"❌ Error getting Tidal playlist states: {e}")
+ print(f"Error getting Tidal playlist states: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/state/', methods=['GET'])
@@ -32357,7 +32357,7 @@ def get_tidal_playlist_state(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Tidal playlist state: {e}")
+ print(f"Error getting Tidal playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/reset/', methods=['POST'])
@@ -32386,11 +32386,11 @@ def reset_tidal_playlist(playlist_id):
state['discovery_future'] = None
state['last_accessed'] = time.time()
- print(f"🔄 Reset Tidal playlist to fresh: {playlist_id}")
+ print(f"Reset Tidal playlist to fresh: {playlist_id}")
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
except Exception as e:
- print(f"❌ Error resetting Tidal playlist: {e}")
+ print(f"Error resetting Tidal playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/delete/', methods=['POST'])
@@ -32409,11 +32409,11 @@ def delete_tidal_playlist(playlist_id):
# Remove from state dictionary
del tidal_discovery_states[playlist_id]
- print(f"🗑️ Deleted Tidal playlist state: {playlist_id}")
+ print(f"Deleted Tidal playlist state: {playlist_id}")
return jsonify({"success": True, "message": "Playlist deleted"})
except Exception as e:
- print(f"❌ Error deleting Tidal playlist: {e}")
+ print(f"Error deleting Tidal playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/update_phase/', methods=['POST'])
@@ -32438,11 +32438,11 @@ def update_tidal_playlist_phase(playlist_id):
state['phase'] = new_phase
state['last_accessed'] = time.time()
- print(f"🔄 Updated Tidal playlist {playlist_id} phase: {old_phase} → {new_phase}")
+ print(f"Updated Tidal playlist {playlist_id} phase: {old_phase} → {new_phase}")
return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase})
except Exception as e:
- print(f"❌ Error updating Tidal playlist phase: {e}")
+ print(f"Error updating Tidal playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@@ -32463,7 +32463,7 @@ def _pause_enrichment_workers(label='discovery'):
if worker and not worker.paused:
worker.pause()
was_running[name] = True
- print(f"⏸️ Paused {name} enrichment worker during {label}")
+ print(f"Paused {name} enrichment worker during {label}")
except Exception:
pass
return was_running
@@ -32481,7 +32481,7 @@ def _resume_enrichment_workers(was_running, label='discovery'):
try:
if was_running.get(name) and worker:
worker.resume()
- print(f"▶️ Resumed {name} enrichment worker after {label}")
+ print(f"Resumed {name} enrichment worker after {label}")
except Exception:
pass
@@ -32500,10 +32500,10 @@ def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discove
break
if not mirrored_pl:
- print(f"⚠️ [Discovery Sync] No mirrored playlist found for {source_type}:{source_playlist_id} (profile {profile_id})")
+ print(f"[Discovery Sync] No mirrored playlist found for {source_type}:{source_playlist_id} (profile {profile_id})")
return
- print(f"📝 [Discovery Sync] Found mirrored playlist '{mirrored_pl.get('name')}' (DB id={mirrored_pl['id']}) for {source_type}:{source_playlist_id}")
+ print(f"[Discovery Sync] Found mirrored playlist '{mirrored_pl.get('name')}' (DB id={mirrored_pl['id']}) for {source_type}:{source_playlist_id}")
mirrored_tracks = db.get_mirrored_playlist_tracks(mirrored_pl['id'])
if not mirrored_tracks:
return
@@ -32558,11 +32558,11 @@ def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discove
updated += 1
if updated > 0:
- print(f"📝 Synced {updated} discovery results back to mirrored playlist '{mirrored_pl.get('name', '')}'")
+ print(f"Synced {updated} discovery results back to mirrored playlist '{mirrored_pl.get('name', '')}'")
except Exception as e:
import traceback
- print(f"⚠️ Failed to sync discovery results to mirrored playlist: {e}")
+ print(f"Failed to sync discovery results to mirrored playlist: {e}")
traceback.print_exc()
@@ -32580,7 +32580,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
try:
itunes_client_instance = _get_metadata_fallback_client()
except Exception:
- print(f"❌ Neither Spotify nor {_get_metadata_fallback_source()} available for discovery")
+ print(f"Neither Spotify nor {_get_metadata_fallback_source()} available for discovery")
_update_automation_progress(automation_id, status='error', progress=100,
phase='Error', log_line=f'Neither Spotify nor {_get_metadata_fallback_source()} available',
log_type='error')
@@ -32612,7 +32612,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
if not tracks:
continue
- print(f"🔍 Starting discovery for playlist '{pl_name}' ({len(tracks)} tracks, using {discovery_source.upper()})")
+ print(f"Starting discovery for playlist '{pl_name}' ({len(tracks)} tracks, using {discovery_source.upper()})")
_update_automation_progress(automation_id, phase=f'Discovering: "{pl_name}"',
log_line=f'Playlist "{pl_name}" — {len(tracks)} tracks ({discovery_source.upper()})', log_type='info')
@@ -32661,7 +32661,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
# Check for cancellation
if automation_id and automation_id in _playlist_discovery_cancelled:
_playlist_discovery_cancelled.discard(automation_id)
- print(f"🛑 Playlist discovery cancelled (automation {automation_id})")
+ print(f"Playlist discovery cancelled (automation {automation_id})")
_update_automation_progress(automation_id, status='finished', progress=100,
phase='Discovery cancelled',
log_line=f'Cancelled: {total_discovered} discovered, {total_failed} failed',
@@ -32687,7 +32687,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
}
db.update_mirrored_track_extra_data(track_id, extra_data)
total_discovered += 1
- print(f"⚡ CACHE [{i+1}/{len(undiscovered_tracks)}]: {track_name} → {cached_match.get('name', '?')}")
+ print(f"CACHE [{i+1}/{len(undiscovered_tracks)}]: {track_name} → {cached_match.get('name', '?')}")
_update_automation_progress(automation_id,
progress=((total_skipped + total_discovered + total_failed) / max(1, grand_total)) * 100,
current_item=track_name,
@@ -32822,7 +32822,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
except Exception:
pass
- print(f"✅ [{i+1}/{len(undiscovered_tracks)}] {track_name} → {matched_data['name']} ({best_confidence:.2f})")
+ print(f"[{i+1}/{len(undiscovered_tracks)}] {track_name} → {matched_data['name']} ({best_confidence:.2f})")
_update_automation_progress(automation_id,
progress=((total_skipped + total_discovered + total_failed) / max(1, grand_total)) * 100,
processed=total_discovered + total_failed,
@@ -32836,7 +32836,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
}
db.update_mirrored_track_extra_data(track_id, extra_data)
total_failed += 1
- print(f"❌ [{i+1}/{len(undiscovered_tracks)}] No match: {track_name} by {artist_name}")
+ print(f"[{i+1}/{len(undiscovered_tracks)}] No match: {track_name} by {artist_name}")
_update_automation_progress(automation_id,
progress=((total_skipped + total_discovered + total_failed) / max(1, grand_total)) * 100,
processed=total_discovered + total_failed,
@@ -32861,14 +32861,14 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
except Exception:
pass
- print(f"✅ Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped")
+ print(f"Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped")
_update_automation_progress(automation_id, status='finished', progress=100,
phase='Discovery complete',
log_line=f'Done: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped',
log_type='success')
except Exception as e:
- print(f"❌ Error in playlist discovery worker: {e}")
+ print(f"Error in playlist discovery worker: {e}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id, status='error', progress=100,
@@ -32929,7 +32929,7 @@ def _validate_discovery_cache_artist(source_artist, cached_match):
best_sim = sim
if best_sim < min_artist_similarity:
- print(f"🚫 Cache artist mismatch: source='{source_artist}' vs cached='{cached_artists[0]}' (sim={best_sim:.2f}), re-searching")
+ print(f"Cache artist mismatch: source='{source_artist}' vs cached='{cached_artists[0]}' (sim={best_sim:.2f}), re-searching")
return False
return True
@@ -33016,7 +33016,7 @@ def _discovery_score_candidates(source_title, source_artist, source_duration_ms,
best_index = idx
except Exception as e:
- print(f"⚠️ Error scoring candidate {idx}: {e}")
+ print(f"Error scoring candidate {idx}: {e}")
continue
return best_match, best_confidence, best_index
@@ -33039,7 +33039,7 @@ def _run_tidal_discovery_worker(playlist_id):
if not use_spotify:
itunes_client_instance = _get_metadata_fallback_client()
- print(f"🎵 Starting Tidal discovery for: {playlist.name} (using {discovery_source.upper()})")
+ print(f"Starting Tidal discovery for: {playlist.name} (using {discovery_source.upper()})")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -33051,7 +33051,7 @@ def _run_tidal_discovery_worker(playlist_id):
break
try:
- print(f"🔍 [{i+1}/{len(playlist.tracks)}] Searching {discovery_source.upper()}: {tidal_track.name} by {', '.join(tidal_track.artists)}")
+ print(f"[{i+1}/{len(playlist.tracks)}] Searching {discovery_source.upper()}: {tidal_track.name} by {', '.join(tidal_track.artists)}")
# Check discovery cache first
cache_key = _get_discovery_cache_key(tidal_track.name, tidal_track.artists[0] if tidal_track.artists else '')
@@ -33059,7 +33059,7 @@ def _run_tidal_discovery_worker(playlist_id):
cache_db = get_database()
cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source)
if cached_match and _validate_discovery_cache_artist(tidal_track.artists[0] if tidal_track.artists else '', cached_match):
- print(f"⚡ CACHE HIT [{i+1}/{len(playlist.tracks)}]: {tidal_track.name} by {', '.join(tidal_track.artists)}")
+ print(f"CACHE HIT [{i+1}/{len(playlist.tracks)}]: {tidal_track.name} by {', '.join(tidal_track.artists)}")
result = {
'tidal_track': {
'id': tidal_track.id,
@@ -33079,7 +33079,7 @@ def _run_tidal_discovery_worker(playlist_id):
state['discovery_progress'] = int(((i + 1) / len(playlist.tracks)) * 100)
continue
except Exception as cache_err:
- print(f"⚠️ Cache lookup error: {cache_err}")
+ print(f"Cache lookup error: {cache_err}")
# Use the search function with appropriate provider
track_result = _search_spotify_for_tidal_track(
@@ -33169,9 +33169,9 @@ def _run_tidal_discovery_worker(playlist_id):
result['match_data'], tidal_track.name,
tidal_track.artists[0] if tidal_track.artists else ''
)
- print(f"💾 CACHE SAVED: {tidal_track.name} (confidence: {match_confidence:.3f})")
+ print(f"CACHE SAVED: {tidal_track.name} (confidence: {match_confidence:.3f})")
except Exception as cache_err:
- print(f"⚠️ Cache save error: {cache_err}")
+ print(f"Cache save error: {cache_err}")
state['discovery_results'].append(result)
state['discovery_progress'] = int(((i + 1) / len(playlist.tracks)) * 100)
@@ -33180,7 +33180,7 @@ def _run_tidal_discovery_worker(playlist_id):
time.sleep(0.1)
except Exception as e:
- print(f"❌ Error processing track {i+1}: {e}")
+ print(f"Error processing track {i+1}: {e}")
# Add error result
result = {
'tidal_track': {
@@ -33203,15 +33203,15 @@ def _run_tidal_discovery_worker(playlist_id):
# Add activity for discovery completion
source_label = discovery_source.upper()
- add_activity_item("✅", f"Tidal Discovery Complete ({source_label})", f"'{playlist.name}' - {successful_discoveries}/{len(playlist.tracks)} tracks found", "Now")
+ add_activity_item("", f"Tidal Discovery Complete ({source_label})", f"'{playlist.name}' - {successful_discoveries}/{len(playlist.tracks)} tracks found", "Now")
- print(f"✅ Tidal discovery complete ({source_label}): {successful_discoveries}/{len(playlist.tracks)} tracks found")
+ print(f"Tidal discovery complete ({source_label}): {successful_discoveries}/{len(playlist.tracks)} tracks found")
# Sync discovery results back to mirrored playlist
_sync_discovery_results_to_mirrored('tidal', playlist_id, state.get('discovery_results', []), discovery_source, profile_id=state.get('_profile_id', 1))
except Exception as e:
- print(f"❌ Error in Tidal discovery worker: {e}")
+ print(f"Error in Tidal discovery worker: {e}")
state['phase'] = 'error'
state['status'] = f'error: {str(e)}'
finally:
@@ -33249,7 +33249,7 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
source_duration = getattr(tidal_track, 'duration_ms', 0) or 0
source_name = "Spotify" if use_spotify else _get_metadata_fallback_source().capitalize()
- print(f"🔍 Tidal track: '{artist_name}' - '{track_name}' (searching {source_name})")
+ print(f"Tidal track: '{artist_name}' - '{track_name}' (searching {source_name})")
# Use matching engine to generate search queries (with fallback)
try:
@@ -33259,9 +33259,9 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
'album': None
})()
search_queries = matching_engine.generate_download_queries(temp_track)
- print(f"🔍 Generated {len(search_queries)} search queries for Tidal track")
+ print(f"Generated {len(search_queries)} search queries for Tidal track")
except Exception as e:
- print(f"⚠️ Matching engine failed for Tidal, falling back to basic queries: {e}")
+ print(f"Matching engine failed for Tidal, falling back to basic queries: {e}")
if use_spotify:
search_queries = [
f'track:"{track_name}" artist:"{artist_name}"',
@@ -33282,7 +33282,7 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
for query_idx, search_query in enumerate(search_queries):
try:
- print(f"🔍 Tidal query {query_idx + 1}/{len(search_queries)}: {search_query} ({source_name})")
+ print(f"Tidal query {query_idx + 1}/{len(search_queries)}: {search_query} ({source_name})")
if use_spotify and not _spotify_rate_limited():
results = spotify_client.search_tracks(search_query, limit=10)
@@ -33306,19 +33306,19 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
best_match_raw = _cache.get_entity('spotify', 'track', match.id)
else:
best_match_raw = None
- print(f"✅ New best Tidal match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"New best Tidal match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
if best_confidence >= 0.9:
- print(f"🎯 High confidence Tidal match found ({best_confidence:.3f}), stopping search")
+ print(f"High confidence Tidal match found ({best_confidence:.3f}), stopping search")
break
except Exception as e:
- print(f"❌ Error in Tidal {source_name} search for query '{search_query}': {e}")
+ print(f"Error in Tidal {source_name} search for query '{search_query}': {e}")
continue
# Strategy 4: Extended search with higher limit (last resort)
if not best_match:
- print(f"🔄 Tidal Strategy 4: Extended search with limit=50")
+ print(f"Tidal Strategy 4: Extended search with limit=50")
query = f"{artist_name} {track_name}"
if use_spotify:
extended_results = spotify_client.search_tracks(query, limit=50)
@@ -33331,17 +33331,17 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
if match and confidence >= min_confidence:
best_match = match
best_confidence = confidence
- print(f"✅ Strategy 4 Tidal match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 4 Tidal match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
if best_match:
if use_spotify:
- print(f"✅ Final Tidal Spotify match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})")
+ print(f"Final Tidal Spotify match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})")
return (best_match, best_match_raw, best_confidence)
else:
result_artists = best_match.artists if hasattr(best_match, 'artists') else []
result_artist = result_artists[0] if result_artists else 'Unknown'
result_name = best_match.name if hasattr(best_match, 'name') else 'Unknown'
- print(f"✅ Final Tidal {source_name} match: {result_artist} - {result_name} (confidence: {best_confidence:.3f})")
+ print(f"Final Tidal {source_name} match: {result_artist} - {result_name} (confidence: {best_confidence:.3f})")
album_name = best_match.album if hasattr(best_match, 'album') else 'Unknown Album'
image_url = best_match.image_url if hasattr(best_match, 'image_url') else ''
@@ -33378,11 +33378,11 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
if detailed:
track_number = detailed.get('track_number')
disc_number = detailed.get('disc_number')
- print(f"🔢 [Discovery Enrich] {result_name}: track_number={track_number}, disc={disc_number}")
+ print(f"[Discovery Enrich] {result_name}: track_number={track_number}, disc={disc_number}")
else:
- print(f"⚠️ [Discovery Enrich] get_track_details returned None for ID {track_id} ({result_name})")
+ print(f"[Discovery Enrich] get_track_details returned None for ID {track_id} ({result_name})")
except Exception as _enrich_err:
- print(f"⚠️ [Discovery Enrich] Failed for {result_name} (ID {track_id}): {_enrich_err}")
+ print(f"[Discovery Enrich] Failed for {result_name} (ID {track_id}): {_enrich_err}")
result_data = {
'id': track_id,
@@ -33399,11 +33399,11 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client
result_data['disc_number'] = disc_number
return result_data
else:
- print(f"❌ No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})")
+ print(f"No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})")
return None
except Exception as e:
- print(f"❌ Error searching Spotify for Tidal track: {e}")
+ print(f"Error searching Spotify for Tidal track: {e}")
return None
@@ -33440,7 +33440,7 @@ def convert_tidal_results_to_spotify_tracks(discovery_results):
}
spotify_tracks.append(track)
- print(f"🔄 Converted {len(spotify_tracks)} Tidal matches to Spotify tracks for sync")
+ print(f"Converted {len(spotify_tracks)} Tidal matches to Spotify tracks for sync")
return spotify_tracks
@@ -33472,7 +33472,7 @@ def start_tidal_sync(playlist_id):
playlist_name = state['playlist'].name # Tidal playlist object has .name attribute
# Add activity for sync start
- add_activity_item("🔄", "Tidal Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
+ add_activity_item("", "Tidal Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
# Update Tidal state
state['phase'] = 'syncing'
@@ -33493,11 +33493,11 @@ def start_tidal_sync(playlist_id):
future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id())
active_sync_workers[sync_playlist_id] = future
- print(f"🔄 Started Tidal sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ print(f"Started Tidal sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
return jsonify({"success": True, "sync_playlist_id": sync_playlist_id})
except Exception as e:
- print(f"❌ Error starting Tidal sync: {e}")
+ print(f"Error starting Tidal sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/sync/status/', methods=['GET'])
@@ -33533,17 +33533,17 @@ def get_tidal_sync_status(playlist_id):
# Add activity for sync completion
playlist = state.get('playlist')
playlist_name = playlist.name if playlist and hasattr(playlist, 'name') else 'Unknown Playlist'
- add_activity_item("🔄", "Sync Complete", f"Tidal playlist '{playlist_name}' synced successfully", "Now")
+ add_activity_item("", "Sync Complete", f"Tidal playlist '{playlist_name}' synced successfully", "Now")
elif sync_state.get('status') == 'error':
state['phase'] = 'discovered' # Revert on error
playlist = state.get('playlist')
playlist_name = playlist.name if playlist and hasattr(playlist, 'name') else 'Unknown Playlist'
- add_activity_item("❌", "Sync Failed", f"Tidal playlist '{playlist_name}' sync failed", "Now")
+ add_activity_item("", "Sync Failed", f"Tidal playlist '{playlist_name}' sync failed", "Now")
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Tidal sync status: {e}")
+ print(f"Error getting Tidal sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/sync/cancel/', methods=['POST'])
@@ -33574,7 +33574,7 @@ def cancel_tidal_sync(playlist_id):
return jsonify({"success": True, "message": "Tidal sync cancelled"})
except Exception as e:
- print(f"❌ Error cancelling Tidal sync: {e}")
+ print(f"Error cancelling Tidal sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -33675,7 +33675,7 @@ def get_deezer_arl_playlists():
'sync_status': 'Never Synced',
})
- print(f"🎵 Loaded {len(playlist_data)} Deezer user playlists via ARL")
+ print(f"Loaded {len(playlist_data)} Deezer user playlists via ARL")
return jsonify(playlist_data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -33695,7 +33695,7 @@ def get_deezer_arl_playlist_tracks(playlist_id):
if not playlist:
return jsonify({'error': 'Playlist not found or unable to access.'}), 404
- print(f"🎵 Loaded {len(playlist.get('tracks', []))} tracks from Deezer playlist: {playlist.get('name')}")
+ print(f"Loaded {len(playlist.get('tracks', []))} tracks from Deezer playlist: {playlist.get('name')}")
return jsonify(playlist)
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -33721,7 +33721,7 @@ def get_deezer_playlist(playlist_id):
return jsonify(playlist)
except Exception as e:
- print(f"❌ Error fetching Deezer playlist: {e}")
+ print(f"Error fetching Deezer playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/discovery/start/', methods=['POST'])
@@ -33787,18 +33787,18 @@ def start_deezer_discovery(playlist_id):
# Add activity for discovery start
playlist_name = state['playlist']['name']
track_count = len(state['playlist']['tracks'])
- add_activity_item("🔍", "Deezer Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
+ add_activity_item("", "Deezer Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
# Start discovery worker (capture profile ID while we have Flask context)
deezer_discovery_states[playlist_id]['_profile_id'] = get_current_profile_id()
future = deezer_discovery_executor.submit(_run_deezer_discovery_worker, playlist_id)
state['discovery_future'] = future
- print(f"🔍 Started Spotify discovery for Deezer playlist: {playlist_name}")
+ print(f"Started Spotify discovery for Deezer playlist: {playlist_name}")
return jsonify({"success": True, "message": "Discovery started"})
except Exception as e:
- print(f"❌ Error starting Deezer discovery: {e}")
+ print(f"Error starting Deezer discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/discovery/status/', methods=['GET'])
@@ -33824,7 +33824,7 @@ def get_deezer_discovery_status(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Deezer discovery status: {e}")
+ print(f"Error getting Deezer discovery status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/discovery/update_match', methods=['POST'])
@@ -33853,7 +33853,7 @@ def update_deezer_discovery_match():
old_status = result.get('status')
# Update with user-selected track
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = spotify_track['name']
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
@@ -33881,10 +33881,10 @@ def update_deezer_discovery_match():
result['manual_match'] = True
# Update match count if status changed from not found/error
- if old_status != 'found' and old_status != '✅ Found':
+ if old_status != 'found' and old_status != 'Found':
state['spotify_matches'] = state.get('spotify_matches', 0) + 1
- print(f"✅ Manual match updated: deezer - {identifier} - track {track_index}")
+ print(f"Manual match updated: deezer - {identifier} - track {track_index}")
print(f" → {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
@@ -33915,14 +33915,14 @@ def update_deezer_discovery_match():
cache_key[0], cache_key[1], 'spotify', 1.0, matched_data,
original_name, original_artist
)
- print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}")
+ print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}")
except Exception as cache_err:
- print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}")
+ print(f"Error saving manual fix to discovery cache: {cache_err}")
return jsonify({'success': True, 'result': result})
except Exception as e:
- print(f"❌ Error updating Deezer discovery match: {e}")
+ print(f"Error updating Deezer discovery match: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/deezer/playlists/states', methods=['GET'])
@@ -33949,11 +33949,11 @@ def get_deezer_playlist_states():
}
states.append(state_info)
- print(f"🎵 Returning {len(states)} stored Deezer playlist states for hydration")
+ print(f"Returning {len(states)} stored Deezer playlist states for hydration")
return jsonify({"states": states})
except Exception as e:
- print(f"❌ Error getting Deezer playlist states: {e}")
+ print(f"Error getting Deezer playlist states: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/state/', methods=['GET'])
@@ -33986,7 +33986,7 @@ def get_deezer_playlist_state(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Deezer playlist state: {e}")
+ print(f"Error getting Deezer playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/reset/', methods=['POST'])
@@ -34015,11 +34015,11 @@ def reset_deezer_playlist(playlist_id):
state['discovery_future'] = None
state['last_accessed'] = time.time()
- print(f"🔄 Reset Deezer playlist to fresh: {playlist_id}")
+ print(f"Reset Deezer playlist to fresh: {playlist_id}")
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
except Exception as e:
- print(f"❌ Error resetting Deezer playlist: {e}")
+ print(f"Error resetting Deezer playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/delete/', methods=['POST'])
@@ -34038,11 +34038,11 @@ def delete_deezer_playlist(playlist_id):
# Remove from state dictionary
del deezer_discovery_states[playlist_id]
- print(f"🗑️ Deleted Deezer playlist state: {playlist_id}")
+ print(f"Deleted Deezer playlist state: {playlist_id}")
return jsonify({"success": True, "message": "Playlist deleted"})
except Exception as e:
- print(f"❌ Error deleting Deezer playlist: {e}")
+ print(f"Error deleting Deezer playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/update_phase/', methods=['POST'])
@@ -34075,11 +34075,11 @@ def update_deezer_playlist_phase(playlist_id):
if 'converted_spotify_playlist_id' in data:
state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id']
- print(f"🔄 Updated Deezer playlist {playlist_id} phase: {old_phase} → {new_phase}")
+ print(f"Updated Deezer playlist {playlist_id} phase: {old_phase} → {new_phase}")
return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase})
except Exception as e:
- print(f"❌ Error updating Deezer playlist phase: {e}")
+ print(f"Error updating Deezer playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@@ -34100,7 +34100,7 @@ def _run_deezer_discovery_worker(playlist_id):
if not use_spotify:
itunes_client_instance = _get_metadata_fallback_client()
- print(f"🎵 Starting Deezer discovery for: {playlist['name']} (using {discovery_source.upper()})")
+ print(f"Starting Deezer discovery for: {playlist['name']} (using {discovery_source.upper()})")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -34119,7 +34119,7 @@ def _run_deezer_discovery_worker(playlist_id):
track_album = deezer_track.get('album', '')
track_duration_ms = deezer_track.get('duration_ms', 0)
- print(f"🔍 [{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}")
+ print(f"[{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}")
# Check discovery cache first
cache_key = _get_discovery_cache_key(track_name, track_artists[0] if track_artists else '')
@@ -34127,7 +34127,7 @@ def _run_deezer_discovery_worker(playlist_id):
cache_db = get_database()
cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source)
if cached_match and _validate_discovery_cache_artist(track_artists[0] if track_artists else '', cached_match):
- print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}")
+ print(f"CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}")
# Extract display-friendly artist string from cached match
cached_artists = cached_match.get('artists', [])
if cached_artists:
@@ -34150,7 +34150,7 @@ def _run_deezer_discovery_worker(playlist_id):
},
'spotify_data': cached_match,
'match_data': cached_match,
- 'status': '✅ Found',
+ 'status': 'Found',
'status_class': 'found',
'spotify_track': cached_match.get('name', ''),
'spotify_artist': cached_artist_str,
@@ -34165,7 +34165,7 @@ def _run_deezer_discovery_worker(playlist_id):
state['discovery_progress'] = int(((i + 1) / len(tracks)) * 100)
continue
except Exception as cache_err:
- print(f"⚠️ Cache lookup error: {cache_err}")
+ print(f"Cache lookup error: {cache_err}")
# Create a SimpleNamespace duck-type object for _search_spotify_for_tidal_track
track_ns = types.SimpleNamespace(
@@ -34194,7 +34194,7 @@ def _run_deezer_discovery_worker(playlist_id):
},
'spotify_data': None,
'match_data': None,
- 'status': '❌ Not Found',
+ 'status': 'Not Found',
'status_class': 'not-found',
'spotify_track': '',
'spotify_artist': '',
@@ -34237,7 +34237,7 @@ def _run_deezer_discovery_worker(playlist_id):
match_data['disc_number'] = raw_track_data['disc_number']
result['spotify_data'] = match_data
result['match_data'] = match_data
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = track_obj.name
result['spotify_artist'] = ', '.join(track_obj.artists) if isinstance(track_obj.artists, list) else str(track_obj.artists)
@@ -34259,7 +34259,7 @@ def _run_deezer_discovery_worker(playlist_id):
match_data['image_url'] = _fb_images[0].get('url', '')
result['spotify_data'] = match_data
result['match_data'] = match_data
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = match_data.get('name', '')
itunes_artists = match_data.get('artists', [])
@@ -34279,9 +34279,9 @@ def _run_deezer_discovery_worker(playlist_id):
result['match_data'], track_name,
track_artists[0] if track_artists else ''
)
- print(f"💾 CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})")
+ print(f"CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})")
except Exception as cache_err:
- print(f"⚠️ Cache save error: {cache_err}")
+ print(f"Cache save error: {cache_err}")
result['index'] = i
state['discovery_results'].append(result)
@@ -34291,7 +34291,7 @@ def _run_deezer_discovery_worker(playlist_id):
time.sleep(0.1)
except Exception as e:
- print(f"❌ Error processing track {i+1}: {e}")
+ print(f"Error processing track {i+1}: {e}")
# Add error result
result = {
'deezer_track': {
@@ -34300,7 +34300,7 @@ def _run_deezer_discovery_worker(playlist_id):
},
'spotify_data': None,
'match_data': None,
- 'status': '❌ Error',
+ 'status': 'Error',
'status_class': 'error',
'spotify_track': '',
'spotify_artist': '',
@@ -34319,15 +34319,15 @@ def _run_deezer_discovery_worker(playlist_id):
# Add activity for discovery completion
source_label = discovery_source.upper()
- add_activity_item("✅", f"Deezer Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now")
+ add_activity_item("", f"Deezer Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now")
- print(f"✅ Deezer discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found")
+ print(f"Deezer discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found")
# Sync discovery results back to mirrored playlist
_sync_discovery_results_to_mirrored('deezer', playlist_id, state.get('discovery_results', []), discovery_source, profile_id=state.get('_profile_id', 1))
except Exception as e:
- print(f"❌ Error in Deezer discovery worker: {e}")
+ print(f"Error in Deezer discovery worker: {e}")
if playlist_id in deezer_discovery_states:
deezer_discovery_states[playlist_id]['phase'] = 'error'
deezer_discovery_states[playlist_id]['status'] = f'error: {str(e)}'
@@ -34366,7 +34366,7 @@ def convert_deezer_results_to_spotify_tracks(discovery_results):
}
spotify_tracks.append(track)
- print(f"🔄 Converted {len(spotify_tracks)} Deezer matches to Spotify tracks for sync")
+ print(f"Converted {len(spotify_tracks)} Deezer matches to Spotify tracks for sync")
return spotify_tracks
@@ -34398,7 +34398,7 @@ def start_deezer_sync(playlist_id):
playlist_name = state['playlist']['name']
# Add activity for sync start
- add_activity_item("🔄", "Deezer Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
+ add_activity_item("", "Deezer Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
# Update Deezer state
state['phase'] = 'syncing'
@@ -34419,11 +34419,11 @@ def start_deezer_sync(playlist_id):
future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id())
active_sync_workers[sync_playlist_id] = future
- print(f"🔄 Started Deezer sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ print(f"Started Deezer sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
return jsonify({"success": True, "sync_playlist_id": sync_playlist_id})
except Exception as e:
- print(f"❌ Error starting Deezer sync: {e}")
+ print(f"Error starting Deezer sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/sync/status/', methods=['GET'])
@@ -34457,16 +34457,16 @@ def get_deezer_sync_status(playlist_id):
state['phase'] = 'sync_complete'
state['sync_progress'] = sync_state.get('progress', {})
playlist_name = state['playlist']['name']
- add_activity_item("🔄", "Sync Complete", f"Deezer playlist '{playlist_name}' synced successfully", "Now")
+ add_activity_item("", "Sync Complete", f"Deezer playlist '{playlist_name}' synced successfully", "Now")
elif sync_state.get('status') == 'error':
state['phase'] = 'discovered' # Revert on error
playlist_name = state['playlist']['name']
- add_activity_item("❌", "Sync Failed", f"Deezer playlist '{playlist_name}' sync failed", "Now")
+ add_activity_item("", "Sync Failed", f"Deezer playlist '{playlist_name}' sync failed", "Now")
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Deezer sync status: {e}")
+ print(f"Error getting Deezer sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/sync/cancel/', methods=['POST'])
@@ -34497,7 +34497,7 @@ def cancel_deezer_sync(playlist_id):
return jsonify({"success": True, "message": "Deezer sync cancelled"})
except Exception as e:
- print(f"❌ Error cancelling Deezer sync: {e}")
+ print(f"Error cancelling Deezer sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -34525,7 +34525,7 @@ def parse_spotify_public_endpoint():
if not parsed:
return jsonify({"error": "Invalid Spotify URL. Please use a playlist or album link from open.spotify.com"}), 400
- print(f"🎵 Scraping public Spotify {parsed['type']}: {parsed['id']}")
+ print(f"Scraping public Spotify {parsed['type']}: {parsed['id']}")
result = scrape_spotify_embed(parsed['type'], parsed['id'])
@@ -34584,11 +34584,11 @@ def parse_spotify_public_endpoint():
spotify_public_discovery_states[url_hash]['playlist'] = response_data
spotify_public_discovery_states[url_hash]['last_accessed'] = time.time()
- print(f"✅ Spotify {parsed['type']} scraped: {result['name']} ({len(spotify_tracks)} tracks)")
+ print(f"Spotify {parsed['type']} scraped: {result['name']} ({len(spotify_tracks)} tracks)")
return jsonify(response_data)
except Exception as e:
- print(f"❌ Error parsing Spotify URL: {e}")
+ print(f"Error parsing Spotify URL: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -34617,17 +34617,17 @@ def start_spotify_public_discovery(url_hash):
# Add activity for discovery start
playlist_name = state['playlist']['name']
track_count = len(state['playlist']['tracks'])
- add_activity_item("🔍", "Spotify Link Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
+ add_activity_item("", "Spotify Link Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
# Start discovery worker
future = spotify_public_discovery_executor.submit(_run_spotify_public_discovery_worker, url_hash)
state['discovery_future'] = future
- print(f"🔍 Started Spotify discovery for Spotify Public playlist: {playlist_name}")
+ print(f"Started Spotify discovery for Spotify Public playlist: {playlist_name}")
return jsonify({"success": True, "message": "Discovery started"})
except Exception as e:
- print(f"❌ Error starting Spotify Public discovery: {e}")
+ print(f"Error starting Spotify Public discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/discovery/status/', methods=['GET'])
@@ -34653,7 +34653,7 @@ def get_spotify_public_discovery_status(url_hash):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Spotify Public discovery status: {e}")
+ print(f"Error getting Spotify Public discovery status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/discovery/update_match', methods=['POST'])
@@ -34682,7 +34682,7 @@ def update_spotify_public_discovery_match():
old_status = result.get('status')
# Update with user-selected track
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = spotify_track['name']
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
@@ -34710,10 +34710,10 @@ def update_spotify_public_discovery_match():
result['manual_match'] = True
# Update match count if status changed from not found/error
- if old_status != 'found' and old_status != '✅ Found':
+ if old_status != 'found' and old_status != 'Found':
state['spotify_matches'] = state.get('spotify_matches', 0) + 1
- print(f"✅ Manual match updated: spotify_public - {identifier} - track {track_index}")
+ print(f"Manual match updated: spotify_public - {identifier} - track {track_index}")
print(f" → {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
@@ -34744,14 +34744,14 @@ def update_spotify_public_discovery_match():
cache_key[0], cache_key[1], 'spotify', 1.0, matched_data,
original_name, original_artist
)
- print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}")
+ print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}")
except Exception as cache_err:
- print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}")
+ print(f"Error saving manual fix to discovery cache: {cache_err}")
return jsonify({'success': True, 'result': result})
except Exception as e:
- print(f"❌ Error updating Spotify Public discovery match: {e}")
+ print(f"Error updating Spotify Public discovery match: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/spotify-public/playlists/states', methods=['GET'])
@@ -34778,11 +34778,11 @@ def get_spotify_public_playlist_states():
}
states.append(state_info)
- print(f"🎵 Returning {len(states)} stored Spotify Public playlist states for hydration")
+ print(f"Returning {len(states)} stored Spotify Public playlist states for hydration")
return jsonify({"states": states})
except Exception as e:
- print(f"❌ Error getting Spotify Public playlist states: {e}")
+ print(f"Error getting Spotify Public playlist states: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/state/', methods=['GET'])
@@ -34814,7 +34814,7 @@ def get_spotify_public_playlist_state(url_hash):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Spotify Public playlist state: {e}")
+ print(f"Error getting Spotify Public playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/reset/', methods=['POST'])
@@ -34843,11 +34843,11 @@ def reset_spotify_public_playlist(url_hash):
state['discovery_future'] = None
state['last_accessed'] = time.time()
- print(f"🔄 Reset Spotify Public playlist to fresh: {url_hash}")
+ print(f"Reset Spotify Public playlist to fresh: {url_hash}")
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
except Exception as e:
- print(f"❌ Error resetting Spotify Public playlist: {e}")
+ print(f"Error resetting Spotify Public playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/delete/', methods=['POST'])
@@ -34866,11 +34866,11 @@ def delete_spotify_public_playlist(url_hash):
# Remove from state dictionary
del spotify_public_discovery_states[url_hash]
- print(f"🗑️ Deleted Spotify Public playlist state: {url_hash}")
+ print(f"Deleted Spotify Public playlist state: {url_hash}")
return jsonify({"success": True, "message": "Playlist deleted"})
except Exception as e:
- print(f"❌ Error deleting Spotify Public playlist: {e}")
+ print(f"Error deleting Spotify Public playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/update_phase/', methods=['POST'])
@@ -34903,11 +34903,11 @@ def update_spotify_public_playlist_phase(url_hash):
if 'converted_spotify_playlist_id' in data:
state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id']
- print(f"🔄 Updated Spotify Public playlist {url_hash} phase: {old_phase} → {new_phase}")
+ print(f"Updated Spotify Public playlist {url_hash} phase: {old_phase} → {new_phase}")
return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase})
except Exception as e:
- print(f"❌ Error updating Spotify Public playlist phase: {e}")
+ print(f"Error updating Spotify Public playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@@ -34928,7 +34928,7 @@ def _run_spotify_public_discovery_worker(url_hash):
if not use_spotify:
itunes_client_instance = _get_metadata_fallback_client()
- print(f"🎵 Starting Spotify Public discovery for: {playlist['name']} (using {discovery_source.upper()})")
+ print(f"Starting Spotify Public discovery for: {playlist['name']} (using {discovery_source.upper()})")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -34958,7 +34958,7 @@ def _run_spotify_public_discovery_worker(url_hash):
track_album_name = track_album or ''
track_duration_ms = sp_track.get('duration_ms', 0)
- print(f"🔍 [{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}")
+ print(f"[{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}")
# Check discovery cache first
cache_key = _get_discovery_cache_key(track_name, track_artists[0] if track_artists else '')
@@ -34966,7 +34966,7 @@ def _run_spotify_public_discovery_worker(url_hash):
cache_db = get_database()
cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source)
if cached_match and _validate_discovery_cache_artist(track_artists[0] if track_artists else '', cached_match):
- print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}")
+ print(f"CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}")
# Extract display-friendly artist string from cached match
cached_artists = cached_match.get('artists', [])
if cached_artists:
@@ -34989,7 +34989,7 @@ def _run_spotify_public_discovery_worker(url_hash):
},
'spotify_data': cached_match,
'match_data': cached_match,
- 'status': '✅ Found',
+ 'status': 'Found',
'status_class': 'found',
'spotify_track': cached_match.get('name', ''),
'spotify_artist': cached_artist_str,
@@ -35004,7 +35004,7 @@ def _run_spotify_public_discovery_worker(url_hash):
state['discovery_progress'] = int(((i + 1) / len(tracks)) * 100)
continue
except Exception as cache_err:
- print(f"⚠️ Cache lookup error: {cache_err}")
+ print(f"Cache lookup error: {cache_err}")
# Create a SimpleNamespace duck-type object for _search_spotify_for_tidal_track
track_ns = types.SimpleNamespace(
@@ -35033,7 +35033,7 @@ def _run_spotify_public_discovery_worker(url_hash):
},
'spotify_data': None,
'match_data': None,
- 'status': '❌ Not Found',
+ 'status': 'Not Found',
'status_class': 'not-found',
'spotify_track': '',
'spotify_artist': '',
@@ -35076,7 +35076,7 @@ def _run_spotify_public_discovery_worker(url_hash):
match_data['disc_number'] = raw_track_data['disc_number']
result['spotify_data'] = match_data
result['match_data'] = match_data
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = track_obj.name
result['spotify_artist'] = ', '.join(track_obj.artists) if isinstance(track_obj.artists, list) else str(track_obj.artists)
@@ -35098,7 +35098,7 @@ def _run_spotify_public_discovery_worker(url_hash):
match_data['image_url'] = _fb_images[0].get('url', '')
result['spotify_data'] = match_data
result['match_data'] = match_data
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = match_data.get('name', '')
itunes_artists = match_data.get('artists', [])
@@ -35118,9 +35118,9 @@ def _run_spotify_public_discovery_worker(url_hash):
result['match_data'], track_name,
track_artists[0] if track_artists else ''
)
- print(f"💾 CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})")
+ print(f"CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})")
except Exception as cache_err:
- print(f"⚠️ Cache save error: {cache_err}")
+ print(f"Cache save error: {cache_err}")
result['index'] = i
state['discovery_results'].append(result)
@@ -35130,7 +35130,7 @@ def _run_spotify_public_discovery_worker(url_hash):
time.sleep(0.1)
except Exception as e:
- print(f"❌ Error processing track {i+1}: {e}")
+ print(f"Error processing track {i+1}: {e}")
# Add error result
result = {
'spotify_public_track': {
@@ -35139,7 +35139,7 @@ def _run_spotify_public_discovery_worker(url_hash):
},
'spotify_data': None,
'match_data': None,
- 'status': '❌ Error',
+ 'status': 'Error',
'status_class': 'error',
'spotify_track': '',
'spotify_artist': '',
@@ -35158,12 +35158,12 @@ def _run_spotify_public_discovery_worker(url_hash):
# Add activity for discovery completion
source_label = discovery_source.upper()
- add_activity_item("✅", f"Spotify Link Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now")
+ add_activity_item("", f"Spotify Link Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now")
- print(f"✅ Spotify Public discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found")
+ print(f"Spotify Public discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found")
except Exception as e:
- print(f"❌ Error in Spotify Public discovery worker: {e}")
+ print(f"Error in Spotify Public discovery worker: {e}")
if url_hash in spotify_public_discovery_states:
spotify_public_discovery_states[url_hash]['phase'] = 'error'
spotify_public_discovery_states[url_hash]['status'] = f'error: {str(e)}'
@@ -35203,7 +35203,7 @@ def convert_spotify_public_results_to_spotify_tracks(discovery_results):
}
spotify_tracks.append(track)
- print(f"🔄 Converted {len(spotify_tracks)} Spotify Public matches to Spotify tracks for sync")
+ print(f"Converted {len(spotify_tracks)} Spotify Public matches to Spotify tracks for sync")
return spotify_tracks
@@ -35235,7 +35235,7 @@ def start_spotify_public_sync(url_hash):
playlist_name = state['playlist']['name']
# Add activity for sync start
- add_activity_item("🔄", "Spotify Link Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
+ add_activity_item("", "Spotify Link Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
# Update Spotify Public state
state['phase'] = 'syncing'
@@ -35256,11 +35256,11 @@ def start_spotify_public_sync(url_hash):
future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id())
active_sync_workers[sync_playlist_id] = future
- print(f"🔄 Started Spotify Public sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ print(f"Started Spotify Public sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
return jsonify({"success": True, "sync_playlist_id": sync_playlist_id})
except Exception as e:
- print(f"❌ Error starting Spotify Public sync: {e}")
+ print(f"Error starting Spotify Public sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/sync/status/', methods=['GET'])
@@ -35294,16 +35294,16 @@ def get_spotify_public_sync_status(url_hash):
state['phase'] = 'sync_complete'
state['sync_progress'] = sync_state.get('progress', {})
playlist_name = state['playlist']['name']
- add_activity_item("🔄", "Sync Complete", f"Spotify Link playlist '{playlist_name}' synced successfully", "Now")
+ add_activity_item("", "Sync Complete", f"Spotify Link playlist '{playlist_name}' synced successfully", "Now")
elif sync_state.get('status') == 'error':
state['phase'] = 'discovered' # Revert on error
playlist_name = state['playlist']['name']
- add_activity_item("❌", "Sync Failed", f"Spotify Link playlist '{playlist_name}' sync failed", "Now")
+ add_activity_item("", "Sync Failed", f"Spotify Link playlist '{playlist_name}' sync failed", "Now")
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Spotify Public sync status: {e}")
+ print(f"Error getting Spotify Public sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/sync/cancel/', methods=['POST'])
@@ -35334,7 +35334,7 @@ def cancel_spotify_public_sync(url_hash):
return jsonify({"success": True, "message": "Spotify Public sync cancelled"})
except Exception as e:
- print(f"❌ Error cancelling Spotify Public sync: {e}")
+ print(f"Error cancelling Spotify Public sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -35368,7 +35368,7 @@ def parse_youtube_playlist_endpoint():
if not ('youtube.com/playlist' in url or 'music.youtube.com/playlist' in url):
return jsonify({"error": "Invalid YouTube playlist URL"}), 400
- print(f"🎬 Parsing YouTube playlist: {url}")
+ print(f"Parsing YouTube playlist: {url}")
# Parse the playlist using our function
playlist_data = parse_youtube_playlist(url)
@@ -35433,11 +35433,11 @@ def parse_youtube_playlist_endpoint():
playlist_data['url_hash'] = url_hash
- print(f"✅ YouTube playlist parsed successfully: {playlist_data['name']} ({len(playlist_data['tracks'])} tracks)")
+ print(f"YouTube playlist parsed successfully: {playlist_data['name']} ({len(playlist_data['tracks'])} tracks)")
return jsonify(playlist_data)
except Exception as e:
- print(f"❌ Error parsing YouTube playlist: {e}")
+ print(f"Error parsing YouTube playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/discovery/start/', methods=['POST'])
@@ -35467,17 +35467,17 @@ def start_youtube_discovery(url_hash):
# Add activity for discovery start
playlist_name = state['playlist']['name']
track_count = len(state['playlist']['tracks'])
- add_activity_item("🔍", "YouTube Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
+ add_activity_item("", "YouTube Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
# Start discovery worker
future = youtube_discovery_executor.submit(_run_youtube_discovery_worker, url_hash)
state['discovery_future'] = future
- print(f"🔍 Started Spotify discovery for YouTube playlist: {state['playlist']['name']}")
+ print(f"Started Spotify discovery for YouTube playlist: {state['playlist']['name']}")
return jsonify({"success": True, "message": "Discovery started"})
except Exception as e:
- print(f"❌ Error starting YouTube discovery: {e}")
+ print(f"Error starting YouTube discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/discovery/status/', methods=['GET'])
@@ -35503,7 +35503,7 @@ def get_youtube_discovery_status(url_hash):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting YouTube discovery status: {e}")
+ print(f"Error getting YouTube discovery status: {e}")
return jsonify({"error": str(e)}), 500
@@ -35533,7 +35533,7 @@ def update_youtube_discovery_match():
old_status = result.get('status')
# Update with user-selected track
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = spotify_track['name']
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
@@ -35561,10 +35561,10 @@ def update_youtube_discovery_match():
result['manual_match'] = True # Flag for tracking
# Update match count if status changed from not found/error
- if old_status != 'found' and old_status != '✅ Found':
+ if old_status != 'found' and old_status != 'Found':
state['spotify_matches'] = state.get('spotify_matches', 0) + 1
- print(f"✅ Manual match updated: youtube - {identifier} - track {track_index}")
+ print(f"Manual match updated: youtube - {identifier} - track {track_index}")
print(f" → {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
@@ -35599,9 +35599,9 @@ def update_youtube_discovery_match():
cache_key[0], cache_key[1], 'spotify', 1.0, matched_data,
original_name, original_artist
)
- print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}")
+ print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}")
except Exception as cache_err:
- print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}")
+ print(f"Error saving manual fix to discovery cache: {cache_err}")
# Persist manual fix to DB for mirrored playlists
if identifier.startswith('mirrored_'):
@@ -35620,14 +35620,14 @@ def update_youtube_discovery_match():
}
db.update_mirrored_track_extra_data(db_track_id, extra_data)
result['matched_data'] = matched_data
- print(f"💾 Persisted manual fix to DB for track {db_track_id}")
+ print(f"Persisted manual fix to DB for track {db_track_id}")
except Exception as wb_err:
- print(f"⚠️ Error persisting manual fix to DB: {wb_err}")
+ print(f"Error persisting manual fix to DB: {wb_err}")
return jsonify({'success': True, 'result': result})
except Exception as e:
- print(f"❌ Error updating YouTube discovery match: {e}")
+ print(f"Error updating YouTube discovery match: {e}")
return jsonify({'error': str(e)}), 500
@@ -35647,7 +35647,7 @@ def _run_youtube_discovery_worker(url_hash):
# Get fallback client
itunes_client = _get_metadata_fallback_client()
- print(f"🔍 Starting {discovery_source} discovery for {len(tracks)} YouTube tracks...")
+ print(f"Starting {discovery_source} discovery for {len(tracks)} YouTube tracks...")
# Store the discovery source in state
state['discovery_source'] = discovery_source
@@ -35657,7 +35657,7 @@ def _run_youtube_discovery_worker(url_hash):
try:
# Check for cancellation (phase changed by reset/delete/close)
if state.get('phase') != 'discovering':
- print(f"🛑 Discovery cancelled for {url_hash} (phase changed to '{state.get('phase')}')")
+ print(f"Discovery cancelled for {url_hash} (phase changed to '{state.get('phase')}')")
return
# Update progress
@@ -35671,7 +35671,7 @@ def _run_youtube_discovery_worker(url_hash):
cleaned_title = track['name']
cleaned_artist = track['artists'][0] if track['artists'] else 'Unknown Artist'
- print(f"🔍 Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'")
+ print(f"Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'")
# Check discovery cache first
cache_key = _get_discovery_cache_key(cleaned_title, cleaned_artist)
@@ -35679,12 +35679,12 @@ def _run_youtube_discovery_worker(url_hash):
cache_db = get_database()
cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source)
if cached_match and _validate_discovery_cache_artist(cleaned_artist, cached_match):
- print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}")
+ print(f"CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}")
result = {
'index': i,
'yt_track': cleaned_title,
'yt_artist': cleaned_artist,
- 'status': '✅ Found',
+ 'status': 'Found',
'status_class': 'found',
'spotify_track': cached_match.get('name', ''),
'spotify_artist': _extract_artist_name(cached_match.get('artists', [''])[0]) if cached_match.get('artists') else '',
@@ -35698,7 +35698,7 @@ def _run_youtube_discovery_worker(url_hash):
state['discovery_results'].append(result)
continue
except Exception as cache_err:
- print(f"⚠️ Cache lookup error: {cache_err}")
+ print(f"Cache lookup error: {cache_err}")
# Try multiple search strategies using matching engine
matched_track = None
@@ -35715,14 +35715,14 @@ def _run_youtube_discovery_worker(url_hash):
'album': None
})()
search_queries = matching_engine.generate_download_queries(temp_track)
- print(f"🔍 Generated {len(search_queries)} search queries for YouTube track")
+ print(f"Generated {len(search_queries)} search queries for YouTube track")
except Exception as e:
- print(f"⚠️ Matching engine failed for YouTube, falling back to basic query: {e}")
+ print(f"Matching engine failed for YouTube, falling back to basic query: {e}")
search_queries = [f"{cleaned_artist} {cleaned_title}", cleaned_title]
for query_idx, search_query in enumerate(search_queries):
try:
- print(f"🔍 YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}")
+ print(f"YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}")
search_results = None
@@ -35747,22 +35747,22 @@ def _run_youtube_discovery_worker(url_hash):
best_raw_track = _cache.get_entity('spotify', 'track', match.id)
else:
best_raw_track = None
- print(f"✅ New best YouTube match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"New best YouTube match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
if best_confidence >= 0.9:
- print(f"🎯 High confidence YouTube match found ({best_confidence:.3f}), stopping search")
+ print(f"High confidence YouTube match found ({best_confidence:.3f}), stopping search")
break
except Exception as e:
- print(f"❌ Error in YouTube search for query '{search_query}': {e}")
+ print(f"Error in YouTube search for query '{search_query}': {e}")
continue
if matched_track:
- print(f"✅ Strategy 1 YouTube match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})")
+ print(f"Strategy 1 YouTube match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})")
# Strategy 2: Swapped search (if first failed) - score results properly
if not matched_track:
- print("🔄 YouTube Strategy 2: Trying swapped search (artist/title reversed)")
+ print("YouTube Strategy 2: Trying swapped search (artist/title reversed)")
if use_spotify:
query = f"artist:{cleaned_title} track:{cleaned_artist}"
fallback_results = spotify_client.search_tracks(query, limit=5)
@@ -35776,13 +35776,13 @@ def _run_youtube_discovery_worker(url_hash):
if match and confidence >= min_confidence:
matched_track = match
best_confidence = confidence
- print(f"✅ Strategy 2 YouTube match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 2 YouTube match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Strategy 3: Raw data search (if still failed) - score results properly
if not matched_track:
raw_title = track.get('raw_title', cleaned_title)
raw_artist = track.get('raw_artist', cleaned_artist)
- print(f"🔄 YouTube Strategy 3: Trying raw data search: '{raw_artist} {raw_title}'")
+ print(f"YouTube Strategy 3: Trying raw data search: '{raw_artist} {raw_title}'")
query = f"{raw_artist} {raw_title}"
if use_spotify:
fallback_results = spotify_client.search_tracks(query, limit=5)
@@ -35795,11 +35795,11 @@ def _run_youtube_discovery_worker(url_hash):
if match and confidence >= min_confidence:
matched_track = match
best_confidence = confidence
- print(f"✅ Strategy 3 YouTube match (raw): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 3 YouTube match (raw): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Strategy 4: Extended search with higher limit (last resort)
if not matched_track:
- print(f"🔄 YouTube Strategy 4: Extended search with limit=50")
+ print(f"YouTube Strategy 4: Extended search with limit=50")
query = f"{cleaned_artist} {cleaned_title}"
if use_spotify:
extended_results = spotify_client.search_tracks(query, limit=50)
@@ -35812,14 +35812,14 @@ def _run_youtube_discovery_worker(url_hash):
if match and confidence >= min_confidence:
matched_track = match
best_confidence = confidence
- print(f"✅ Strategy 4 YouTube match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 4 YouTube match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Create result entry
result = {
'index': i,
'yt_track': cleaned_title,
'yt_artist': cleaned_artist,
- 'status': '✅ Found' if matched_track else '❌ Not Found',
+ 'status': 'Found' if matched_track else 'Not Found',
'status_class': 'found' if matched_track else 'not-found',
'spotify_track': matched_track.name if matched_track else '',
'spotify_artist': _extract_artist_name(matched_track.artists[0]) if matched_track else '',
@@ -35866,21 +35866,21 @@ def _run_youtube_discovery_worker(url_hash):
cache_key[0], cache_key[1], discovery_source, best_confidence,
result['matched_data'], cleaned_title, cleaned_artist
)
- print(f"💾 CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})")
+ print(f"CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})")
except Exception as cache_err:
- print(f"⚠️ Cache save error: {cache_err}")
+ print(f"Cache save error: {cache_err}")
state['discovery_results'].append(result)
- print(f" {'✅' if matched_track else '❌'} Track {i+1}/{len(tracks)}: {result['status']}")
+ print(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}")
except Exception as e:
- print(f"❌ Error processing track {i}: {e}")
+ print(f"Error processing track {i}: {e}")
result = {
'index': i,
'yt_track': track['name'],
'yt_artist': track['artists'][0] if track['artists'] else 'Unknown',
- 'status': '❌ Error',
+ 'status': 'Error',
'status_class': 'error',
'spotify_track': '',
'spotify_artist': '',
@@ -35927,18 +35927,18 @@ def _run_youtube_discovery_worker(url_hash):
'provider': discovery_source,
}
db.update_mirrored_track_extra_data(db_track_id, extra_data)
- print(f"💾 Wrote discovery results to DB for {url_hash}")
+ print(f"Wrote discovery results to DB for {url_hash}")
except Exception as wb_err:
- print(f"⚠️ Error writing discovery results to DB: {wb_err}")
+ print(f"Error writing discovery results to DB: {wb_err}")
playlist_name = playlist['name']
source_label = discovery_source.upper()
- add_activity_item("✅", f"YouTube Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now")
+ add_activity_item("", f"YouTube Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now")
- print(f"✅ YouTube discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched")
+ print(f"YouTube discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched")
except Exception as e:
- print(f"❌ Error in YouTube discovery worker: {e}")
+ print(f"Error in YouTube discovery worker: {e}")
state['status'] = 'error'
state['phase'] = 'fresh'
finally:
@@ -35961,7 +35961,7 @@ def _run_listenbrainz_discovery_worker(state_key):
# Get fallback client
itunes_client = _get_metadata_fallback_client()
- print(f"🔍 Starting {discovery_source} discovery for {len(tracks)} ListenBrainz tracks...")
+ print(f"Starting {discovery_source} discovery for {len(tracks)} ListenBrainz tracks...")
# Store the discovery source in state
state['discovery_source'] = discovery_source
@@ -35971,7 +35971,7 @@ def _run_listenbrainz_discovery_worker(state_key):
try:
# Check for cancellation
if state.get('phase') != 'discovering':
- print(f"🛑 ListenBrainz discovery cancelled (phase changed to '{state.get('phase')}')")
+ print(f"ListenBrainz discovery cancelled (phase changed to '{state.get('phase')}')")
return
# Update progress
@@ -35983,7 +35983,7 @@ def _run_listenbrainz_discovery_worker(state_key):
album_name = track.get('album_name', '')
duration_ms = track.get('duration_ms', 0)
- print(f"🔍 Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'")
+ print(f"Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'")
# Check discovery cache first
cache_key = _get_discovery_cache_key(cleaned_title, cleaned_artist)
@@ -35991,12 +35991,12 @@ def _run_listenbrainz_discovery_worker(state_key):
cache_db = get_database()
cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source)
if cached_match and _validate_discovery_cache_artist(cleaned_artist, cached_match):
- print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}")
+ print(f"CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}")
result = {
'index': i,
'lb_track': cleaned_title,
'lb_artist': cleaned_artist,
- 'status': '✅ Found',
+ 'status': 'Found',
'status_class': 'found',
'spotify_track': cached_match.get('name', ''),
'spotify_artist': _extract_artist_name(cached_match.get('artists', [''])[0]) if cached_match.get('artists') else '',
@@ -36010,7 +36010,7 @@ def _run_listenbrainz_discovery_worker(state_key):
state['discovery_results'].append(result)
continue
except Exception as cache_err:
- print(f"⚠️ Cache lookup error: {cache_err}")
+ print(f"Cache lookup error: {cache_err}")
# Try multiple search strategies using matching engine
matched_track = None
@@ -36027,14 +36027,14 @@ def _run_listenbrainz_discovery_worker(state_key):
'album': album_name if album_name else None
})()
search_queries = matching_engine.generate_download_queries(temp_track)
- print(f"🔍 Generated {len(search_queries)} search queries for ListenBrainz track")
+ print(f"Generated {len(search_queries)} search queries for ListenBrainz track")
except Exception as e:
- print(f"⚠️ Matching engine failed for ListenBrainz, falling back to basic query: {e}")
+ print(f"Matching engine failed for ListenBrainz, falling back to basic query: {e}")
search_queries = [f"{cleaned_artist} {cleaned_title}", cleaned_title]
for query_idx, search_query in enumerate(search_queries):
try:
- print(f"🔍 ListenBrainz query {query_idx + 1}/{len(search_queries)}: {search_query}")
+ print(f"ListenBrainz query {query_idx + 1}/{len(search_queries)}: {search_query}")
search_results = None
@@ -36059,22 +36059,22 @@ def _run_listenbrainz_discovery_worker(state_key):
best_raw_track = _cache.get_entity('spotify', 'track', match.id)
else:
best_raw_track = None
- print(f"✅ New best ListenBrainz match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"New best ListenBrainz match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
if best_confidence >= 0.9:
- print(f"🎯 High confidence ListenBrainz match found ({best_confidence:.3f}), stopping search")
+ print(f"High confidence ListenBrainz match found ({best_confidence:.3f}), stopping search")
break
except Exception as e:
- print(f"❌ Error in ListenBrainz search for query '{search_query}': {e}")
+ print(f"Error in ListenBrainz search for query '{search_query}': {e}")
continue
if matched_track:
- print(f"✅ Strategy 1 ListenBrainz match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})")
+ print(f"Strategy 1 ListenBrainz match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})")
# Strategy 2: Swapped search (if first failed) - score results properly
if not matched_track:
- print("🔄 ListenBrainz Strategy 2: Trying swapped search (artist/title reversed)")
+ print("ListenBrainz Strategy 2: Trying swapped search (artist/title reversed)")
if use_spotify:
query = f"artist:{cleaned_title} track:{cleaned_artist}"
fallback_results = spotify_client.search_tracks(query, limit=5)
@@ -36088,11 +36088,11 @@ def _run_listenbrainz_discovery_worker(state_key):
if match and confidence >= min_confidence:
matched_track = match
best_confidence = confidence
- print(f"✅ Strategy 2 ListenBrainz match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 2 ListenBrainz match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Strategy 3: Album-based search (if still failed and we have album name) - score results properly
if not matched_track and album_name:
- print(f"🔄 ListenBrainz Strategy 3: Trying album-based search: '{cleaned_artist} {album_name} {cleaned_title}'")
+ print(f"ListenBrainz Strategy 3: Trying album-based search: '{cleaned_artist} {album_name} {cleaned_title}'")
if use_spotify:
query = f"artist:{cleaned_artist} album:{album_name} track:{cleaned_title}"
fallback_results = spotify_client.search_tracks(query, limit=5)
@@ -36106,11 +36106,11 @@ def _run_listenbrainz_discovery_worker(state_key):
if match and confidence >= min_confidence:
matched_track = match
best_confidence = confidence
- print(f"✅ Strategy 3 ListenBrainz match (album): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 3 ListenBrainz match (album): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Strategy 4: Extended search with higher limit (last resort)
if not matched_track:
- print(f"🔄 ListenBrainz Strategy 4: Extended search with limit=50")
+ print(f"ListenBrainz Strategy 4: Extended search with limit=50")
query = f"{cleaned_artist} {cleaned_title}"
if use_spotify:
extended_results = spotify_client.search_tracks(query, limit=50)
@@ -36123,14 +36123,14 @@ def _run_listenbrainz_discovery_worker(state_key):
if match and confidence >= min_confidence:
matched_track = match
best_confidence = confidence
- print(f"✅ Strategy 4 ListenBrainz match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 4 ListenBrainz match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Create result entry
result = {
'index': i,
'lb_track': cleaned_title,
'lb_artist': cleaned_artist,
- 'status': '✅ Found' if matched_track else '❌ Not Found',
+ 'status': 'Found' if matched_track else 'Not Found',
'status_class': 'found' if matched_track else 'not-found',
'spotify_track': matched_track.name if matched_track else '',
'spotify_artist': _extract_artist_name(matched_track.artists[0]) if matched_track else '',
@@ -36177,21 +36177,21 @@ def _run_listenbrainz_discovery_worker(state_key):
cache_key[0], cache_key[1], discovery_source, best_confidence,
result['matched_data'], cleaned_title, cleaned_artist
)
- print(f"💾 CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})")
+ print(f"CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})")
except Exception as cache_err:
- print(f"⚠️ Cache save error: {cache_err}")
+ print(f"Cache save error: {cache_err}")
state['discovery_results'].append(result)
- print(f" {'✅' if matched_track else '❌'} Track {i+1}/{len(tracks)}: {result['status']}")
+ print(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}")
except Exception as e:
- print(f"❌ Error processing track {i}: {e}")
+ print(f"Error processing track {i}: {e}")
result = {
'index': i,
'lb_track': track['track_name'],
'lb_artist': track['artist_name'],
- 'status': '❌ Error',
+ 'status': 'Error',
'status_class': 'error',
'spotify_track': '',
'spotify_artist': '',
@@ -36207,12 +36207,12 @@ def _run_listenbrainz_discovery_worker(state_key):
playlist_name = playlist['name']
source_label = discovery_source.upper()
- add_activity_item("✅", f"ListenBrainz Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now")
+ add_activity_item("", f"ListenBrainz Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now")
- print(f"✅ ListenBrainz discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched")
+ print(f"ListenBrainz discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched")
except Exception as e:
- print(f"❌ Error in ListenBrainz discovery worker: {e}")
+ print(f"Error in ListenBrainz discovery worker: {e}")
state['status'] = 'error'
state['phase'] = 'fresh'
finally:
@@ -36266,7 +36266,7 @@ def start_youtube_sync(url_hash):
playlist_name = state['playlist']['name']
# Add activity for sync start
- add_activity_item("🔄", "YouTube Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
+ add_activity_item("", "YouTube Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
# Update YouTube state
state['phase'] = 'syncing'
@@ -36287,11 +36287,11 @@ def start_youtube_sync(url_hash):
future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id())
active_sync_workers[sync_playlist_id] = future
- print(f"🔄 Started YouTube sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ print(f"Started YouTube sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
return jsonify({"success": True, "sync_playlist_id": sync_playlist_id})
except Exception as e:
- print(f"❌ Error starting YouTube sync: {e}")
+ print(f"Error starting YouTube sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/sync/status/', methods=['GET'])
@@ -36326,16 +36326,16 @@ def get_youtube_sync_status(url_hash):
state['sync_progress'] = sync_state.get('progress', {})
# Add activity for sync completion
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
- add_activity_item("🔄", "Sync Complete", f"YouTube playlist '{playlist_name}' synced successfully", "Now")
+ add_activity_item("", "Sync Complete", f"YouTube playlist '{playlist_name}' synced successfully", "Now")
elif sync_state.get('status') == 'error':
state['phase'] = 'discovered' # Revert on error
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
- add_activity_item("❌", "Sync Failed", f"YouTube playlist '{playlist_name}' sync failed", "Now")
+ add_activity_item("", "Sync Failed", f"YouTube playlist '{playlist_name}' sync failed", "Now")
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting YouTube sync status: {e}")
+ print(f"Error getting YouTube sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/sync/cancel/', methods=['POST'])
@@ -36366,7 +36366,7 @@ def cancel_youtube_sync(url_hash):
return jsonify({"success": True, "message": "YouTube sync cancelled"})
except Exception as e:
- print(f"❌ Error cancelling YouTube sync: {e}")
+ print(f"Error cancelling YouTube sync: {e}")
return jsonify({"error": str(e)}), 500
# New YouTube Playlist Management Endpoints (for persistent state)
@@ -36402,11 +36402,11 @@ def get_all_youtube_playlists():
}
playlists.append(playlist_info)
- print(f"📋 Returning {len(playlists)} stored YouTube playlists for hydration")
+ print(f"Returning {len(playlists)} stored YouTube playlists for hydration")
return jsonify({"playlists": playlists})
except Exception as e:
- print(f"❌ Error getting YouTube playlists: {e}")
+ print(f"Error getting YouTube playlists: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/state/', methods=['GET'])
@@ -36440,7 +36440,7 @@ def get_youtube_playlist_state(url_hash):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting YouTube playlist state: {e}")
+ print(f"Error getting YouTube playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/reset/', methods=['POST'])
@@ -36468,11 +36468,11 @@ def reset_youtube_playlist(url_hash):
state['discovery_future'] = None
state['last_accessed'] = time.time()
- print(f"🔄 Reset YouTube playlist to fresh phase: {state['playlist']['name']}")
+ print(f"Reset YouTube playlist to fresh phase: {state['playlist']['name']}")
return jsonify({"success": True, "message": "Playlist reset to fresh state"})
except Exception as e:
- print(f"❌ Error resetting YouTube playlist: {e}")
+ print(f"Error resetting YouTube playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/delete/', methods=['DELETE'])
@@ -36492,11 +36492,11 @@ def delete_youtube_playlist(url_hash):
playlist_name = state['playlist']['name']
del youtube_playlist_states[url_hash]
- print(f"🗑️ Deleted YouTube playlist from backend: {playlist_name}")
+ print(f"Deleted YouTube playlist from backend: {playlist_name}")
return jsonify({"success": True, "message": f"Playlist '{playlist_name}' deleted"})
except Exception as e:
- print(f"❌ Error deleting YouTube playlist: {e}")
+ print(f"Error deleting YouTube playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/update_phase/', methods=['POST'])
@@ -36521,11 +36521,11 @@ def update_youtube_playlist_phase(url_hash):
state['phase'] = new_phase
state['last_accessed'] = time.time()
- print(f"🔄 Updated YouTube playlist {url_hash} phase: {old_phase} → {new_phase}")
+ print(f"Updated YouTube playlist {url_hash} phase: {old_phase} → {new_phase}")
return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase})
except Exception as e:
- print(f"❌ Error updating YouTube playlist phase: {e}")
+ print(f"Error updating YouTube playlist phase: {e}")
return jsonify({"error": str(e)}), 500
def convert_youtube_results_to_spotify_tracks(discovery_results):
@@ -36561,7 +36561,7 @@ def convert_youtube_results_to_spotify_tracks(discovery_results):
}
spotify_tracks.append(track)
- print(f"🔄 Converted {len(spotify_tracks)} YouTube matches to Spotify tracks for sync")
+ print(f"Converted {len(spotify_tracks)} YouTube matches to Spotify tracks for sync")
return spotify_tracks
@@ -36572,8 +36572,8 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
global sync_states, sync_service
task_start_time = time.time()
- print(f"🚀 [TIMING] _run_sync_task STARTED for playlist '{playlist_name}' at {time.strftime('%H:%M:%S')}")
- print(f"📊 Received {len(tracks_json)} tracks from frontend")
+ print(f"[TIMING] _run_sync_task STARTED for playlist '{playlist_name}' at {time.strftime('%H:%M:%S')}")
+ print(f"Received {len(tracks_json)} tracks from frontend")
# Record sync history start (skip for re-syncs triggered from history)
_is_resync = playlist_id.startswith('resync_')
@@ -36600,7 +36600,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
try:
# Recreate a Playlist object from the JSON data sent by the frontend
# This avoids needing to re-fetch it from Spotify
- print(f"🔄 Converting JSON tracks to SpotifyTrack objects...")
+ print(f"Converting JSON tracks to SpotifyTrack objects...")
# Store original track data with full album objects (for wishlist with cover art)
# Normalize formats for wishlist: album must be dict {'name': ...}, artists must be [{'name': ...}]
@@ -36668,7 +36668,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
if i < 3: # Log first 3 tracks for debugging
print(f" Track {i+1}: '{track.name}' by {track.artists}")
- print(f"✅ Created {len(tracks)} SpotifyTrack objects")
+ print(f"Created {len(tracks)} SpotifyTrack objects")
playlist = SpotifyPlaylist(
id=playlist_id,
@@ -36680,7 +36680,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
tracks=tracks,
total_tracks=len(tracks)
)
- print(f"✅ Created SpotifyPlaylist object: '{playlist.name}' with {playlist.total_tracks} tracks")
+ print(f"Created SpotifyPlaylist object: '{playlist.name}' with {playlist.total_tracks} tracks")
first_callback_time = [None] # Use list to allow modification in nested function
@@ -36691,15 +36691,15 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
first_callback_duration = (first_callback_time[0] - task_start_time) * 1000
print(f"⏱️ [TIMING] FIRST progress callback at {time.strftime('%H:%M:%S')} (took {first_callback_duration:.1f}ms from start)")
- print(f"⚡ PROGRESS CALLBACK: {progress.current_step} - {progress.current_track}")
- print(f" 📊 Progress: {progress.progress}% ({progress.matched_tracks}/{progress.total_tracks} matched, {progress.failed_tracks} failed)")
+ print(f"PROGRESS CALLBACK: {progress.current_step} - {progress.current_track}")
+ print(f" Progress: {progress.progress}% ({progress.matched_tracks}/{progress.total_tracks} matched, {progress.failed_tracks} failed)")
with sync_lock:
sync_states[playlist_id] = {
"status": "syncing",
"progress": progress.__dict__ # Convert dataclass to dict
}
- print(f" ✅ Updated sync_states for {playlist_id}")
+ print(f" Updated sync_states for {playlist_id}")
# Update automation progress card
if automation_id:
@@ -36719,7 +36719,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
log_line=f'{track} — {step}' if track else step, log_type=log_type)
except Exception as setup_error:
- print(f"❌ SETUP ERROR in _run_sync_task: {setup_error}")
+ print(f"SETUP ERROR in _run_sync_task: {setup_error}")
import traceback
traceback.print_exc()
with sync_lock:
@@ -36733,7 +36733,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
return
try:
- print(f"🔧 Setting up sync service...")
+ print(f"Setting up sync service...")
print(f" sync_service available: {sync_service is not None}")
if sync_service is None:
@@ -36762,24 +36762,24 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
db = MusicDatabase()
print(f" Database initialized: {db is not None}")
except Exception as db_error:
- print(f" ❌ Database initialization failed: {db_error}")
+ print(f" Database initialization failed: {db_error}")
- print(f"🔄 Attaching progress callback...")
+ print(f"Attaching progress callback...")
# Attach the progress callback
sync_service.set_progress_callback(progress_callback, playlist.name)
- print(f"✅ Progress callback attached for playlist: {playlist.name}")
+ print(f"Progress callback attached for playlist: {playlist.name}")
# CRITICAL FIX: Add database-only fallback for web context
# If media client is not connected, patch the sync service to use database-only matching
if media_client is None or not media_client.is_connected():
- print(f"⚠️ Media client not connected - patching sync service for database-only matching")
+ print(f"Media client not connected - patching sync service for database-only matching")
# Store original method
original_find_track = sync_service._find_track_in_media_server
# Create database-only replacement method
async def database_only_find_track(spotify_track):
- print(f"🗃️ Database-only search for: '{spotify_track.name}' by {spotify_track.artists}")
+ print(f"Database-only search for: '{spotify_track.name}' by {spotify_track.artists}")
try:
from database.music_database import MusicDatabase
from config.settings import config_manager
@@ -36801,9 +36801,9 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
self.ratingKey = db_t.id
self.title = db_t.title
self.id = db_t.id
- print(f"⚡ Sync cache hit: '{original_title}' → server track {cached['server_track_id']}")
+ print(f"Sync cache hit: '{original_title}' → server track {cached['server_track_id']}")
return DatabaseTrackCached(db_track_check), cached['confidence']
- print(f"🔄 Sync cache stale for '{original_title}' — track gone")
+ print(f"Sync cache stale for '{original_title}' — track gone")
except Exception:
pass
# --- End cache fast-path ---
@@ -36825,7 +36825,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
)
if db_track and confidence >= 0.80:
- print(f"✅ Database match: '{db_track.title}' (confidence: {confidence:.2f})")
+ print(f"Database match: '{db_track.title}' (confidence: {confidence:.2f})")
# Save to sync match cache
if spotify_id:
@@ -36848,21 +36848,21 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
return DatabaseTrackMock(db_track), confidence
- print(f"❌ No database match found for: '{original_title}'")
+ print(f"No database match found for: '{original_title}'")
return None, 0.0
except Exception as e:
- print(f"❌ Database search error: {e}")
+ print(f"Database search error: {e}")
return None, 0.0
# Patch the method
sync_service._find_track_in_media_server = database_only_find_track
- print(f"✅ Patched sync service to use database-only matching")
+ print(f"Patched sync service to use database-only matching")
sync_start_time = time.time()
setup_duration = (sync_start_time - task_start_time) * 1000
print(f"⏱️ [TIMING] Setup completed at {time.strftime('%H:%M:%S')} (took {setup_duration:.1f}ms)")
- print(f"🚀 Starting actual sync process with run_async()...")
+ print(f"Starting actual sync process with run_async()...")
# Attach original tracks map to sync_service for wishlist with album images
sync_service._original_tracks_map = original_tracks_map
@@ -36883,7 +36883,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
sync_duration = (time.time() - sync_start_time) * 1000
total_duration = (time.time() - task_start_time) * 1000
print(f"⏱️ [TIMING] Sync completed at {time.strftime('%H:%M:%S')} (sync: {sync_duration:.1f}ms, total: {total_duration:.1f}ms)")
- print(f"✅ Sync process completed! Result type: {type(result)}")
+ print(f"Sync process completed! Result type: {type(result)}")
print(f" Result details: matched={getattr(result, 'matched_tracks', 'N/A')}, total={getattr(result, 'total_tracks', 'N/A')}")
# Update final state on completion
@@ -36909,7 +36909,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
"progress": result_dict,
"result": result_dict
}
- print(f"🏁 Sync finished for {playlist_id} - state updated")
+ print(f"Sync finished for {playlist_id} - state updated")
# Set playlist poster image if available (Plex, Jellyfin, Emby)
if playlist_image_url and getattr(result, 'synced_tracks', 0) > 0:
@@ -36921,7 +36921,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
jellyfin_client.set_playlist_image(playlist_name, playlist_image_url)
# Navidrome doesn't support custom playlist images
except Exception as img_err:
- print(f"⚠️ Could not set playlist image: {img_err}")
+ print(f"Could not set playlist image: {img_err}")
# Record sync history completion with per-track data
try:
@@ -36948,11 +36948,11 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
try:
track_results_json = json.dumps(match_details, default=str)
saved = db.update_sync_history_track_results(target_batch_id, track_results_json)
- print(f"📝 [Sync History] Saved {len(match_details)} track results for batch {target_batch_id} (saved={saved})")
+ print(f"[Sync History] Saved {len(match_details)} track results for batch {target_batch_id} (saved={saved})")
except Exception as json_err:
- print(f"⚠️ [Sync History] Failed to serialize track results: {json_err}")
+ print(f"[Sync History] Failed to serialize track results: {json_err}")
else:
- print(f"⚠️ [Sync History] No match_details on SyncResult for batch {target_batch_id}")
+ print(f"[Sync History] No match_details on SyncResult for batch {target_batch_id}")
except Exception as e:
logger.warning(f"Failed to record sync history completion: {e}")
@@ -36989,7 +36989,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
tracks_hash=_tracks_hash)
except Exception as e:
- print(f"❌ SYNC FAILED for {playlist_id}: {e}")
+ print(f"SYNC FAILED for {playlist_id}: {e}")
import traceback
traceback.print_exc()
with sync_lock:
@@ -37001,14 +37001,14 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
_update_automation_progress(automation_id, status='error', progress=100,
phase='Error', log_line=f'Sync failed: {str(e)}', log_type='error')
finally:
- print(f"🧹 Cleaning up progress callback for {playlist.name}")
+ print(f"Cleaning up progress callback for {playlist.name}")
# Clean up the callback
if sync_service:
sync_service.clear_progress_callback(playlist.name)
# Clean up original tracks map
if hasattr(sync_service, '_original_tracks_map'):
del sync_service._original_tracks_map
- print(f"✅ Cleanup completed for {playlist_id}")
+ print(f"Cleanup completed for {playlist_id}")
@app.route('/api/sync/start', methods=['POST'])
@@ -37027,9 +37027,9 @@ def start_playlist_sync():
return jsonify({"success": False, "error": "Missing playlist_id, name, or tracks."}), 400
# Add activity for sync start
- add_activity_item("🔄", "Spotify Sync Started", f"'{playlist_name}' - {len(tracks_json)} tracks", "Now")
+ add_activity_item("", "Spotify Sync Started", f"'{playlist_name}' - {len(tracks_json)} tracks", "Now")
- logger.info(f"🔄 Starting playlist sync for '{playlist_name}' with {len(tracks_json)} tracks")
+ logger.info(f"Starting playlist sync for '{playlist_name}' with {len(tracks_json)} tracks")
logger.debug(f"Request parsed at {time.strftime('%H:%M:%S')} (took {(time.time()-request_start_time)*1000:.1f}ms)")
with sync_lock:
@@ -37102,25 +37102,25 @@ def cancel_playlist_sync():
def test_database_access():
"""Test endpoint to verify database connectivity for sync operations"""
try:
- print(f"🧪 Testing database access for sync operations...")
+ print(f"Testing database access for sync operations...")
# Test database initialization
from database.music_database import MusicDatabase
db = MusicDatabase()
- print(f" ✅ Database initialized: {db is not None}")
+ print(f" Database initialized: {db is not None}")
# Test basic database query
stats = db.get_database_info_for_server()
- print(f" ✅ Database stats retrieved: {stats}")
+ print(f" Database stats retrieved: {stats}")
# Test track existence check (like sync service does)
db_track, confidence = db.check_track_exists("test track", "test artist", confidence_threshold=0.7)
- print(f" ✅ Track existence check works: found={db_track is not None}, confidence={confidence}")
+ print(f" Track existence check works: found={db_track is not None}, confidence={confidence}")
# Test config manager
from config.settings import config_manager
active_server = config_manager.get_active_media_server()
- print(f" ✅ Active media server: {active_server}")
+ print(f" Active media server: {active_server}")
# Test media clients
print(f" Media clients status:")
@@ -37144,7 +37144,7 @@ def test_database_access():
})
except Exception as e:
- print(f" ❌ Database test failed: {e}")
+ print(f" Database test failed: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37173,7 +37173,7 @@ def save_discover_download_snapshot():
db.save_bubble_snapshot('discover_downloads', downloads, profile_id=get_current_profile_id())
download_count = len(downloads)
- print(f"📸 Saved discover download snapshot: {download_count} downloads")
+ print(f"Saved discover download snapshot: {download_count} downloads")
return jsonify({
'success': True,
@@ -37182,7 +37182,7 @@ def save_discover_download_snapshot():
})
except Exception as e:
- print(f"❌ Error saving discover download snapshot: {e}")
+ print(f"Error saving discover download snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37218,7 +37218,7 @@ def hydrate_discover_downloads():
snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00'))
cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff:
- print(f"🧹 Cleaning up old discover download snapshot from {snapshot_time}")
+ print(f"Cleaning up old discover download snapshot from {snapshot_time}")
db.delete_bubble_snapshot('discover_downloads', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37226,7 +37226,7 @@ def hydrate_discover_downloads():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"⚠️ Error checking discover snapshot age: {e}")
+ print(f"Error checking discover snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -37242,11 +37242,11 @@ def hydrate_discover_downloads():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"⚠️ Error fetching active processes for discover download hydration: {e}")
+ print(f"Error fetching active processes for discover download hydration: {e}")
# If no active processes exist, the app likely restarted - clean up snapshots
if not current_processes:
- print(f"🧹 No active processes found - app likely restarted, cleaning up discover download snapshot")
+ print(f"No active processes found - app likely restarted, cleaning up discover download snapshot")
db.delete_bubble_snapshot('discover_downloads', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37261,11 +37261,11 @@ def hydrate_discover_downloads():
if playlist_id in current_processes:
process_info = current_processes[playlist_id]
live_status = 'in_progress'
- print(f"🔄 Found active process for discover download {playlist_id}: {process_info['phase']}")
+ print(f"Found active process for discover download {playlist_id}: {process_info['phase']}")
else:
# No active process - likely completed
live_status = 'completed'
- print(f"✅ No active process for discover download {playlist_id} - marking as completed")
+ print(f"No active process for discover download {playlist_id} - marking as completed")
# Create updated download entry
hydrated_downloads[playlist_id] = {
@@ -37281,7 +37281,7 @@ def hydrate_discover_downloads():
active_count = sum(1 for d in hydrated_downloads.values() if d['status'] == 'in_progress')
completed_count = sum(1 for d in hydrated_downloads.values() if d['status'] == 'completed')
- print(f"✅ Hydrated {download_count} discover downloads: {active_count} active, {completed_count} completed")
+ print(f"Hydrated {download_count} discover downloads: {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -37294,7 +37294,7 @@ def hydrate_discover_downloads():
})
except Exception as e:
- print(f"❌ Error hydrating discover downloads: {e}")
+ print(f"Error hydrating discover downloads: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37322,7 +37322,7 @@ def save_artist_bubble_snapshot():
db.save_bubble_snapshot('artist_bubbles', bubbles, profile_id=get_current_profile_id())
bubble_count = len(bubbles)
- print(f"📸 Saved artist bubble snapshot: {bubble_count} artists")
+ print(f"Saved artist bubble snapshot: {bubble_count} artists")
return jsonify({
'success': True,
@@ -37331,7 +37331,7 @@ def save_artist_bubble_snapshot():
})
except Exception as e:
- print(f"❌ Error saving artist bubble snapshot: {e}")
+ print(f"Error saving artist bubble snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37367,7 +37367,7 @@ def hydrate_artist_bubbles():
snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00'))
cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff:
- print(f"🧹 Cleaning up old snapshot from {snapshot_time}")
+ print(f"Cleaning up old snapshot from {snapshot_time}")
db.delete_bubble_snapshot('artist_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37375,7 +37375,7 @@ def hydrate_artist_bubbles():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"⚠️ Error checking snapshot age: {e}")
+ print(f"Error checking snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -37391,11 +37391,11 @@ def hydrate_artist_bubbles():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"⚠️ Error fetching active processes for hydration: {e}")
+ print(f"Error fetching active processes for hydration: {e}")
# If no active processes exist, the app likely restarted - clean up snapshots
if not current_processes:
- print(f"🧹 No active processes found - app likely restarted, cleaning up snapshot")
+ print(f"No active processes found - app likely restarted, cleaning up snapshot")
db.delete_bubble_snapshot('artist_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37419,11 +37419,11 @@ def hydrate_artist_bubbles():
if virtual_playlist_id in current_processes:
process_info = current_processes[virtual_playlist_id]
live_status = 'in_progress'
- print(f"🔄 Found active process for {download['album']['name']}: {process_info['phase']}")
+ print(f"Found active process for {download['album']['name']}: {process_info['phase']}")
else:
# No active process - likely completed
live_status = 'view_results'
- print(f"✅ No active process for {download['album']['name']} - marking as completed")
+ print(f"No active process for {download['album']['name']} - marking as completed")
# Create updated download entry
updated_download = {
@@ -37452,7 +37452,7 @@ def hydrate_artist_bubbles():
for download in bubble['downloads']
if download['status'] == 'view_results')
- print(f"🔄 Hydrated {bubble_count} artist bubbles: {active_count} active, {completed_count} completed")
+ print(f"Hydrated {bubble_count} artist bubbles: {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -37466,7 +37466,7 @@ def hydrate_artist_bubbles():
})
except Exception as e:
- print(f"❌ Error hydrating artist bubbles: {e}")
+ print(f"Error hydrating artist bubbles: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37494,7 +37494,7 @@ def save_search_bubble_snapshot():
db.save_bubble_snapshot('search_bubbles', bubbles, profile_id=get_current_profile_id())
bubble_count = len(bubbles)
- print(f"📸 Saved search bubble snapshot: {bubble_count} albums/tracks")
+ print(f"Saved search bubble snapshot: {bubble_count} albums/tracks")
return jsonify({
'success': True,
@@ -37503,7 +37503,7 @@ def save_search_bubble_snapshot():
})
except Exception as e:
- print(f"❌ Error saving search bubble snapshot: {e}")
+ print(f"Error saving search bubble snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37539,7 +37539,7 @@ def hydrate_search_bubbles():
snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00'))
cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff:
- print(f"🧹 Cleaning up old search snapshot from {snapshot_time}")
+ print(f"Cleaning up old search snapshot from {snapshot_time}")
db.delete_bubble_snapshot('search_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37547,7 +37547,7 @@ def hydrate_search_bubbles():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"⚠️ Error checking snapshot age: {e}")
+ print(f"Error checking snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -37563,11 +37563,11 @@ def hydrate_search_bubbles():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"⚠️ Error fetching active processes for hydration: {e}")
+ print(f"Error fetching active processes for hydration: {e}")
# If no active processes exist, the app likely restarted - clean up snapshots
if not current_processes:
- print(f"🧹 No active processes found - app likely restarted, cleaning up search snapshot")
+ print(f"No active processes found - app likely restarted, cleaning up search snapshot")
db.delete_bubble_snapshot('search_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37590,11 +37590,11 @@ def hydrate_search_bubbles():
if virtual_playlist_id in current_processes:
process_info = current_processes[virtual_playlist_id]
live_status = 'in_progress'
- print(f"🔄 Found active process for {download['item']['name']}: {process_info['phase']}")
+ print(f"Found active process for {download['item']['name']}: {process_info['phase']}")
else:
# No active process - likely completed
live_status = 'view_results'
- print(f"✅ No active process for {download['item']['name']} - marking as completed")
+ print(f"No active process for {download['item']['name']} - marking as completed")
# Create updated download entry
updated_download = {
@@ -37619,7 +37619,7 @@ def hydrate_search_bubbles():
for download in bubble['downloads']
if download['status'] == 'view_results')
- print(f"🔄 Hydrated {bubble_count} search bubbles (artists): {active_count} active, {completed_count} completed")
+ print(f"Hydrated {bubble_count} search bubbles (artists): {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -37633,7 +37633,7 @@ def hydrate_search_bubbles():
})
except Exception as e:
- print(f"❌ Error hydrating search bubbles: {e}")
+ print(f"Error hydrating search bubbles: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37657,7 +37657,7 @@ def save_beatport_bubble_snapshot():
db.save_bubble_snapshot('beatport_bubbles', bubbles, profile_id=get_current_profile_id())
bubble_count = len(bubbles)
- print(f"📸 Saved Beatport bubble snapshot: {bubble_count} charts")
+ print(f"Saved Beatport bubble snapshot: {bubble_count} charts")
return jsonify({
'success': True,
@@ -37666,7 +37666,7 @@ def save_beatport_bubble_snapshot():
})
except Exception as e:
- print(f"❌ Error saving Beatport bubble snapshot: {e}")
+ print(f"Error saving Beatport bubble snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -37699,7 +37699,7 @@ def hydrate_beatport_bubbles():
snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00'))
cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff:
- print(f"🧹 Cleaning up old Beatport snapshot from {snapshot_time}")
+ print(f"Cleaning up old Beatport snapshot from {snapshot_time}")
db.delete_bubble_snapshot('beatport_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37707,7 +37707,7 @@ def hydrate_beatport_bubbles():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"⚠️ Error checking Beatport snapshot age: {e}")
+ print(f"Error checking Beatport snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -37723,11 +37723,11 @@ def hydrate_beatport_bubbles():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"⚠️ Error fetching active processes for Beatport hydration: {e}")
+ print(f"Error fetching active processes for Beatport hydration: {e}")
# If no active processes exist, app likely restarted — clean up
if not current_processes:
- print(f"🧹 No active processes found - cleaning up Beatport snapshot")
+ print(f"No active processes found - cleaning up Beatport snapshot")
db.delete_bubble_snapshot('beatport_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -37766,7 +37766,7 @@ def hydrate_beatport_bubbles():
completed_count = sum(1 for b in hydrated_bubbles.values()
for d in b['downloads'] if d['status'] == 'view_results')
- print(f"🔄 Hydrated {bubble_count} Beatport bubbles: {active_count} active, {completed_count} completed")
+ print(f"Hydrated {bubble_count} Beatport bubbles: {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -37779,7 +37779,7 @@ def hydrate_beatport_bubbles():
})
except Exception as e:
- print(f"❌ Error hydrating Beatport bubbles: {e}")
+ print(f"Error hydrating Beatport bubbles: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -38432,18 +38432,18 @@ def add_to_watchlist():
dc_data = dc.get_artist(artist_id)
if dc_data:
image_url = dc_data.get('image_url')
- print(f"🖼️ Discogs artist image: {image_url[:60] if image_url else 'None'}")
+ print(f"Discogs artist image: {image_url[:60] if image_url else 'None'}")
elif source == 'deezer' or fallback_source == 'deezer':
# Deezer: fetch artist image directly from API
dz_resp = requests.get(f'https://api.deezer.com/artist/{artist_id}', timeout=5)
if dz_resp.ok:
dz_data = dz_resp.json()
image_url = dz_data.get('picture_xl') or dz_data.get('picture_big') or dz_data.get('picture_medium')
- print(f"🖼️ Deezer artist image: {image_url[:60] if image_url else 'None'}")
+ print(f"Deezer artist image: {image_url[:60] if image_url else 'None'}")
else:
# iTunes: look up album entity for artwork
itunes_url = f"https://itunes.apple.com/lookup?id={artist_id}&entity=album&limit=5"
- print(f"🔍 Fetching iTunes artist image: {itunes_url}")
+ print(f"Fetching iTunes artist image: {itunes_url}")
resp = requests.get(itunes_url, timeout=5)
image_url = None
@@ -38459,11 +38459,11 @@ def add_to_watchlist():
if image_url:
database.update_watchlist_artist_image(artist_id, image_url)
- print(f"✅ Cached {fallback_source} artist image for {artist_name}")
+ print(f"Cached {fallback_source} artist image for {artist_name}")
else:
- print(f"⚠️ No artwork found for {fallback_source} artist {artist_name}")
+ print(f"No artwork found for {fallback_source} artist {artist_name}")
except Exception as fb_error:
- print(f"⚠️ Error fetching {fallback_source} artwork: {fb_error}")
+ print(f"Error fetching {fallback_source} artwork: {fb_error}")
elif spotify_client and spotify_client.is_authenticated():
# For Spotify artists, fetch from Spotify API
artist_data = spotify_client.get_artist(artist_id)
@@ -38478,16 +38478,16 @@ def add_to_watchlist():
# Update in database
if image_url:
database.update_watchlist_artist_image(artist_id, image_url)
- print(f"✅ Cached artist image for {artist_name}")
+ print(f"Cached artist image for {artist_name}")
else:
- print(f"⚠️ No image URL found for {artist_name}")
+ print(f"No image URL found for {artist_name}")
else:
- print(f"⚠️ No images in Spotify data for {artist_name}")
+ print(f"No images in Spotify data for {artist_name}")
else:
- print(f"⚠️ Spotify client not available for fetching artist image")
+ print(f"Spotify client not available for fetching artist image")
except Exception as img_error:
# Don't fail the add operation if image fetch fails
- print(f"⚠️ Could not fetch artist image for {artist_name}: {img_error}")
+ print(f"Could not fetch artist image for {artist_name}: {img_error}")
# Push updated count to this profile's WebSocket room immediately
try:
@@ -38609,7 +38609,7 @@ def add_batch_to_watchlist():
if image_url:
database.update_watchlist_artist_image(artist_id, image_url)
except Exception as img_error:
- print(f"⚠️ Could not fetch artist image for {artist_name}: {img_error}")
+ print(f"Could not fetch artist image for {artist_name}: {img_error}")
return jsonify({
"success": True,
@@ -38810,7 +38810,7 @@ def start_watchlist_scan():
with watchlist_timer_lock:
watchlist_auto_scanning = True
watchlist_auto_scanning_timestamp = time.time()
- print(f"🔒 [Manual Watchlist Scan] Flag set at timestamp {watchlist_auto_scanning_timestamp}")
+ print(f"[Manual Watchlist Scan] Flag set at timestamp {watchlist_auto_scanning_timestamp}")
# Get list of artists to scan (for the current profile)
database = get_database()
@@ -38848,10 +38848,10 @@ def start_watchlist_scan():
pass
for _bf_provider in providers_to_backfill:
try:
- print(f"🔍 Checking for missing {_bf_provider} IDs in watchlist...")
+ print(f"Checking for missing {_bf_provider} IDs in watchlist...")
scanner._backfill_missing_ids(watchlist_artists, _bf_provider)
except Exception as backfill_error:
- print(f"⚠️ Error during {_bf_provider} ID backfilling: {backfill_error}")
+ print(f"Error during {_bf_provider} ID backfilling: {backfill_error}")
# Continue with next provider
# IMAGE BACKFILL — fix watchlist artists with missing images
@@ -38869,7 +38869,7 @@ def start_watchlist_scan():
imageless = cursor.fetchall()
if imageless:
- print(f"🖼️ Backfilling images for {len(imageless)} watchlist artists...")
+ print(f"Backfilling images for {len(imageless)} watchlist artists...")
filled = 0
for row in imageless:
name = row['artist_name']
@@ -38928,9 +38928,9 @@ def start_watchlist_scan():
filled += 1
if filled:
- print(f"🖼️ Backfilled {filled}/{len(imageless)} watchlist artist images")
+ print(f"Backfilled {filled}/{len(imageless)} watchlist artist images")
except Exception as img_err:
- print(f"⚠️ Image backfill error: {img_err}")
+ print(f"Image backfill error: {img_err}")
# Initialize detailed progress tracking
watchlist_scan_state.update({
@@ -38976,7 +38976,7 @@ def start_watchlist_scan():
artist_delay = base_artist_delay
album_delay = base_album_delay
- print(f"📊 Scan parameters: {artist_count} artists, lookback={lookback_period}, "
+ print(f"Scan parameters: {artist_count} artists, lookback={lookback_period}, "
f"delays: {artist_delay:.1f}s/artist, {album_delay:.1f}s/album")
# Circuit breaker: pause scan on consecutive rate-limit failures
@@ -38987,7 +38987,7 @@ def start_watchlist_scan():
for i, artist in enumerate(watchlist_artists):
# Check for cancel request
if watchlist_scan_state.get('cancel_requested'):
- print(f"🛑 [Manual Watchlist Scan] Cancel requested after {i}/{len(watchlist_artists)} artists")
+ print(f"[Manual Watchlist Scan] Cancel requested after {i}/{len(watchlist_artists)} artists")
watchlist_scan_state['status'] = 'cancelled'
watchlist_scan_state['current_phase'] = 'cancelled'
watchlist_scan_state['summary'] = {
@@ -39124,7 +39124,7 @@ def start_watchlist_scan():
'error_message': None
})())
- print(f"✅ Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist")
+ print(f"Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist")
# Fetch similar artists for discovery feature
# This is critical for the discover page to work
@@ -39143,7 +39143,7 @@ def start_watchlist_scan():
scanner.update_similar_artists(artist, profile_id=scan_profile_id)
print(f" Similar artists updated for {artist.artist_name}")
except Exception as similar_error:
- print(f" ⚠️ Failed to update similar artists for {artist.artist_name}: {similar_error}")
+ print(f" Failed to update similar artists for {artist.artist_name}: {similar_error}")
# Delay between artists
if i < len(watchlist_artists) - 1:
@@ -39162,7 +39162,7 @@ def start_watchlist_scan():
if "429" in error_str or "rate limit" in error_str:
consecutive_failures += 1
if consecutive_failures >= CIRCUIT_BREAKER_THRESHOLD:
- print(f"🛑 Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s")
+ print(f"Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s")
watchlist_scan_state['current_phase'] = 'circuit_breaker_pause'
time.sleep(circuit_breaker_pause)
circuit_breaker_pause = min(circuit_breaker_pause * 2, 600)
@@ -39204,23 +39204,23 @@ def start_watchlist_scan():
print(f"Watchlist scan completed: {len(successful_scans)}/{len(scan_results)} artists scanned successfully")
print(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist")
else:
- print(f"🛑 Watchlist scan cancelled — skipping post-scan steps")
+ print(f"Watchlist scan cancelled — skipping post-scan steps")
# Post-scan steps — skip if cancelled
if not was_cancelled:
# Populate discovery pool from similar artists
- print("🎵 Starting discovery pool population...")
+ print("Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
try:
scanner.populate_discovery_pool(profile_id=scan_profile_id)
- print("✅ Discovery pool population complete")
+ print("Discovery pool population complete")
except Exception as discovery_error:
- print(f"⚠️ Error populating discovery pool: {discovery_error}")
+ print(f"Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
# Update ListenBrainz playlists cache
- print("🧠 Starting ListenBrainz playlists update...")
+ print("Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
try:
from core.listenbrainz_manager import ListenBrainzManager
@@ -39233,22 +39233,22 @@ def start_watchlist_scan():
lb_manager = ListenBrainzManager(db_path, profile_id=lb_prof['id'], token=lb_prof['token'], base_url=lb_prof['base_url'])
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
- print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {lb_result.get('summary', {})}")
+ print(f"ListenBrainz update complete for profile {lb_prof['id']}: {lb_result.get('summary', {})}")
else:
# Fallback: use global config token
lb_manager = ListenBrainzManager(db_path)
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
- print(f"✅ ListenBrainz update complete (global): {lb_result.get('summary', {})}")
+ print(f"ListenBrainz update complete (global): {lb_result.get('summary', {})}")
elif lb_result.get('error'):
- print(f"⚠️ ListenBrainz update skipped: {lb_result.get('error')}")
+ print(f"ListenBrainz update skipped: {lb_result.get('error')}")
except Exception as lb_error:
- print(f"⚠️ Error updating ListenBrainz: {lb_error}")
+ print(f"Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
# Update current seasonal playlist (weekly refresh)
- print("🎃 Starting seasonal content update...")
+ print("Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
try:
from core.seasonal_discovery import get_seasonal_discovery_service
@@ -39258,27 +39258,27 @@ def start_watchlist_scan():
current_season = seasonal_service.get_current_season()
if current_season:
if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7):
- print(f"🎃 Updating {current_season} seasonal content...")
+ print(f"Updating {current_season} seasonal content...")
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
- print(f"✅ {current_season.capitalize()} seasonal content updated")
+ print(f"{current_season.capitalize()} seasonal content updated")
else:
- print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping")
+ print(f"{current_season.capitalize()} seasonal content recently updated, skipping")
else:
print("ℹ️ No active season at this time")
except Exception as seasonal_error:
- print(f"⚠️ Error updating seasonal content: {seasonal_error}")
+ print(f"Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
# Sync Spotify library cache
- print("📚 Syncing Spotify library cache...")
+ print("Syncing Spotify library cache...")
watchlist_scan_state['current_phase'] = 'syncing_spotify_library'
try:
scanner.sync_spotify_library_cache(profile_id=scan_profile_id)
- print("✅ Spotify library cache sync complete")
+ print("Spotify library cache sync complete")
except Exception as lib_error:
- print(f"⚠️ Error syncing Spotify library: {lib_error}")
+ print(f"Error syncing Spotify library: {lib_error}")
except Exception as e:
print(f"Error during watchlist scan: {e}")
@@ -39299,7 +39299,7 @@ def start_watchlist_scan():
with watchlist_timer_lock:
watchlist_auto_scanning = False
watchlist_auto_scanning_timestamp = 0
- print("🔓 [Manual Watchlist Scan] Flag reset - scan complete")
+ print("[Manual Watchlist Scan] Flag reset - scan complete")
# Initialize scan state
global watchlist_scan_state
@@ -39362,7 +39362,7 @@ def cancel_watchlist_scan():
return jsonify({"success": False, "error": "No scan is currently running"}), 400
watchlist_scan_state['cancel_requested'] = True
- print("🛑 [Watchlist Scan] Cancel requested by user")
+ print("[Watchlist Scan] Cancel requested by user")
return jsonify({"success": True, "message": "Cancel request sent"})
except Exception as e:
@@ -39609,7 +39609,7 @@ def watchlist_artist_config(artist_id):
conn.close()
- print(f"✅ Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}, live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, compilations={include_compilations}, instrumentals={include_instrumentals}")
+ print(f"Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}, live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, compilations={include_compilations}, instrumentals={include_instrumentals}")
return jsonify({
"success": True,
@@ -39692,7 +39692,7 @@ def watchlist_artist_link_provider(artist_id):
conn.close()
action = 'Cleared' if is_clear else 'Linked'
- print(f"✅ {action} watchlist artist '{artist_name}' {provider} ID: {new_provider_id or 'NULL'}")
+ print(f"{action} watchlist artist '{artist_name}' {provider} ID: {new_provider_id or 'NULL'}")
return jsonify({
"success": True,
@@ -39756,7 +39756,7 @@ def watchlist_global_config():
config_manager.set('watchlist.global_include_instrumentals', include_instrumentals)
config_manager.set('watchlist.exclude_terms', exclude_terms)
- print(f"✅ Updated global watchlist config: override={global_override_enabled}, "
+ print(f"Updated global watchlist config: override={global_override_enabled}, "
f"albums={include_albums}, eps={include_eps}, singles={include_singles}, "
f"live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, "
f"compilations={include_compilations}, instrumentals={include_instrumentals}, "
@@ -39798,7 +39798,7 @@ def _apply_watchlist_global_overrides(watchlist_artists):
g_acoustic = config_manager.get('watchlist.global_include_acoustic', False)
g_compilations = config_manager.get('watchlist.global_include_compilations', False)
g_instrumentals = config_manager.get('watchlist.global_include_instrumentals', False)
- print(f"🌐 [Watchlist] Global override is ACTIVE — applying to {len(watchlist_artists)} artists "
+ print(f"[Watchlist] Global override is ACTIVE — applying to {len(watchlist_artists)} artists "
f"(albums={g_albums}, eps={g_eps}, singles={g_singles}, live={g_live}, "
f"remixes={g_remixes}, acoustic={g_acoustic}, compilations={g_compilations}, "
f"instrumentals={g_instrumentals})")
@@ -39821,7 +39821,7 @@ def _update_similar_artists_worker():
from database.music_database import get_database
import time
- print("🎵 [Similar Artists] Starting similar artists update...")
+ print("[Similar Artists] Starting similar artists update...")
database = get_database()
all_profiles = database.get_all_profiles()
@@ -39838,11 +39838,11 @@ def _update_similar_artists_worker():
if not artist_profiles:
similar_artists_update_state['status'] = 'completed'
- print("📭 [Similar Artists] No watchlist artists to process")
+ print("[Similar Artists] No watchlist artists to process")
return
similar_artists_update_state['total_artists'] = len(artist_profiles)
- print(f"📊 [Similar Artists] Processing {len(artist_profiles)} unique watchlist artists across {len(all_profiles)} profiles")
+ print(f"[Similar Artists] Processing {len(artist_profiles)} unique watchlist artists across {len(all_profiles)} profiles")
scanner = get_watchlist_scanner(spotify_client)
@@ -39862,17 +39862,17 @@ def _update_similar_artists_worker():
time.sleep(2.0) # 2 seconds between artists
except Exception as artist_error:
- print(f"❌ [Similar Artists] Error processing {artist.artist_name}: {artist_error}")
+ print(f"[Similar Artists] Error processing {artist.artist_name}: {artist_error}")
continue
# Update complete
similar_artists_update_state['status'] = 'completed'
similar_artists_update_state['current_artist'] = None
- print(f"✅ [Similar Artists] Update complete! Processed {len(artist_profiles)} artists")
+ print(f"[Similar Artists] Update complete! Processed {len(artist_profiles)} artists")
except Exception as e:
- print(f"❌ [Similar Artists] Critical error: {e}")
+ print(f"[Similar Artists] Critical error: {e}")
import traceback
traceback.print_exc()
@@ -39899,7 +39899,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
global watchlist_auto_scanning, watchlist_auto_scanning_timestamp, watchlist_scan_state
scope_label = f"profile {profile_id}" if profile_id else "all profiles"
- print(f"🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan ({scope_label})...")
+ print(f"[Auto-Watchlist] Timer triggered - starting automatic watchlist scan ({scope_label})...")
_ew_state = {}
@@ -39907,20 +39907,20 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
# CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock
# This prevents deadlock and handles stuck flags (2-hour timeout)
if is_watchlist_actually_scanning():
- print("⚠️ [Auto-Watchlist] Already scanning (verified with stuck detection), skipping.")
+ print("[Auto-Watchlist] Already scanning (verified with stuck detection), skipping.")
return
with watchlist_timer_lock:
# Re-check inside lock to handle race conditions
if watchlist_auto_scanning:
- print("⚠️ [Auto-Watchlist] Already scanning (race condition check), skipping.")
+ print("[Auto-Watchlist] Already scanning (race condition check), skipping.")
return
# Set flag and timestamp
import time
watchlist_auto_scanning = True
watchlist_auto_scanning_timestamp = time.time()
- print(f"🔒 [Auto-Watchlist] Flag set at timestamp {watchlist_auto_scanning_timestamp}")
+ print(f"[Auto-Watchlist] Flag set at timestamp {watchlist_auto_scanning_timestamp}")
# Use app context for database operations
with app.app_context():
@@ -39939,7 +39939,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
watchlist_count = sum(database.get_watchlist_count(profile_id=p['id']) for p in scan_profiles)
profile_label = f"profile {profile_id}" if profile_id else f"{len(scan_profiles)} profiles"
- print(f"🔍 [Auto-Watchlist] Watchlist count check: {watchlist_count} artists found ({profile_label})")
+ print(f"[Auto-Watchlist] Watchlist count check: {watchlist_count} artists found ({profile_label})")
if watchlist_count == 0:
print("ℹ️ [Auto-Watchlist] No artists in watchlist for auto-scanning.")
@@ -39955,7 +39955,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
watchlist_auto_scanning_timestamp = 0
return
- print(f"👁️ [Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...")
+ print(f"[Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...")
_update_automation_progress(automation_id, progress=5, phase='Loading watchlist',
log_line=f'{watchlist_count} artists ({profile_label})', log_type='info')
@@ -40019,7 +40019,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
artist_delay = base_artist_delay
album_delay = base_album_delay
- print(f"📊 [Auto-Watchlist] Scan parameters: {artist_count} artists, lookback={lookback_period}, "
+ print(f"[Auto-Watchlist] Scan parameters: {artist_count} artists, lookback={lookback_period}, "
f"delays: {artist_delay:.1f}s/artist, {album_delay:.1f}s/album")
# Circuit breaker: pause scan on consecutive rate-limit failures
@@ -40031,7 +40031,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
for i, artist in enumerate(watchlist_artists):
# Check for cancel request
if watchlist_scan_state.get('cancel_requested'):
- print(f"🛑 [Auto-Watchlist] Cancel requested after {i}/{len(watchlist_artists)} artists")
+ print(f"[Auto-Watchlist] Cancel requested after {i}/{len(watchlist_artists)} artists")
watchlist_scan_state['status'] = 'cancelled'
watchlist_scan_state['current_phase'] = 'cancelled'
watchlist_scan_state['summary'] = {
@@ -40176,7 +40176,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
'error_message': None
})())
- print(f"✅ Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist")
+ print(f"Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist")
if artist_new_tracks > 0:
_update_automation_progress(automation_id,
log_line=f'{artist.artist_name} — {artist_new_tracks} new, {artist_added_tracks} added', log_type='success')
@@ -40211,7 +40211,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
scanner.update_similar_artists(artist, profile_id=artist_profile_id)
print(f" Similar artists updated for {artist.artist_name}")
except Exception as similar_error:
- print(f" ⚠️ Failed to update similar artists for {artist.artist_name}: {similar_error}")
+ print(f" Failed to update similar artists for {artist.artist_name}: {similar_error}")
# Delay between artists
if i < len(watchlist_artists) - 1:
@@ -40232,7 +40232,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
if "429" in error_str or "rate limit" in error_str:
consecutive_failures += 1
if consecutive_failures >= CIRCUIT_BREAKER_THRESHOLD:
- print(f"🛑 [Auto-Watchlist] Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s")
+ print(f"[Auto-Watchlist] Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s")
watchlist_scan_state['current_phase'] = 'circuit_breaker_pause'
time.sleep(circuit_breaker_pause)
circuit_breaker_pause = min(circuit_breaker_pause * 2, 600)
@@ -40276,12 +40276,12 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
else:
total_new_tracks = watchlist_scan_state.get('summary', {}).get('new_tracks_found', 0)
total_added_to_wishlist = watchlist_scan_state.get('summary', {}).get('tracks_added_to_wishlist', 0)
- print(f"🛑 Automatic watchlist scan cancelled — skipping post-scan steps")
+ print(f"Automatic watchlist scan cancelled — skipping post-scan steps")
# Post-scan steps — skip if cancelled
if not was_cancelled:
# Populate discovery pool from similar artists (per-profile)
- print("🎵 Starting discovery pool population...")
+ print("Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
_update_automation_progress(automation_id, progress=96, phase='Populating discovery pool',
log_line='Building discovery pool from similar artists...', log_type='info')
@@ -40303,16 +40303,16 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
for p in all_profiles:
scanner.populate_discovery_pool(profile_id=p['id'], progress_callback=_discovery_progress)
- print("✅ Discovery pool population complete")
+ print("Discovery pool population complete")
except Exception as discovery_error:
- print(f"⚠️ Error populating discovery pool: {discovery_error}")
+ print(f"Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'Discovery pool error: {discovery_error}', log_type='error')
# Update ListenBrainz playlists cache
- print("🧠 Starting ListenBrainz playlists update...")
+ print("Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
_update_automation_progress(automation_id, progress=97, phase='Updating ListenBrainz',
log_line='Fetching ListenBrainz playlists...', log_type='info')
@@ -40327,7 +40327,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
summary = lb_result.get('summary', {})
- print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {summary}")
+ print(f"ListenBrainz update complete for profile {lb_prof['id']}: {summary}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz (profile {lb_prof["id"]}): playlists updated', log_type='success')
else:
@@ -40335,22 +40335,22 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
summary = lb_result.get('summary', {})
- print(f"✅ ListenBrainz update complete (global): {summary}")
+ print(f"ListenBrainz update complete (global): {summary}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz: playlists updated', log_type='success')
else:
- print(f"⚠️ ListenBrainz update had issues: {lb_result.get('error', 'Unknown error')}")
+ print(f"ListenBrainz update had issues: {lb_result.get('error', 'Unknown error')}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz: {lb_result.get("error", "Unknown error")}', log_type='error')
except Exception as lb_error:
- print(f"⚠️ Error updating ListenBrainz: {lb_error}")
+ print(f"Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'ListenBrainz error: {lb_error}', log_type='error')
# Update current seasonal playlist (weekly refresh)
- print("🎃 Starting seasonal content update...")
+ print("Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
_update_automation_progress(automation_id, progress=98, phase='Updating seasonal content',
log_line='Checking seasonal playlists...', log_type='info')
@@ -40362,16 +40362,16 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
current_season = seasonal_service.get_current_season()
if current_season:
if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7):
- print(f"🎃 Updating {current_season} seasonal content...")
+ print(f"Updating {current_season} seasonal content...")
_update_automation_progress(automation_id,
log_line=f'Updating {current_season} seasonal content...', log_type='info')
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
- print(f"✅ {current_season.capitalize()} seasonal content updated")
+ print(f"{current_season.capitalize()} seasonal content updated")
_update_automation_progress(automation_id,
log_line=f'{current_season.capitalize()} seasonal content updated', log_type='success')
else:
- print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping")
+ print(f"{current_season.capitalize()} seasonal content recently updated, skipping")
_update_automation_progress(automation_id,
log_line=f'{current_season.capitalize()} seasonal content up to date', log_type='info')
else:
@@ -40379,28 +40379,28 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
_update_automation_progress(automation_id,
log_line='No active season', log_type='info')
except Exception as seasonal_error:
- print(f"⚠️ Error updating seasonal content: {seasonal_error}")
+ print(f"Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'Seasonal error: {seasonal_error}', log_type='error')
# Sync Spotify library cache
- print("📚 Syncing Spotify library cache...")
+ print("Syncing Spotify library cache...")
try:
for p in all_profiles:
scanner.sync_spotify_library_cache(profile_id=p['id'])
- print("✅ Spotify library cache sync complete")
+ print("Spotify library cache sync complete")
_update_automation_progress(automation_id,
log_line='Spotify library cache synced', log_type='info')
except Exception as lib_error:
- print(f"⚠️ Error syncing Spotify library: {lib_error}")
+ print(f"Error syncing Spotify library: {lib_error}")
_update_automation_progress(automation_id,
log_line=f'Library cache error: {lib_error}', log_type='error')
# Add activity for watchlist scan completion
if total_added_to_wishlist > 0:
- add_activity_item("👁️", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now")
+ add_activity_item("", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now")
try:
if automation_engine:
@@ -40413,7 +40413,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
pass
except Exception as e:
- print(f"❌ Error in automatic watchlist scan: {e}")
+ print(f"Error in automatic watchlist scan: {e}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id, log_line=f'Error: {str(e)}', log_type='error')
@@ -40437,7 +40437,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
watchlist_auto_scanning = False
watchlist_auto_scanning_timestamp = 0
- print("✅ Automatic watchlist scanning complete")
+ print("Automatic watchlist scanning complete")
# --- Metadata Updater System ---
@@ -40485,7 +40485,7 @@ def get_discover_hero():
# Determine active source
active_source = _get_active_discovery_source()
- print(f"🎵 Discover hero using source: {active_source}")
+ print(f"Discover hero using source: {active_source}")
# Import fallback client for non-Spotify lookups
itunes_client = _get_metadata_fallback_client()
@@ -40913,7 +40913,7 @@ def refresh_spotify_library():
database.set_metadata('spotify_library_last_sync', '')
database.set_metadata('spotify_library_last_full_sync', '')
scanner.sync_spotify_library_cache(profile_id=get_current_profile_id())
- print("✅ Manual Spotify library refresh complete")
+ print("Manual Spotify library refresh complete")
except Exception as e:
print(f"Error in manual Spotify library refresh: {e}")
@@ -41671,10 +41671,10 @@ def refresh_seasonal_content():
try:
current_season = seasonal_service.get_current_season()
if current_season:
- print(f"🔄 Force-refreshing seasonal content for: {current_season}")
+ print(f"Force-refreshing seasonal content for: {current_season}")
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
- print(f"✅ Seasonal content refreshed for: {current_season}")
+ print(f"Seasonal content refreshed for: {current_season}")
else:
print("ℹ️ No active season to refresh")
except Exception as e:
@@ -42019,7 +42019,7 @@ def refresh_your_artists():
if request.args.get('clear', '').lower() == 'true':
database = get_database()
cleared = database.clear_liked_artists(profile_id)
- print(f"🎵 [Your Artists] Cleared {cleared} entries before refresh")
+ print(f"[Your Artists] Cleared {cleared} entries before refresh")
_trigger_your_artists_refresh(profile_id)
return jsonify({"success": True, "message": "Refresh started"})
except Exception as e:
@@ -42061,7 +42061,7 @@ def _fetch_and_match_liked_artists(profile_id: int):
# 1. Fetch from Spotify (followed artists)
try:
if spotify_client and spotify_client.is_spotify_authenticated():
- print("🎵 [Your Artists] Fetching followed artists from Spotify...")
+ print("[Your Artists] Fetching followed artists from Spotify...")
artists = spotify_client.get_followed_artists()
for a in artists:
database.upsert_liked_artist(
@@ -42071,7 +42071,7 @@ def _fetch_and_match_liked_artists(profile_id: int):
profile_id=profile_id
)
fetched += len(artists)
- print(f"🎵 [Your Artists] Fetched {len(artists)} from Spotify")
+ print(f"[Your Artists] Fetched {len(artists)} from Spotify")
except Exception as e:
logger.error(f"[Your Artists] Spotify fetch error: {e}")
@@ -42080,7 +42080,7 @@ def _fetch_and_match_liked_artists(profile_id: int):
if tidal_client and hasattr(tidal_client, 'get_favorite_artists'):
tidal_auth = tidal_client._ensure_valid_token() if hasattr(tidal_client, '_ensure_valid_token') else False
if tidal_auth:
- print("🎵 [Your Artists] Fetching favorite artists from Tidal...")
+ print("[Your Artists] Fetching favorite artists from Tidal...")
artists = tidal_client.get_favorite_artists(limit=200)
for a in artists:
database.upsert_liked_artist(
@@ -42088,7 +42088,7 @@ def _fetch_and_match_liked_artists(profile_id: int):
image_url=a.get('image_url'), profile_id=profile_id
)
fetched += len(artists)
- print(f"🎵 [Your Artists] Fetched {len(artists)} from Tidal")
+ print(f"[Your Artists] Fetched {len(artists)} from Tidal")
except Exception as e:
logger.error(f"[Your Artists] Tidal fetch error: {e}")
@@ -42097,14 +42097,14 @@ def _fetch_and_match_liked_artists(profile_id: int):
lastfm_key = config_manager.get('lastfm.api_key', '')
lastfm_secret = config_manager.get('lastfm.api_secret', '')
lastfm_session = config_manager.get('lastfm.session_key', '')
- print(f"🎵 [Your Artists] Last.fm credentials: key={'yes' if lastfm_key else 'NO'}, secret={'yes' if lastfm_secret else 'NO'}, session={'yes' if lastfm_session else 'NO'}")
+ print(f"[Your Artists] Last.fm credentials: key={'yes' if lastfm_key else 'NO'}, secret={'yes' if lastfm_secret else 'NO'}, session={'yes' if lastfm_session else 'NO'}")
if lastfm_key and lastfm_secret and lastfm_session:
from core.lastfm_client import LastFMClient
lfm = LastFMClient(api_key=lastfm_key, api_secret=lastfm_secret, session_key=lastfm_session)
username = lfm.get_authenticated_username()
- print(f"🎵 [Your Artists] Last.fm username resolved: {username or 'NONE'}")
+ print(f"[Your Artists] Last.fm username resolved: {username or 'NONE'}")
if username:
- print(f"🎵 [Your Artists] Fetching top artists from Last.fm ({username})...")
+ print(f"[Your Artists] Fetching top artists from Last.fm ({username})...")
artists = lfm.get_user_top_artists(username, period='overall', limit=200)
for a in artists:
database.upsert_liked_artist(
@@ -42112,7 +42112,7 @@ def _fetch_and_match_liked_artists(profile_id: int):
image_url=a.get('image_url'), profile_id=profile_id
)
fetched += len(artists)
- print(f"🎵 [Your Artists] Fetched {len(artists)} from Last.fm")
+ print(f"[Your Artists] Fetched {len(artists)} from Last.fm")
except Exception as e:
logger.error(f"[Your Artists] Last.fm fetch error: {e}")
@@ -42120,7 +42120,7 @@ def _fetch_and_match_liked_artists(profile_id: int):
try:
deezer_cl = _get_deezer_client()
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
- print("🎵 [Your Artists] Fetching favorite artists from Deezer...")
+ print("[Your Artists] Fetching favorite artists from Deezer...")
artists = deezer_cl.get_user_favorite_artists(limit=200)
for a in artists:
database.upsert_liked_artist(
@@ -42129,11 +42129,11 @@ def _fetch_and_match_liked_artists(profile_id: int):
image_url=a.get('image_url'), profile_id=profile_id
)
fetched += len(artists)
- print(f"🎵 [Your Artists] Fetched {len(artists)} from Deezer")
+ print(f"[Your Artists] Fetched {len(artists)} from Deezer")
except Exception as e:
logger.error(f"[Your Artists] Deezer fetch error: {e}")
- print(f"🎵 [Your Artists] Total fetched: {fetched}")
+ print(f"[Your Artists] Total fetched: {fetched}")
# 5. Match pending artists to active source
_match_liked_artists_to_all_sources(database, profile_id)
@@ -42336,7 +42336,7 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
matched += 1
database.sync_liked_artists_watchlist_flags(profile_id)
- print(f"🎵 [Your Artists] Matched {matched}/{len(pending)} artists to {len(search_clients)} sources ({api_calls} API calls)")
+ print(f"[Your Artists] Matched {matched}/{len(pending)} artists to {len(search_clients)} sources ({api_calls} API calls)")
# Image backfill: fetch images for matched artists that have IDs but no image
_backfill_liked_artist_images(database, profile_id, search_clients)
@@ -42360,7 +42360,7 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic
if not rows:
return
- print(f"🖼️ [Your Artists] Backfilling images for {len(rows)} artists...")
+ print(f"[Your Artists] Backfilling images for {len(rows)} artists...")
filled = 0
for row in rows:
@@ -42396,7 +42396,7 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic
conn.commit()
if filled:
- print(f"🖼️ [Your Artists] Backfilled {filled}/{len(rows)} artist images")
+ print(f"[Your Artists] Backfilled {filled}/{len(rows)} artist images")
except Exception as e:
logger.debug(f"[Your Artists] Image backfill error: {e}")
@@ -43759,7 +43759,7 @@ def _get_lb_discover_playlists(playlist_type):
"count": 0,
"username": None
})
- print(f"📦 Cache empty for profile {lb_manager.profile_id}, populating ListenBrainz playlists...")
+ print(f"Cache empty for profile {lb_manager.profile_id}, populating ListenBrainz playlists...")
lb_manager.update_all_playlists()
playlists = lb_manager.get_cached_playlists(playlist_type)
@@ -43827,7 +43827,7 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
if not tracks:
# Cache miss or stale entry with no tracks — try fetching from LB API
if lb_manager.client.is_authenticated():
- print(f"🔄 Cache miss for playlist {playlist_mbid}, fetching from ListenBrainz...")
+ print(f"Cache miss for playlist {playlist_mbid}, fetching from ListenBrainz...")
# Remove stale playlist row (if any) so _update_playlist doesn't
# skip due to matching track_count with 0 actual tracks
existing_type = lb_manager.get_playlist_type(playlist_mbid) or 'created_for'
@@ -43914,11 +43914,11 @@ def get_all_listenbrainz_playlists():
}
playlists.append(playlist_info)
- print(f"📋 Returning {len(playlists)} stored ListenBrainz playlists for profile {profile_id}")
+ print(f"Returning {len(playlists)} stored ListenBrainz playlists for profile {profile_id}")
return jsonify({"playlists": playlists})
except Exception as e:
- print(f"❌ Error getting ListenBrainz playlists: {e}")
+ print(f"Error getting ListenBrainz playlists: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/state/', methods=['GET'])
@@ -43953,7 +43953,7 @@ def get_listenbrainz_playlist_state(playlist_mbid):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting ListenBrainz playlist state: {e}")
+ print(f"Error getting ListenBrainz playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/reset/', methods=['POST'])
@@ -43982,11 +43982,11 @@ def reset_listenbrainz_playlist(playlist_mbid):
state['discovery_future'] = None
state['last_accessed'] = time.time()
- print(f"🔄 Reset ListenBrainz playlist to fresh: {state['playlist']['title']}")
+ print(f"Reset ListenBrainz playlist to fresh: {state['playlist']['title']}")
return jsonify({"success": True, "phase": "fresh"})
except Exception as e:
- print(f"❌ Error resetting ListenBrainz playlist: {e}")
+ print(f"Error resetting ListenBrainz playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/remove/', methods=['POST'])
@@ -44006,11 +44006,11 @@ def remove_listenbrainz_playlist(playlist_mbid):
# Remove from state
del listenbrainz_playlist_states[state_key]
- print(f"🗑️ Removed ListenBrainz playlist from state: {playlist_mbid}")
+ print(f"Removed ListenBrainz playlist from state: {playlist_mbid}")
return jsonify({"success": True})
except Exception as e:
- print(f"❌ Error removing ListenBrainz playlist: {e}")
+ print(f"Error removing ListenBrainz playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/discovery/start/', methods=['POST'])
@@ -44039,7 +44039,7 @@ def start_listenbrainz_discovery(playlist_mbid):
'created_at': time.time(),
'last_accessed': time.time()
}
- print(f"✅ Created new ListenBrainz playlist state: {playlist_data.get('name', 'Unknown')}")
+ print(f"Created new ListenBrainz playlist state: {playlist_data.get('name', 'Unknown')}")
else:
# State already exists, update it
state = listenbrainz_playlist_states[state_key]
@@ -44059,17 +44059,17 @@ def start_listenbrainz_discovery(playlist_mbid):
# Add activity for discovery start
playlist_name = playlist_data.get('name', 'Unknown Playlist')
track_count = len(playlist_data.get('tracks', []))
- add_activity_item("🔍", "ListenBrainz Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
+ add_activity_item("", "ListenBrainz Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now")
# Start discovery worker (pass state_key for profile-scoped state access)
future = listenbrainz_discovery_executor.submit(_run_listenbrainz_discovery_worker, state_key)
state['discovery_future'] = future
- print(f"🔍 Started Spotify discovery for ListenBrainz playlist: {playlist_name}")
+ print(f"Started Spotify discovery for ListenBrainz playlist: {playlist_name}")
return jsonify({"success": True, "message": "Discovery started"})
except Exception as e:
- print(f"❌ Error starting ListenBrainz discovery: {e}")
+ print(f"Error starting ListenBrainz discovery: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -44098,7 +44098,7 @@ def get_listenbrainz_discovery_status(playlist_mbid):
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting ListenBrainz discovery status: {e}")
+ print(f"Error getting ListenBrainz discovery status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/update-phase/', methods=['POST'])
@@ -44122,14 +44122,14 @@ def update_listenbrainz_phase(playlist_mbid):
# Update download process ID if provided (for download persistence)
if 'download_process_id' in data:
state['download_process_id'] = data['download_process_id']
- logger.info(f"🎵 Updated ListenBrainz download_process_id: {data['download_process_id']}")
+ logger.info(f"Updated ListenBrainz download_process_id: {data['download_process_id']}")
# Update converted Spotify playlist ID if provided (for download persistence)
if 'converted_spotify_playlist_id' in data:
state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id']
- logger.info(f"🎵 Updated ListenBrainz converted_spotify_playlist_id: {data['converted_spotify_playlist_id']}")
+ logger.info(f"Updated ListenBrainz converted_spotify_playlist_id: {data['converted_spotify_playlist_id']}")
- logger.info(f"🎵 Updated ListenBrainz playlist {playlist_mbid} phase to: {new_phase}")
+ logger.info(f"Updated ListenBrainz playlist {playlist_mbid} phase to: {new_phase}")
return jsonify({
"success": True,
@@ -44137,7 +44137,7 @@ def update_listenbrainz_phase(playlist_mbid):
})
except Exception as e:
- print(f"❌ Error updating ListenBrainz playlist phase: {e}")
+ print(f"Error updating ListenBrainz playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/discovery/update_match', methods=['POST'])
@@ -44170,7 +44170,7 @@ def update_listenbrainz_discovery_match():
state['spotify_matches'] -= 1
# Update result
- result['status'] = '✅ Found' if spotify_track else '❌ Not Found'
+ result['status'] = 'Found' if spotify_track else 'Not Found'
result['status_class'] = 'found' if spotify_track else 'not-found'
result['spotify_track'] = spotify_track.get('name', '') if spotify_track else ''
# Join all artists (matching YouTube/Tidal/Beatport format)
@@ -44195,13 +44195,13 @@ def update_listenbrainz_discovery_match():
result['manual_match'] = True
- print(f"✅ Updated ListenBrainz match for track {track_index}: {result['status']}")
+ print(f"Updated ListenBrainz match for track {track_index}: {result['status']}")
return jsonify({'success': True})
else:
return jsonify({'error': 'Invalid track index'}), 400
except Exception as e:
- print(f"❌ Error updating ListenBrainz discovery match: {e}")
+ print(f"Error updating ListenBrainz discovery match: {e}")
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@@ -44239,7 +44239,7 @@ def convert_listenbrainz_results_to_spotify_tracks(discovery_results):
}
spotify_tracks.append(track)
- print(f"🔄 Converted {len(spotify_tracks)} ListenBrainz matches to Spotify tracks for sync")
+ print(f"Converted {len(spotify_tracks)} ListenBrainz matches to Spotify tracks for sync")
return spotify_tracks
@app.route('/api/wing-it/sync', methods=['POST'])
@@ -44284,7 +44284,7 @@ def wing_it_sync():
sync_playlist_id = f"wing_it_sync_{int(time.time())}"
- add_activity_item("⚡", "Wing It Sync Started", f"'{playlist_name}' — {len(sync_tracks)} tracks", "Now")
+ add_activity_item("", "Wing It Sync Started", f"'{playlist_name}' — {len(sync_tracks)} tracks", "Now")
with sync_lock:
sync_states[sync_playlist_id] = {"status": "starting", "progress": {}}
@@ -44296,7 +44296,7 @@ def wing_it_sync():
future = sync_executor.submit(_run_sync_task, sync_playlist_id, playlist_name, sync_tracks, None, get_current_profile_id())
active_sync_workers[sync_playlist_id] = future
- logger.info(f"⚡ [Wing It] Started sync for: {playlist_name} ({len(sync_tracks)} tracks)")
+ logger.info(f"[Wing It] Started sync for: {playlist_name} ({len(sync_tracks)} tracks)")
return jsonify({"success": True, "sync_playlist_id": sync_playlist_id})
except Exception as e:
@@ -44329,7 +44329,7 @@ def start_listenbrainz_sync(playlist_mbid):
playlist_name = state['playlist']['name']
# Add activity for sync start
- add_activity_item("🔄", "ListenBrainz Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
+ add_activity_item("", "ListenBrainz Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
# Update ListenBrainz state
state['phase'] = 'syncing'
@@ -44350,11 +44350,11 @@ def start_listenbrainz_sync(playlist_mbid):
future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id())
active_sync_workers[sync_playlist_id] = future
- print(f"🔄 Started ListenBrainz sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ print(f"Started ListenBrainz sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
return jsonify({"success": True, "sync_playlist_id": sync_playlist_id})
except Exception as e:
- print(f"❌ Error starting ListenBrainz sync: {e}")
+ print(f"Error starting ListenBrainz sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/sync/status/', methods=['GET'])
@@ -44390,16 +44390,16 @@ def get_listenbrainz_sync_status(playlist_mbid):
state['sync_progress'] = sync_state.get('progress', {})
# Add activity for sync completion
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
- add_activity_item("🔄", "Sync Complete", f"ListenBrainz playlist '{playlist_name}' synced successfully", "Now")
+ add_activity_item("", "Sync Complete", f"ListenBrainz playlist '{playlist_name}' synced successfully", "Now")
elif sync_state.get('status') == 'error':
state['phase'] = 'discovered' # Revert on error
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
- add_activity_item("❌", "Sync Failed", f"ListenBrainz playlist '{playlist_name}' sync failed", "Now")
+ add_activity_item("", "Sync Failed", f"ListenBrainz playlist '{playlist_name}' sync failed", "Now")
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting ListenBrainz sync status: {e}")
+ print(f"Error getting ListenBrainz sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/sync/cancel/', methods=['POST'])
@@ -44431,7 +44431,7 @@ def cancel_listenbrainz_sync(playlist_mbid):
return jsonify({"success": True, "message": "ListenBrainz sync cancelled"})
except Exception as e:
- print(f"❌ Error cancelling ListenBrainz sync: {e}")
+ print(f"Error cancelling ListenBrainz sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -44456,7 +44456,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
# Convert to our standard format - prepare tracks first without cover art
tracks = []
- print(f"🎵 Processing {len(jspf_tracks)} tracks from playlist")
+ print(f"Processing {len(jspf_tracks)} tracks from playlist")
# First pass: extract all track data without cover art
track_data_list = []
@@ -44474,8 +44474,8 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
mb_data = extension.get('https://musicbrainz.org/doc/jspf#track', {})
if idx == 0:
- print(f"📋 Sample track extension data: {extension}")
- print(f"📋 Sample mb_data keys: {mb_data.keys() if mb_data else 'None'}")
+ print(f"Sample track extension data: {extension}")
+ print(f"Sample mb_data keys: {mb_data.keys() if mb_data else 'None'}")
# Extract release MBID for cover art
release_mbid = None
@@ -44535,7 +44535,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
return None
- print(f"🎨 Fetching cover art for {len(track_data_list)} tracks in parallel...")
+ print(f"Fetching cover art for {len(track_data_list)} tracks in parallel...")
start_time = time.time()
# Fetch up to 10 covers at a time
@@ -44554,7 +44554,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
elapsed = time.time() - start_time
covers_found = sum(1 for t in track_data_list if t.get('album_cover_url'))
- print(f"✅ Fetched {covers_found}/{len(track_data_list)} covers in {elapsed:.2f}s")
+ print(f"Fetched {covers_found}/{len(track_data_list)} covers in {elapsed:.2f}s")
tracks = track_data_list
@@ -44591,17 +44591,17 @@ def start_metadata_update():
if active_server == "jellyfin":
media_client = jellyfin_client
if not media_client:
- add_activity_item("❌", "Metadata Update", "Jellyfin client not available", "Now")
+ add_activity_item("", "Metadata Update", "Jellyfin client not available", "Now")
return jsonify({"success": False, "error": "Jellyfin client not available"}), 400
elif active_server == "navidrome":
media_client = navidrome_client
if not media_client:
- add_activity_item("❌", "Metadata Update", "Navidrome client not available", "Now")
+ add_activity_item("", "Metadata Update", "Navidrome client not available", "Now")
return jsonify({"success": False, "error": "Navidrome client not available"}), 400
else: # plex
media_client = plex_client
if not media_client:
- add_activity_item("❌", "Metadata Update", "Plex client not available", "Now")
+ add_activity_item("", "Metadata Update", "Plex client not available", "Now")
return jsonify({"success": False, "error": "Plex client not available"}), 400
# DEBUG: Check Plex connection details
@@ -44621,7 +44621,7 @@ def start_metadata_update():
# Check Spotify client - EXACTLY like dashboard.py
if not spotify_client:
- add_activity_item("❌", "Metadata Update", "Spotify client not available", "Now")
+ add_activity_item("", "Metadata Update", "Spotify client not available", "Now")
return jsonify({"success": False, "error": "Spotify client not available"}), 400
# Reset state
@@ -44654,11 +44654,11 @@ def start_metadata_update():
print(f"Error in metadata update worker: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
- add_activity_item("❌", "Metadata Error", str(e), "Now")
+ add_activity_item("", "Metadata Error", str(e), "Now")
metadata_update_worker = metadata_update_executor.submit(run_metadata_update)
- add_activity_item("🎵", "Metadata Update", "Loading artists from library...", "Now")
+ add_activity_item("", "Metadata Update", "Loading artists from library...", "Now")
return jsonify({"success": True})
@@ -44677,7 +44677,7 @@ def stop_metadata_update():
if metadata_update_state['status'] == 'running':
metadata_update_state['status'] = 'stopping'
metadata_update_state['current_artist'] = 'Stopping...'
- add_activity_item("⏹️", "Metadata Update", "Stopping metadata update process", "Now")
+ add_activity_item("", "Metadata Update", "Stopping metadata update process", "Now")
return jsonify({"success": True})
@@ -44722,7 +44722,7 @@ def get_active_media_server():
def get_beatport_genres():
"""Get current Beatport genres with images dynamically scraped from homepage"""
try:
- logger.info("🔍 API request for Beatport genres")
+ logger.info("API request for Beatport genres")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44732,13 +44732,13 @@ def get_beatport_genres():
# Discover genres dynamically
if include_images:
- logger.info("🖼️ Including genre images in response (slower)")
+ logger.info("Including genre images in response (slower)")
genres = scraper.discover_genres_with_images(include_images=True)
else:
- logger.info("📝 Returning genres without images (faster)")
+ logger.info("Returning genres without images (faster)")
genres = scraper.discover_genres_from_homepage()
- logger.info(f"✅ Successfully discovered {len(genres)} Beatport genres")
+ logger.info(f"Successfully discovered {len(genres)} Beatport genres")
return jsonify({
"success": True,
@@ -44748,7 +44748,7 @@ def get_beatport_genres():
})
except Exception as e:
- logger.error(f"❌ Error fetching Beatport genres: {e}")
+ logger.error(f"Error fetching Beatport genres: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -44760,7 +44760,7 @@ def get_beatport_genres():
def get_beatport_genre_tracks(genre_slug, genre_id):
"""Get tracks for a specific Beatport genre"""
try:
- logger.info(f"🎵 API request for {genre_slug} genre tracks (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre tracks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44779,7 +44779,7 @@ def get_beatport_genre_tracks(genre_slug, genre_id):
# Scrape tracks for this genre
tracks = scraper.scrape_genre_charts(genre, limit=limit, enrich=enrich)
- logger.info(f"✅ Successfully scraped {len(tracks)} tracks for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} tracks for {genre_slug}")
return jsonify({
"success": True,
@@ -44789,7 +44789,7 @@ def get_beatport_genre_tracks(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching tracks for {genre_slug}: {e}")
+ logger.error(f"Error fetching tracks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -44816,8 +44816,8 @@ def extract_beatport_chart_tracks():
enrich = data.get('enrich', True)
- logger.info(f"🔍 API request to extract tracks from chart: {chart_name}")
- logger.info(f"🔗 Chart URL: {chart_url}")
+ logger.info(f"API request to extract tracks from chart: {chart_name}")
+ logger.info(f"Chart URL: {chart_url}")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44840,7 +44840,7 @@ def extract_beatport_chart_tracks():
if len(table_tracks) > len(tracks):
tracks = table_tracks
- logger.info(f"✅ Successfully extracted {len(tracks)} tracks from chart: {chart_name}")
+ logger.info(f"Successfully extracted {len(tracks)} tracks from chart: {chart_name}")
return jsonify({
"success": True,
@@ -44851,7 +44851,7 @@ def extract_beatport_chart_tracks():
})
except Exception as e:
- logger.error(f"❌ Error extracting tracks from chart: {e}")
+ logger.error(f"Error extracting tracks from chart: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -44863,7 +44863,7 @@ def extract_beatport_chart_tracks():
def get_beatport_genre_top_10(genre_slug, genre_id):
"""Get top 10 tracks for a specific Beatport genre"""
try:
- logger.info(f"🔥 API request for {genre_slug} genre top 10 tracks (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre top 10 tracks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44878,7 +44878,7 @@ def get_beatport_genre_top_10(genre_slug, genre_id):
# Scrape top 10 tracks for this genre
tracks = scraper.scrape_genre_top_10(genre)
- logger.info(f"✅ Successfully scraped {len(tracks)} top 10 tracks for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} top 10 tracks for {genre_slug}")
return jsonify({
"success": True,
@@ -44888,7 +44888,7 @@ def get_beatport_genre_top_10(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching top 10 tracks for {genre_slug}: {e}")
+ logger.error(f"Error fetching top 10 tracks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -44900,7 +44900,7 @@ def get_beatport_genre_top_10(genre_slug, genre_id):
def get_beatport_genre_releases_top_10(genre_slug, genre_id):
"""Get top 10 releases for a specific Beatport genre"""
try:
- logger.info(f"📊 API request for {genre_slug} genre top 10 releases (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre top 10 releases (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44915,7 +44915,7 @@ def get_beatport_genre_releases_top_10(genre_slug, genre_id):
# Scrape top 10 releases for this genre
releases = scraper.scrape_genre_releases(genre, limit=10)
- logger.info(f"✅ Successfully scraped {len(releases)} top 10 releases for {genre_slug}")
+ logger.info(f"Successfully scraped {len(releases)} top 10 releases for {genre_slug}")
return jsonify({
"success": True,
@@ -44925,7 +44925,7 @@ def get_beatport_genre_releases_top_10(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching top 10 releases for {genre_slug}: {e}")
+ logger.error(f"Error fetching top 10 releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -44937,7 +44937,7 @@ def get_beatport_genre_releases_top_10(genre_slug, genre_id):
def get_beatport_genre_releases_top_100(genre_slug, genre_id):
"""Get top 100 releases for a specific Beatport genre"""
try:
- logger.info(f"📊 API request for {genre_slug} genre top 100 releases (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre top 100 releases (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44955,7 +44955,7 @@ def get_beatport_genre_releases_top_100(genre_slug, genre_id):
# Scrape top releases for this genre
releases = scraper.scrape_genre_releases(genre, limit=limit)
- logger.info(f"✅ Successfully scraped {len(releases)} top 100 releases for {genre_slug}")
+ logger.info(f"Successfully scraped {len(releases)} top 100 releases for {genre_slug}")
return jsonify({
"success": True,
@@ -44965,7 +44965,7 @@ def get_beatport_genre_releases_top_100(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching top 100 releases for {genre_slug}: {e}")
+ logger.error(f"Error fetching top 100 releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -44977,7 +44977,7 @@ def get_beatport_genre_releases_top_100(genre_slug, genre_id):
def get_beatport_genre_staff_picks(genre_slug, genre_id):
"""Get staff picks for a specific Beatport genre"""
try:
- logger.info(f"⭐ API request for {genre_slug} genre staff picks (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre staff picks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -44995,7 +44995,7 @@ def get_beatport_genre_staff_picks(genre_slug, genre_id):
# Scrape staff picks for this genre
tracks = scraper.scrape_genre_staff_picks(genre, limit=limit)
- logger.info(f"✅ Successfully scraped {len(tracks)} staff picks for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} staff picks for {genre_slug}")
return jsonify({
"success": True,
@@ -45005,7 +45005,7 @@ def get_beatport_genre_staff_picks(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching staff picks for {genre_slug}: {e}")
+ logger.error(f"Error fetching staff picks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45017,7 +45017,7 @@ def get_beatport_genre_staff_picks(genre_slug, genre_id):
def get_beatport_genre_hype_top_10(genre_slug, genre_id):
"""Get hype top 10 tracks for a specific Beatport genre"""
try:
- logger.info(f"🚀 API request for {genre_slug} genre hype top 10 (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre hype top 10 (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45032,7 +45032,7 @@ def get_beatport_genre_hype_top_10(genre_slug, genre_id):
# Scrape hype top 10 for this genre
tracks = scraper.scrape_genre_hype_top_10(genre)
- logger.info(f"✅ Successfully scraped {len(tracks)} hype top 10 tracks for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} hype top 10 tracks for {genre_slug}")
return jsonify({
"success": True,
@@ -45042,7 +45042,7 @@ def get_beatport_genre_hype_top_10(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching hype top 10 for {genre_slug}: {e}")
+ logger.error(f"Error fetching hype top 10 for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45054,7 +45054,7 @@ def get_beatport_genre_hype_top_10(genre_slug, genre_id):
def get_beatport_genre_hype_top_100(genre_slug, genre_id):
"""Get hype top 100 tracks for a specific Beatport genre"""
try:
- logger.info(f"💥 API request for {genre_slug} genre hype top 100 (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre hype top 100 (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45069,7 +45069,7 @@ def get_beatport_genre_hype_top_100(genre_slug, genre_id):
# Scrape hype top 100 for this genre
tracks = scraper.scrape_genre_hype_charts(genre, limit=100)
- logger.info(f"✅ Successfully scraped {len(tracks)} hype top 100 tracks for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} hype top 100 tracks for {genre_slug}")
return jsonify({
"success": True,
@@ -45079,7 +45079,7 @@ def get_beatport_genre_hype_top_100(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching hype top 100 for {genre_slug}: {e}")
+ logger.error(f"Error fetching hype top 100 for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45091,7 +45091,7 @@ def get_beatport_genre_hype_top_100(genre_slug, genre_id):
def get_beatport_genre_hype_picks(genre_slug, genre_id):
"""Get hype picks for a specific Beatport genre"""
try:
- logger.info(f"⚡ API request for {genre_slug} genre hype picks (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre hype picks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45109,7 +45109,7 @@ def get_beatport_genre_hype_picks(genre_slug, genre_id):
# Scrape hype picks for this genre
tracks = scraper.scrape_genre_hype_picks(genre, limit=limit)
- logger.info(f"✅ Successfully scraped {len(tracks)} hype picks for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} hype picks for {genre_slug}")
return jsonify({
"success": True,
@@ -45119,7 +45119,7 @@ def get_beatport_genre_hype_picks(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching hype picks for {genre_slug}: {e}")
+ logger.error(f"Error fetching hype picks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45131,7 +45131,7 @@ def get_beatport_genre_hype_picks(genre_slug, genre_id):
def get_beatport_genre_latest_releases(genre_slug, genre_id):
"""Get latest releases for a specific Beatport genre"""
try:
- logger.info(f"🕒 API request for {genre_slug} genre latest releases (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre latest releases (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45149,7 +45149,7 @@ def get_beatport_genre_latest_releases(genre_slug, genre_id):
# Scrape latest releases for this genre
tracks = scraper.scrape_genre_latest_releases(genre, limit=limit)
- logger.info(f"✅ Successfully scraped {len(tracks)} latest releases for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} latest releases for {genre_slug}")
return jsonify({
"success": True,
@@ -45159,7 +45159,7 @@ def get_beatport_genre_latest_releases(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching latest releases for {genre_slug}: {e}")
+ logger.error(f"Error fetching latest releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45171,7 +45171,7 @@ def get_beatport_genre_latest_releases(genre_slug, genre_id):
def get_beatport_genre_new_charts(genre_slug, genre_id):
"""Get new charts for a specific Beatport genre"""
try:
- logger.info(f"📈 API request for {genre_slug} genre new charts (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre new charts (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45189,7 +45189,7 @@ def get_beatport_genre_new_charts(genre_slug, genre_id):
# Scrape new charts for this genre
tracks = scraper.scrape_genre_new_charts(genre, limit=limit)
- logger.info(f"✅ Successfully scraped {len(tracks)} new charts for {genre_slug}")
+ logger.info(f"Successfully scraped {len(tracks)} new charts for {genre_slug}")
return jsonify({
"success": True,
@@ -45199,7 +45199,7 @@ def get_beatport_genre_new_charts(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching new charts for {genre_slug}: {e}")
+ logger.error(f"Error fetching new charts for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45211,14 +45211,14 @@ def get_beatport_genre_new_charts(genre_slug, genre_id):
def get_beatport_genre_hero(genre_slug, genre_id):
"""Get hero slider data for a specific Beatport genre with 1-hour caching"""
try:
- logger.info(f"🎠 API request for {genre_slug} genre hero slider (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre hero slider (ID: {genre_id})")
# Check cache first (1-hour TTL like other genre data)
cache_key = f"hero_{genre_slug}_{genre_id}"
cached_data = get_cached_beatport_data('genre', cache_key, genre_slug)
if cached_data:
- logger.info(f"💾 Returning cached hero data for {genre_slug}")
+ logger.info(f"Returning cached hero data for {genre_slug}")
return jsonify({
"success": True,
"releases": cached_data,
@@ -45239,7 +45239,7 @@ def get_beatport_genre_hero(genre_slug, genre_id):
# Cache the data (1-hour TTL)
set_cached_beatport_data('genre', cache_key, hero_releases, genre_slug)
- logger.info(f"✅ Successfully scraped and cached {len(hero_releases)} hero releases for {genre_slug}")
+ logger.info(f"Successfully scraped and cached {len(hero_releases)} hero releases for {genre_slug}")
return jsonify({
"success": True,
@@ -45251,7 +45251,7 @@ def get_beatport_genre_hero(genre_slug, genre_id):
"scrape_timestamp": time.time()
})
else:
- logger.info(f"⚠️ No hero releases found for {genre_slug}")
+ logger.info(f"No hero releases found for {genre_slug}")
return jsonify({
"success": False,
"releases": [],
@@ -45262,7 +45262,7 @@ def get_beatport_genre_hero(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching hero data for {genre_slug}: {e}")
+ logger.error(f"Error fetching hero data for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45276,12 +45276,12 @@ def get_beatport_genre_hero(genre_slug, genre_id):
def get_beatport_genre_top10_lists(genre_slug, genre_id):
"""Get Top 10 lists (Beatport + Hype) for a specific genre with 1-hour caching"""
try:
- logger.info(f"🎵 API request for {genre_slug} Top 10 lists (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} Top 10 lists (ID: {genre_id})")
# Check cache first (1-hour TTL)
cached_data = get_cached_beatport_data('genre', 'top_10_lists', genre_slug)
if cached_data:
- logger.info(f"✅ Returning cached Top 10 lists for {genre_slug}")
+ logger.info(f"Returning cached Top 10 lists for {genre_slug}")
cached_data['success'] = True
cached_data['cached'] = True
return jsonify(cached_data)
@@ -45323,13 +45323,13 @@ def get_beatport_genre_top10_lists(genre_slug, genre_id):
# Cache the data (1-hour TTL)
set_cached_beatport_data('genre', 'top_10_lists', response_data, genre_slug)
- logger.info(f"✅ Successfully fetched {response_data['beatport_count']} Beatport + {response_data['hype_count']} Hype Top 10 tracks for {genre_slug}")
+ logger.info(f"Successfully fetched {response_data['beatport_count']} Beatport + {response_data['hype_count']} Hype Top 10 tracks for {genre_slug}")
response_data['success'] = True
return jsonify(response_data)
except Exception as e:
- logger.error(f"❌ Error fetching Top 10 lists for {genre_slug}: {e}")
+ logger.error(f"Error fetching Top 10 lists for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45347,12 +45347,12 @@ def get_beatport_genre_top10_lists(genre_slug, genre_id):
def get_beatport_genre_top10_releases(genre_slug, genre_id):
"""Get Top 10 releases for a specific genre using .partial-artwork elements with 1-hour caching"""
try:
- logger.info(f"💿 API request for {genre_slug} Top 10 releases (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} Top 10 releases (ID: {genre_id})")
# Check cache first (1-hour TTL)
cached_data = get_cached_beatport_data('genre', 'top_10_releases', genre_slug)
if cached_data:
- logger.info(f"✅ Returning cached Top 10 releases for {genre_slug}")
+ logger.info(f"Returning cached Top 10 releases for {genre_slug}")
cached_data['success'] = True
cached_data['cached'] = True
return jsonify(cached_data)
@@ -45387,13 +45387,13 @@ def get_beatport_genre_top10_releases(genre_slug, genre_id):
# Cache the data (1-hour TTL)
set_cached_beatport_data('genre', 'top_10_releases', response_data, genre_slug)
- logger.info(f"✅ Successfully fetched {response_data['releases_count']} Top 10 releases for {genre_slug}")
+ logger.info(f"Successfully fetched {response_data['releases_count']} Top 10 releases for {genre_slug}")
response_data['success'] = True
return jsonify(response_data)
except Exception as e:
- logger.error(f"❌ Error fetching Top 10 releases for {genre_slug}: {e}")
+ logger.error(f"Error fetching Top 10 releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45408,7 +45408,7 @@ def get_beatport_genre_top10_releases(genre_slug, genre_id):
def get_beatport_genre_sections(genre_slug, genre_id):
"""Discover all available sections for a specific Beatport genre"""
try:
- logger.info(f"🔍 API request for {genre_slug} genre sections discovery (ID: {genre_id})")
+ logger.info(f"API request for {genre_slug} genre sections discovery (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45423,7 +45423,7 @@ def get_beatport_genre_sections(genre_slug, genre_id):
# Discover sections for this genre
sections = scraper.discover_genre_page_sections(genre)
- logger.info(f"✅ Successfully discovered sections for {genre_slug}")
+ logger.info(f"Successfully discovered sections for {genre_slug}")
return jsonify({
"success": True,
@@ -45432,7 +45432,7 @@ def get_beatport_genre_sections(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error discovering sections for {genre_slug}: {e}")
+ logger.error(f"Error discovering sections for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45443,7 +45443,7 @@ def get_beatport_genre_sections(genre_slug, genre_id):
def get_beatport_top_100():
"""Get Beatport Top 100 tracks"""
try:
- logger.info("🔥 API request for Beatport Top 100")
+ logger.info("API request for Beatport Top 100")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45455,7 +45455,7 @@ def get_beatport_top_100():
# Scrape Top 100
tracks = scraper.scrape_top_100(limit=limit, enrich=enrich)
- logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Top 100")
+ logger.info(f"Successfully scraped {len(tracks)} tracks from Beatport Top 100")
return jsonify({
"success": True,
@@ -45465,7 +45465,7 @@ def get_beatport_top_100():
})
except Exception as e:
- logger.error(f"❌ Error fetching Beatport Top 100: {e}")
+ logger.error(f"Error fetching Beatport Top 100: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45477,7 +45477,7 @@ def get_beatport_top_100():
def get_beatport_genre_image(genre_slug, genre_id):
"""Get image for a specific Beatport genre"""
try:
- logger.info(f"🖼️ API request for {genre_slug} genre image")
+ logger.info(f"API request for {genre_slug} genre image")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45489,7 +45489,7 @@ def get_beatport_genre_image(genre_slug, genre_id):
image_url = scraper.get_genre_image(genre_url)
if image_url:
- logger.info(f"✅ Found image for {genre_slug}")
+ logger.info(f"Found image for {genre_slug}")
return jsonify({
"success": True,
"image_url": image_url,
@@ -45497,7 +45497,7 @@ def get_beatport_genre_image(genre_slug, genre_id):
"genre_id": genre_id
})
else:
- logger.info(f"⚠️ No image found for {genre_slug}")
+ logger.info(f"No image found for {genre_slug}")
return jsonify({
"success": False,
"image_url": None,
@@ -45506,7 +45506,7 @@ def get_beatport_genre_image(genre_slug, genre_id):
})
except Exception as e:
- logger.error(f"❌ Error fetching image for {genre_slug}: {e}")
+ logger.error(f"Error fetching image for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45517,7 +45517,7 @@ def get_beatport_genre_image(genre_slug, genre_id):
def get_beatport_hype_top_100():
"""Get Beatport Hype Top 100 - Improved with fixed URL"""
try:
- logger.info("🔥 API request for Beatport Hype Top 100")
+ logger.info("API request for Beatport Hype Top 100")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45529,7 +45529,7 @@ def get_beatport_hype_top_100():
# Scrape Hype Top 100 using improved method
tracks = scraper.scrape_hype_top_100(limit=limit, enrich=enrich)
- logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Hype Top 100")
+ logger.info(f"Successfully scraped {len(tracks)} tracks from Beatport Hype Top 100")
return jsonify({
"success": True,
@@ -45539,7 +45539,7 @@ def get_beatport_hype_top_100():
})
except Exception as e:
- logger.error(f"❌ Error fetching Beatport Hype Top 100: {e}")
+ logger.error(f"Error fetching Beatport Hype Top 100: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45551,7 +45551,7 @@ def get_beatport_hype_top_100():
def get_beatport_top_100_releases():
"""Get Beatport Top 100 Releases - New endpoint"""
try:
- logger.info("📊 API request for Beatport Top 100 Releases")
+ logger.info("API request for Beatport Top 100 Releases")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45562,7 +45562,7 @@ def get_beatport_top_100_releases():
# Scrape Top 100 Releases using new method
tracks = scraper.scrape_top_100_releases(limit=limit)
- logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Top 100 Releases")
+ logger.info(f"Successfully scraped {len(tracks)} tracks from Beatport Top 100 Releases")
return jsonify({
"success": True,
@@ -45572,7 +45572,7 @@ def get_beatport_top_100_releases():
})
except Exception as e:
- logger.error(f"❌ Error fetching Beatport Top 100 Releases: {e}")
+ logger.error(f"Error fetching Beatport Top 100 Releases: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45593,7 +45593,7 @@ def get_beatport_homepage_new_releases():
# Get new releases from homepage
new_releases = scraper.scrape_new_releases(limit=limit)
- logger.info(f"✅ Successfully extracted {len(new_releases)} new releases from homepage")
+ logger.info(f"Successfully extracted {len(new_releases)} new releases from homepage")
return jsonify({
"success": True,
@@ -45603,7 +45603,7 @@ def get_beatport_homepage_new_releases():
})
except Exception as e:
- logger.error(f"❌ Error getting Beatport homepage new releases: {e}")
+ logger.error(f"Error getting Beatport homepage new releases: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45616,7 +45616,7 @@ def get_beatport_homepage_hype_picks():
"""Get Beatport Hype Picks from homepage section"""
try:
limit = int(request.args.get('limit', 40))
- logger.info(f"🔥 API request for Beatport homepage Hype Picks (limit: {limit})")
+ logger.info(f"API request for Beatport homepage Hype Picks (limit: {limit})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45624,7 +45624,7 @@ def get_beatport_homepage_hype_picks():
# Get hype picks from homepage
hype_picks = scraper.scrape_hype_picks_homepage(limit=limit)
- logger.info(f"✅ Successfully extracted {len(hype_picks)} hype picks from homepage")
+ logger.info(f"Successfully extracted {len(hype_picks)} hype picks from homepage")
return jsonify({
"success": True,
@@ -45634,7 +45634,7 @@ def get_beatport_homepage_hype_picks():
})
except Exception as e:
- logger.error(f"❌ Error getting Beatport homepage hype picks: {e}")
+ logger.error(f"Error getting Beatport homepage hype picks: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45647,7 +45647,7 @@ def get_beatport_homepage_top_10_releases():
"""Get Beatport Top 10 Releases from homepage section"""
try:
limit = int(request.args.get('limit', 10))
- logger.info(f"🔟 API request for Beatport homepage Top 10 Releases (limit: {limit})")
+ logger.info(f"API request for Beatport homepage Top 10 Releases (limit: {limit})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45655,7 +45655,7 @@ def get_beatport_homepage_top_10_releases():
# Get top 10 releases from homepage
top_10_releases = scraper.scrape_top_10_releases_homepage(limit=limit)
- logger.info(f"✅ Successfully extracted {len(top_10_releases)} top 10 releases from homepage")
+ logger.info(f"Successfully extracted {len(top_10_releases)} top 10 releases from homepage")
return jsonify({
"success": True,
@@ -45665,7 +45665,7 @@ def get_beatport_homepage_top_10_releases():
})
except Exception as e:
- logger.error(f"❌ Error getting Beatport homepage top 10 releases: {e}")
+ logger.error(f"Error getting Beatport homepage top 10 releases: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45677,17 +45677,17 @@ def get_beatport_homepage_top_10_releases():
def get_beatport_homepage_top10_lists():
"""Get Beatport Top 10 Lists from homepage - both Beatport Top 10 and Hype Top 10"""
try:
- logger.info("🏆 API request for Beatport homepage Top 10 Lists")
+ logger.info("API request for Beatport homepage Top 10 Lists")
# Check cache first
cached_data = get_cached_beatport_data('homepage', 'top_10_lists')
if cached_data:
- logger.info("🏆 Returning cached top 10 lists data")
+ logger.info("Returning cached top 10 lists data")
response = jsonify(cached_data)
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh top 10 lists data...")
+ logger.info("Cache miss - scraping fresh top 10 lists data...")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45695,7 +45695,7 @@ def get_beatport_homepage_top10_lists():
# Get top 10 lists from homepage
top10_lists = scraper.scrape_homepage_top10_lists()
- logger.info(f"✅ Successfully extracted Beatport Top 10: {len(top10_lists['beatport_top10'])}, Hype Top 10: {len(top10_lists['hype_top10'])}")
+ logger.info(f"Successfully extracted Beatport Top 10: {len(top10_lists['beatport_top10'])}, Hype Top 10: {len(top10_lists['hype_top10'])}")
# Prepare response data
response_data = {
@@ -45714,7 +45714,7 @@ def get_beatport_homepage_top10_lists():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error getting Beatport homepage top 10 lists: {e}")
+ logger.error(f"Error getting Beatport homepage top 10 lists: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -45728,17 +45728,17 @@ def get_beatport_homepage_top10_lists():
def get_beatport_homepage_top10_releases_cards():
"""Get Beatport Top 10 Releases CARDS from homepage (not individual tracks)"""
try:
- logger.info("💿 API request for Beatport homepage Top 10 Releases CARDS")
+ logger.info("API request for Beatport homepage Top 10 Releases CARDS")
# Check cache first
cached_data = get_cached_beatport_data('homepage', 'top_10_releases')
if cached_data:
- logger.info("💿 Returning cached top 10 releases data")
+ logger.info("Returning cached top 10 releases data")
response = jsonify(cached_data)
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh top 10 releases data...")
+ logger.info("Cache miss - scraping fresh top 10 releases data...")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45746,13 +45746,13 @@ def get_beatport_homepage_top10_releases_cards():
# Get top 10 releases from homepage
top10_releases = scraper.scrape_homepage_top10_releases()
- logger.info(f"✅ API extracted {len(top10_releases)} Top 10 Release Cards")
+ logger.info(f"API extracted {len(top10_releases)} Top 10 Release Cards")
# Debug: Log first release if any
if top10_releases:
logger.info(f"First release: {top10_releases[0].get('title', 'No title')} by {top10_releases[0].get('artist', 'No artist')}")
else:
- logger.warning("❌ No releases found by scraper")
+ logger.warning("No releases found by scraper")
# Prepare response data
response_data = {
@@ -45769,7 +45769,7 @@ def get_beatport_homepage_top10_releases_cards():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error getting Beatport homepage Top 10 Releases cards: {e}")
+ logger.error(f"Error getting Beatport homepage Top 10 Releases cards: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
return jsonify({
@@ -45803,7 +45803,7 @@ def scrape_beatport_releases():
"track_count": 0
}), 400
- logger.info(f"🎯 API request to scrape {len(release_urls)} release URLs with source: {source_name}")
+ logger.info(f"API request to scrape {len(release_urls)} release URLs with source: {source_name}")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -45811,7 +45811,7 @@ def scrape_beatport_releases():
# Use our new general scraper function
tracks = scraper.scrape_multiple_releases(release_urls, source_name)
- logger.info(f"✅ Successfully extracted {len(tracks)} tracks from {len(release_urls)} releases")
+ logger.info(f"Successfully extracted {len(tracks)} tracks from {len(release_urls)} releases")
# Apply text cleaning to track data
cleaned_tracks = []
@@ -45834,7 +45834,7 @@ def scrape_beatport_releases():
})
except Exception as e:
- logger.error(f"❌ Error scraping releases: {e}")
+ logger.error(f"Error scraping releases: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
return jsonify({
@@ -45856,7 +45856,7 @@ def get_beatport_release_metadata():
if not release_url:
return jsonify({"success": False, "error": "No release_url provided"}), 400
- logger.info(f"🎯 API request for release metadata: {release_url}")
+ logger.info(f"API request for release metadata: {release_url}")
scraper = BeatportUnifiedScraper()
result = scraper.get_release_metadata(release_url)
@@ -45877,12 +45877,12 @@ def get_beatport_release_metadata():
# Update the embedded album name too
track['album']['name'] = album['name']
- logger.info(f"✅ Release metadata: {album['name']} by {artist['name']} ({len(result['tracks'])} tracks)")
+ logger.info(f"Release metadata: {album['name']} by {artist['name']} ({len(result['tracks'])} tracks)")
return jsonify(result)
except Exception as e:
- logger.error(f"❌ Error getting release metadata: {e}")
+ logger.error(f"Error getting release metadata: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -45905,7 +45905,7 @@ def enrich_beatport_tracks():
enrichment_id = data.get('enrichment_id', str(uuid.uuid4()))
- logger.info(f"🎯 Enriching {len(tracks)} Beatport tracks with per-track metadata (id: {enrichment_id})")
+ logger.info(f"Enriching {len(tracks)} Beatport tracks with per-track metadata (id: {enrichment_id})")
# --- Check enrichment cache (fast, do before spawning background) ---
cached_results = {}
@@ -45927,7 +45927,7 @@ def enrich_beatport_tracks():
cache_hits = len(cached_results)
cache_misses = len(uncached_tracks)
- logger.info(f"📦 Enrichment cache: {cache_hits} hits, {cache_misses} misses")
+ logger.info(f"Enrichment cache: {cache_hits} hits, {cache_misses} misses")
# All cached — return immediately (no background task needed)
if cache_misses == 0:
@@ -45960,7 +45960,7 @@ def enrich_beatport_tracks():
task['completed'] = new_completed
task['current_track'] = track_name
else:
- logger.warning(f"⚠️ on_progress: task {enrichment_id} not found in _enrichment_tasks!")
+ logger.warning(f"on_progress: task {enrichment_id} not found in _enrichment_tasks!")
scraper = BeatportUnifiedScraper()
newly_enriched = scraper.enrich_chart_tracks(uncached_tracks, progress_callback=on_progress)
@@ -45990,7 +45990,7 @@ def enrich_beatport_tracks():
if merged[i] is None:
merged[i] = tracks[i]
- logger.info(f"✅ Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)")
+ logger.info(f"Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)")
with _enrichment_tasks_lock:
task = _enrichment_tasks.get(enrichment_id)
@@ -46000,7 +46000,7 @@ def enrich_beatport_tracks():
task['completed'] = len(tracks)
except Exception as e:
- logger.error(f"❌ Error enriching tracks: {e}")
+ logger.error(f"Error enriching tracks: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
with _enrichment_tasks_lock:
@@ -46015,7 +46015,7 @@ def enrich_beatport_tracks():
return jsonify({"success": True, "enrichment_id": enrichment_id, "async": True})
except Exception as e:
- logger.error(f"❌ Error starting enrichment: {e}")
+ logger.error(f"Error starting enrichment: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -46051,7 +46051,7 @@ def get_beatport_homepage_featured_charts():
"""Get Beatport Featured Charts from homepage section"""
try:
limit = int(request.args.get('limit', 20))
- logger.info(f"📊 API request for Beatport homepage Featured Charts (limit: {limit})")
+ logger.info(f"API request for Beatport homepage Featured Charts (limit: {limit})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -46059,7 +46059,7 @@ def get_beatport_homepage_featured_charts():
# Get featured charts from homepage
featured_charts = scraper.scrape_featured_charts(limit=limit)
- logger.info(f"✅ Successfully extracted {len(featured_charts)} featured charts from homepage")
+ logger.info(f"Successfully extracted {len(featured_charts)} featured charts from homepage")
return jsonify({
"success": True,
@@ -46069,7 +46069,7 @@ def get_beatport_homepage_featured_charts():
})
except Exception as e:
- logger.error(f"❌ Error getting Beatport homepage featured charts: {e}")
+ logger.error(f"Error getting Beatport homepage featured charts: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -46081,7 +46081,7 @@ def get_beatport_homepage_featured_charts():
def get_beatport_chart_sections():
"""Get dynamically discovered Beatport chart sections"""
try:
- logger.info("🔍 API request for Beatport chart sections discovery")
+ logger.info("API request for Beatport chart sections discovery")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -46089,7 +46089,7 @@ def get_beatport_chart_sections():
# Discover chart sections dynamically
chart_sections = scraper.discover_chart_sections()
- logger.info(f"✅ Successfully discovered chart sections")
+ logger.info(f"Successfully discovered chart sections")
return jsonify({
"success": True,
@@ -46098,7 +46098,7 @@ def get_beatport_chart_sections():
})
except Exception as e:
- logger.error(f"❌ Error discovering Beatport chart sections: {e}")
+ logger.error(f"Error discovering Beatport chart sections: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -46110,7 +46110,7 @@ def get_beatport_chart_sections():
def get_beatport_dj_charts_improved():
"""Get Beatport DJ Charts using improved method"""
try:
- logger.info("🎧 API request for Beatport DJ Charts (improved)")
+ logger.info("API request for Beatport DJ Charts (improved)")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
@@ -46121,7 +46121,7 @@ def get_beatport_dj_charts_improved():
# Scrape DJ Charts using improved method
charts = scraper.scrape_dj_charts(limit=limit)
- logger.info(f"✅ Successfully scraped {len(charts)} DJ charts")
+ logger.info(f"Successfully scraped {len(charts)} DJ charts")
return jsonify({
"success": True,
@@ -46131,7 +46131,7 @@ def get_beatport_dj_charts_improved():
})
except Exception as e:
- logger.error(f"❌ Error fetching Beatport DJ Charts: {e}")
+ logger.error(f"Error fetching Beatport DJ Charts: {e}")
return jsonify({
"success": False,
"error": str(e),
@@ -46144,17 +46144,17 @@ def get_beatport_dj_charts_improved():
def get_beatport_hype_picks():
"""Get Beatport Hype Picks for the rebuild slider grid (EXACT same pattern as new-releases)"""
try:
- logger.info("🔥 Fetching Beatport hype picks...")
+ logger.info("Fetching Beatport hype picks...")
# Check cache first
cached_data = get_cached_beatport_data('homepage', 'hype_picks')
if cached_data:
- logger.info("🔥 Returning cached hype picks data")
+ logger.info("Returning cached hype picks data")
response = jsonify(cached_data)
return add_cache_headers(response, 3600) # 1 hour
# Cache miss - scrape fresh data
- logger.info("🔄 Cache miss - scraping fresh hype picks data...")
+ logger.info("Cache miss - scraping fresh hype picks data...")
# Initialize scraper
scraper = BeatportUnifiedScraper()
@@ -46168,7 +46168,7 @@ def get_beatport_hype_picks():
hype_pick_cards = soup.select('[data-testid="hype-picks"]')
releases = []
- logger.info(f"🔍 Found {len(hype_pick_cards)} hype pick cards")
+ logger.info(f"Found {len(hype_pick_cards)} hype pick cards")
for i, card in enumerate(hype_pick_cards[:100]): # Limit to 100 for 10 slides (same as new-releases)
release_data = {}
@@ -46225,7 +46225,7 @@ def get_beatport_hype_picks():
releases.append(release_data)
- logger.info(f"✅ Successfully extracted {len(releases)} hype picks")
+ logger.info(f"Successfully extracted {len(releases)} hype picks")
# Prepare response data
response_data = {
@@ -46243,7 +46243,7 @@ def get_beatport_hype_picks():
return add_cache_headers(response, 3600) # 1 hour
except Exception as e:
- logger.error(f"❌ Error getting Beatport hype picks: {e}")
+ logger.error(f"Error getting Beatport hype picks: {e}")
return jsonify({
'success': False,
'error': str(e),
@@ -46257,25 +46257,25 @@ def start_beatport_discovery(url_hash):
"""Start Spotify discovery for Beatport chart tracks"""
import json
try:
- logger.info(f"🔍 Starting Beatport discovery for: {url_hash}")
+ logger.info(f"Starting Beatport discovery for: {url_hash}")
# Get chart data from request body
data = request.get_json() or {}
- print(f"🔍 Raw request data: {data}")
+ print(f"Raw request data: {data}")
chart_data = data.get('chart_data')
- print(f"🔍 Chart data extracted: {chart_data is not None}")
+ print(f"Chart data extracted: {chart_data is not None}")
# Debug logging
if chart_data:
- print(f"🔍 Chart data keys: {list(chart_data.keys()) if isinstance(chart_data, dict) else 'Not a dict'}")
- print(f"🔍 Chart name: {chart_data.get('name') if isinstance(chart_data, dict) else 'N/A'}")
+ print(f"Chart data keys: {list(chart_data.keys()) if isinstance(chart_data, dict) else 'Not a dict'}")
+ print(f"Chart name: {chart_data.get('name') if isinstance(chart_data, dict) else 'N/A'}")
if isinstance(chart_data, dict) and 'tracks' in chart_data:
- print(f"🔍 Number of tracks: {len(chart_data['tracks'])}")
+ print(f"Number of tracks: {len(chart_data['tracks'])}")
if chart_data['tracks']:
- print(f"🔍 First track: {chart_data['tracks'][0]}")
+ print(f"First track: {chart_data['tracks'][0]}")
else:
- print("🔍 No chart data received")
+ print("No chart data received")
if not chart_data or not chart_data.get('tracks'):
return jsonify({"error": "Chart data with tracks is required"}), 400
@@ -46308,18 +46308,18 @@ def start_beatport_discovery(url_hash):
# Add activity for discovery start
chart_name = chart_data.get('name', 'Unknown Chart')
track_count = len(chart_data['tracks'])
- add_activity_item("🔍", "Beatport Discovery Started", f"'{chart_name}' - {track_count} tracks", "Now")
+ add_activity_item("", "Beatport Discovery Started", f"'{chart_name}' - {track_count} tracks", "Now")
# Start discovery worker (capture profile ID while we have Flask context)
beatport_chart_states[url_hash]['_profile_id'] = get_current_profile_id()
future = beatport_discovery_executor.submit(_run_beatport_discovery_worker, url_hash)
state['discovery_future'] = future
- print(f"🔍 Started Spotify discovery for Beatport chart: {chart_name}")
+ print(f"Started Spotify discovery for Beatport chart: {chart_name}")
return jsonify({"success": True, "message": "Discovery started", "status": "discovering"})
except Exception as e:
- logger.error(f"❌ Error starting Beatport discovery: {e}")
+ logger.error(f"Error starting Beatport discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/discovery/status/', methods=['GET'])
@@ -46345,7 +46345,7 @@ def get_beatport_discovery_status(url_hash):
return jsonify(response)
except Exception as e:
- logger.error(f"❌ Error getting Beatport discovery status: {e}")
+ logger.error(f"Error getting Beatport discovery status: {e}")
return jsonify({"error": str(e)}), 500
@@ -46375,7 +46375,7 @@ def update_beatport_discovery_match():
old_status = result.get('status')
# Update with user-selected track
- result['status'] = '✅ Found'
+ result['status'] = 'Found'
result['status_class'] = 'found'
result['spotify_track'] = spotify_track['name']
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
@@ -46403,16 +46403,16 @@ def update_beatport_discovery_match():
result['manual_match'] = True # Flag for tracking
# Update match count if status changed from not found/error
- if old_status != 'found' and old_status != '✅ Found':
+ if old_status != 'found' and old_status != 'Found':
state['spotify_matches'] = state.get('spotify_matches', 0) + 1
- logger.info(f"✅ Manual match updated: beatport - {identifier} - track {track_index}")
+ logger.info(f"Manual match updated: beatport - {identifier} - track {track_index}")
logger.info(f" → {result['spotify_artist']} - {result['spotify_track']}")
return jsonify({'success': True, 'result': result})
except Exception as e:
- logger.error(f"❌ Error updating Beatport discovery match: {e}")
+ logger.error(f"Error updating Beatport discovery match: {e}")
return jsonify({'error': str(e)}), 500
@@ -46449,7 +46449,7 @@ def _run_beatport_discovery_worker(url_hash):
if not use_spotify:
itunes_client_instance = _get_metadata_fallback_client()
- print(f"🔍 Starting {discovery_source.upper()} discovery for {len(tracks)} Beatport tracks...")
+ print(f"Starting {discovery_source.upper()} discovery for {len(tracks)} Beatport tracks...")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -46459,7 +46459,7 @@ def _run_beatport_discovery_worker(url_hash):
try:
# Check for cancellation
if state.get('phase') != 'discovering':
- print(f"🛑 Beatport discovery cancelled (phase changed to '{state.get('phase')}')")
+ print(f"Beatport discovery cancelled (phase changed to '{state.get('phase')}')")
return
# Update progress
@@ -46478,7 +46478,7 @@ def _run_beatport_discovery_worker(url_hash):
else:
track_artist = clean_beatport_text(str(track_artists))
- print(f"🔍 Searching {discovery_source.upper()} for: '{track_artist}' - '{track_title}'")
+ print(f"Searching {discovery_source.upper()} for: '{track_artist}' - '{track_title}'")
# Check discovery cache first
cache_key = _get_discovery_cache_key(track_title, track_artist)
@@ -46486,7 +46486,7 @@ def _run_beatport_discovery_worker(url_hash):
cache_db = get_database()
cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source)
if cached_match and _validate_discovery_cache_artist(track_artist, cached_match):
- print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {track_artist} - {track_title}")
+ print(f"CACHE HIT [{i+1}/{len(tracks)}]: {track_artist} - {track_title}")
# Convert artists from ['str'] to [{'name': 'str'}] for Beatport frontend format
beatport_artists = cached_match.get('artists', [])
if beatport_artists and isinstance(beatport_artists[0], str):
@@ -46506,7 +46506,7 @@ def _run_beatport_discovery_worker(url_hash):
state['discovery_results'].append(result_entry)
continue
except Exception as cache_err:
- print(f"⚠️ Cache lookup error: {cache_err}")
+ print(f"Cache lookup error: {cache_err}")
# Use matching engine for track matching
found_track = None
@@ -46522,9 +46522,9 @@ def _run_beatport_discovery_worker(url_hash):
'album': None
})()
search_queries = matching_engine.generate_download_queries(temp_track)
- print(f"🔍 Generated {len(search_queries)} search queries using matching engine")
+ print(f"Generated {len(search_queries)} search queries using matching engine")
except Exception as e:
- print(f"⚠️ Matching engine failed for Beatport, falling back to basic queries: {e}")
+ print(f"Matching engine failed for Beatport, falling back to basic queries: {e}")
if use_spotify:
search_queries = [
f"{track_artist} {track_title}",
@@ -46540,7 +46540,7 @@ def _run_beatport_discovery_worker(url_hash):
for query_idx, search_query in enumerate(search_queries):
try:
- print(f"🔍 Query {query_idx + 1}/{len(search_queries)}: {search_query} ({discovery_source.upper()})")
+ print(f"Query {query_idx + 1}/{len(search_queries)}: {search_query} ({discovery_source.upper()})")
search_results = None
@@ -46565,19 +46565,19 @@ def _run_beatport_discovery_worker(url_hash):
best_raw_track = _cache.get_entity('spotify', 'track', match.id)
else:
best_raw_track = None
- print(f"✅ New best Beatport match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"New best Beatport match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
if best_confidence >= 0.9:
- print(f"🎯 High confidence match found ({best_confidence:.3f}), stopping search")
+ print(f"High confidence match found ({best_confidence:.3f}), stopping search")
break
except Exception as e:
- print(f"❌ Error in {discovery_source.upper()} search for query '{search_query}': {e}")
+ print(f"Error in {discovery_source.upper()} search for query '{search_query}': {e}")
continue
# Strategy 4: Extended search with higher limit (last resort)
if not found_track:
- print(f"🔄 Beatport Strategy 4: Extended search with limit=50")
+ print(f"Beatport Strategy 4: Extended search with limit=50")
query = f"{track_artist} {track_title}"
if use_spotify:
extended_results = spotify_client.search_tracks(query, limit=50)
@@ -46590,12 +46590,12 @@ def _run_beatport_discovery_worker(url_hash):
if match and confidence >= min_confidence:
found_track = match
best_confidence = confidence
- print(f"✅ Strategy 4 Beatport match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
+ print(f"Strategy 4 Beatport match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
if found_track:
- print(f"✅ Final Beatport match: {found_track.artists[0]} - {found_track.name} (confidence: {best_confidence:.3f})")
+ print(f"Final Beatport match: {found_track.artists[0]} - {found_track.name} (confidence: {best_confidence:.3f})")
else:
- print(f"❌ No suitable match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})")
+ print(f"No suitable match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})")
# Create result entry
result_entry = {
@@ -46614,7 +46614,7 @@ def _run_beatport_discovery_worker(url_hash):
if use_spotify:
# SPOTIFY result formatting
# Debug: show available attributes
- print(f"🔍 Spotify track attributes: {dir(found_track)}")
+ print(f"Spotify track attributes: {dir(found_track)}")
# Format artists correctly for frontend compatibility
formatted_artists = []
@@ -46691,9 +46691,9 @@ def _run_beatport_discovery_worker(url_hash):
cache_key[0], cache_key[1], discovery_source, best_confidence,
cache_data, track_title, track_artist
)
- print(f"💾 CACHE SAVED: {track_artist} - {track_title} (confidence: {best_confidence:.3f})")
+ print(f"CACHE SAVED: {track_artist} - {track_title} (confidence: {best_confidence:.3f})")
except Exception as cache_err:
- print(f"⚠️ Cache save error: {cache_err}")
+ print(f"Cache save error: {cache_err}")
state['discovery_results'].append(result_entry)
@@ -46701,7 +46701,7 @@ def _run_beatport_discovery_worker(url_hash):
time.sleep(0.1)
except Exception as e:
- print(f"❌ Error processing Beatport track {i}: {e}")
+ print(f"Error processing Beatport track {i}: {e}")
# Add error result
state['discovery_results'].append({
'index': i, # Add index for frontend table row identification
@@ -46723,16 +46723,16 @@ def _run_beatport_discovery_worker(url_hash):
# Add activity for completion
chart_name = chart.get('name', 'Unknown Chart')
source_label = discovery_source.upper()
- add_activity_item("✅", f"Beatport Discovery Complete ({source_label})",
+ add_activity_item("", f"Beatport Discovery Complete ({source_label})",
f"'{chart_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now")
- print(f"✅ Beatport discovery complete ({source_label}): {state['spotify_matches']}/{len(tracks)} tracks found")
+ print(f"Beatport discovery complete ({source_label}): {state['spotify_matches']}/{len(tracks)} tracks found")
# Sync discovery results back to mirrored playlist
_sync_discovery_results_to_mirrored('beatport', url_hash, state.get('discovery_results', []), discovery_source, profile_id=state.get('_profile_id', 1))
except Exception as e:
- print(f"❌ Error in Beatport discovery worker: {e}")
+ print(f"Error in Beatport discovery worker: {e}")
if url_hash in beatport_chart_states:
beatport_chart_states[url_hash]['status'] = 'error'
beatport_chart_states[url_hash]['phase'] = 'fresh'
@@ -46743,19 +46743,19 @@ def _run_beatport_discovery_worker(url_hash):
def start_beatport_sync(url_hash):
"""Start sync process for a Beatport chart using discovered Spotify tracks"""
try:
- print(f"🎧 Beatport sync start requested for: {url_hash}")
+ print(f"Beatport sync start requested for: {url_hash}")
if url_hash not in beatport_chart_states:
- print(f"❌ Beatport chart not found: {url_hash}")
+ print(f"Beatport chart not found: {url_hash}")
return jsonify({"error": "Beatport chart not found"}), 404
state = beatport_chart_states[url_hash]
state['last_accessed'] = time.time() # Update access time
- print(f"🎧 Beatport chart state: phase={state.get('phase')}, has_discovery_results={len(state.get('discovery_results', []))}")
+ print(f"Beatport chart state: phase={state.get('phase')}, has_discovery_results={len(state.get('discovery_results', []))}")
if state['phase'] not in ['discovered', 'sync_complete', 'download_complete']:
- print(f"❌ Beatport chart not ready for sync: {state['phase']}")
+ print(f"Beatport chart not ready for sync: {state['phase']}")
return jsonify({"error": "Beatport chart not ready for sync"}), 400
# Convert discovery results to Spotify tracks format
@@ -46789,11 +46789,11 @@ def start_beatport_sync(url_hash):
future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['name'], spotify_tracks, None, get_current_profile_id())
state['sync_future'] = future
- print(f"🎧 Started Beatport sync for chart: {state['chart']['name']}")
+ print(f"Started Beatport sync for chart: {state['chart']['name']}")
return jsonify({"success": True, "sync_id": sync_playlist_id})
except Exception as e:
- print(f"❌ Error starting Beatport sync: {e}")
+ print(f"Error starting Beatport sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/sync/status/', methods=['GET'])
@@ -46828,16 +46828,16 @@ def get_beatport_sync_status(url_hash):
result = sync_state.get('result', {})
state['converted_spotify_playlist_id'] = result.get('spotify_playlist_id')
chart_name = state.get('chart', {}).get('name', 'Unknown Chart')
- add_activity_item("🔄", "Sync Complete", f"Beatport chart '{chart_name}' synced successfully", "Now")
+ add_activity_item("", "Sync Complete", f"Beatport chart '{chart_name}' synced successfully", "Now")
elif sync_state.get('status') == 'error':
state['phase'] = 'discovered' # Revert on error
chart_name = state.get('chart', {}).get('name', 'Unknown Chart')
- add_activity_item("❌", "Sync Failed", f"Beatport chart '{chart_name}' sync failed", "Now")
+ add_activity_item("", "Sync Failed", f"Beatport chart '{chart_name}' sync failed", "Now")
return jsonify(response)
except Exception as e:
- print(f"❌ Error getting Beatport sync status: {e}")
+ print(f"Error getting Beatport sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/sync/cancel/', methods=['POST'])
@@ -46865,11 +46865,11 @@ def cancel_beatport_sync(url_hash):
state['sync_playlist_id'] = None
state['sync_progress'] = {}
- print(f"🎧 Cancelled Beatport sync for: {url_hash}")
+ print(f"Cancelled Beatport sync for: {url_hash}")
return jsonify({"success": True})
except Exception as e:
- print(f"❌ Error cancelling Beatport sync: {e}")
+ print(f"Error cancelling Beatport sync: {e}")
return jsonify({"error": str(e)}), 500
# ===================================================================
@@ -46909,13 +46909,13 @@ def get_beatport_charts():
# Remove old charts
for chart_hash in to_remove:
del beatport_chart_states[chart_hash]
- logger.info(f"🧹 Cleaned up old Beatport chart: {chart_hash}")
+ logger.info(f"Cleaned up old Beatport chart: {chart_hash}")
- logger.info(f"📊 Returning {len(charts)} Beatport charts for hydration")
+ logger.info(f"Returning {len(charts)} Beatport charts for hydration")
return jsonify(charts)
except Exception as e:
- logger.error(f"❌ Error getting Beatport charts: {e}")
+ logger.error(f"Error getting Beatport charts: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/charts/status/', methods=['GET'])
@@ -46947,7 +46947,7 @@ def get_beatport_chart_status(chart_hash):
return jsonify(response)
except Exception as e:
- logger.error(f"❌ Error getting Beatport chart status: {e}")
+ logger.error(f"Error getting Beatport chart status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/charts/update-phase/', methods=['POST'])
@@ -46978,7 +46978,7 @@ def update_beatport_chart_phase(chart_hash):
state['download_process_id'] = None
state['sync_playlist_id'] = None
state['sync_progress'] = {}
- logger.info(f"🎧 Reset Beatport chart {chart_hash} to fresh state")
+ logger.info(f"Reset Beatport chart {chart_hash} to fresh state")
else:
# Handle other phase updates (like download phase transitions)
converted_playlist_id = data.get('converted_spotify_playlist_id')
@@ -46989,11 +46989,11 @@ def update_beatport_chart_phase(chart_hash):
if download_process_id:
state['download_process_id'] = download_process_id
- logger.info(f"🎧 Updated Beatport chart {chart_hash} phase to: {new_phase}")
+ logger.info(f"Updated Beatport chart {chart_hash} phase to: {new_phase}")
return jsonify({"success": True, "phase": new_phase})
except Exception as e:
- logger.error(f"❌ Error updating Beatport chart phase: {e}")
+ logger.error(f"Error updating Beatport chart phase: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/charts/delete/', methods=['DELETE'])
@@ -47006,11 +47006,11 @@ def delete_beatport_chart(chart_hash):
chart_name = beatport_chart_states[chart_hash]['chart']['name']
del beatport_chart_states[chart_hash]
- logger.info(f"🗑️ Deleted Beatport chart: {chart_name}")
+ logger.info(f"Deleted Beatport chart: {chart_name}")
return jsonify({"success": True, "message": f"Deleted chart: {chart_name}"})
except Exception as e:
- logger.error(f"❌ Error deleting Beatport chart: {e}")
+ logger.error(f"Error deleting Beatport chart: {e}")
return jsonify({"error": str(e)}), 500
# ── Mirrored Playlists ────────────────────────────────────────────────
@@ -47354,7 +47354,7 @@ def prepare_mirrored_discovery(playlist_id):
'index': idx,
'yt_track': track['name'],
'yt_artist': track['artists'][0] if track['artists'] else 'Unknown',
- 'status': '🔄 Provider changed',
+ 'status': 'Provider changed',
'status_class': 'not-found',
'spotify_track': '',
'spotify_artist': '',
@@ -47378,7 +47378,7 @@ def prepare_mirrored_discovery(playlist_id):
'index': idx,
'yt_track': track['name'],
'yt_artist': track['artists'][0] if track['artists'] else 'Unknown',
- 'status': '✅ Found',
+ 'status': 'Found',
'status_class': 'found',
'spotify_track': matched.get('name', ''),
'spotify_artist': artist_str,
@@ -47403,7 +47403,7 @@ def prepare_mirrored_discovery(playlist_id):
'index': idx,
'yt_track': track['name'],
'yt_artist': track['artists'][0] if track['artists'] else 'Unknown',
- 'status': '🔄 Provider changed' if cached_provider != _current_provider else '❌ Not Found',
+ 'status': 'Provider changed' if cached_provider != _current_provider else 'Not Found',
'status_class': 'not-found',
'spotify_track': '',
'spotify_artist': '',
@@ -47530,13 +47530,13 @@ def retry_failed_mirrored_discovery(playlist_id):
'discovery_attempted': False,
})
except Exception as db_err:
- print(f"⚠️ Error clearing discovery_attempted in DB: {db_err}")
+ print(f"Error clearing discovery_attempted in DB: {db_err}")
# Submit worker
future = youtube_discovery_executor.submit(_run_youtube_discovery_worker, url_hash)
state['discovery_future'] = future
- print(f"🔄 Retrying failed discovery for {url_hash}: {retry_count} tracks to retry, {already_found} already found")
+ print(f"Retrying failed discovery for {url_hash}: {retry_count} tracks to retry, {already_found} already found")
return jsonify({
"success": True,
"retry_count": retry_count,
@@ -48031,7 +48031,7 @@ class WebMetadataUpdateWorker:
if not all_artists:
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = f"No artists found in {self.server_type.title()} library"
- add_activity_item("❌", "Metadata Update", metadata_update_state['error'], "Now")
+ add_activity_item("", "Metadata Update", metadata_update_state['error'], "Now")
return
# Filter artists that need processing
@@ -48042,10 +48042,10 @@ class WebMetadataUpdateWorker:
if len(artists_to_process) == 0:
metadata_update_state['status'] = 'completed'
metadata_update_state['completed_at'] = datetime.now()
- add_activity_item("✅", "Metadata Update", "All artists already have good metadata", "Now")
+ add_activity_item("", "Metadata Update", "All artists already have good metadata", "Now")
return
else:
- add_activity_item("🎵", "Metadata Update", f"Processing {len(artists_to_process)} of {len(all_artists)} artists", "Now")
+ add_activity_item("", "Metadata Update", f"Processing {len(artists_to_process)} of {len(all_artists)} artists", "Now")
if not artists_to_process:
metadata_update_state['status'] = 'completed'
@@ -48111,13 +48111,13 @@ class WebMetadataUpdateWorker:
metadata_update_state['current_artist'] = 'Completed'
summary = f"Processed {self.processed_count} artists: {self.successful_count} updated, {self.failed_count} failed"
- add_activity_item("🎵", "Metadata Complete", summary, "Now")
+ add_activity_item("", "Metadata Complete", summary, "Now")
except Exception as e:
print(f"Metadata update failed: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
- add_activity_item("❌", "Metadata Error", str(e), "Now")
+ add_activity_item("", "Metadata Error", str(e), "Now")
def artist_needs_processing(self, artist):
"""Check if an artist needs metadata processing using age-based detection - EXACT copy from dashboard.py"""
@@ -48217,7 +48217,7 @@ class WebMetadataUpdateWorker:
if raw and 'name' in raw:
spotify_artist = SpotifyArtistDC.from_spotify_artist(raw)
highest_score = 1.0
- print(f"✅ Metadata updater: direct Spotify lookup for '{artist_name}' via cached ID {db_spotify_id}")
+ print(f"Metadata updater: direct Spotify lookup for '{artist_name}' via cached ID {db_spotify_id}")
except Exception as e:
print(f"Direct Spotify lookup failed for {db_spotify_id}: {e}")
spotify_artist = None
@@ -48326,15 +48326,15 @@ class WebMetadataUpdateWorker:
try:
# Check if artist already has a good photo (skip check for Jellyfin)
if self.server_type != "jellyfin" and self.artist_has_valid_photo(artist):
- print(f"🖼️ Skipping {artist.title}: already has valid photo ({getattr(artist, 'thumb', 'None')})")
+ print(f"Skipping {artist.title}: already has valid photo ({getattr(artist, 'thumb', 'None')})")
return False
# Get the image URL from Spotify
if not spotify_artist.image_url:
- print(f"🚫 Skipping {artist.title}: no Spotify image URL available")
+ print(f"Skipping {artist.title}: no Spotify image URL available")
return False
- print(f"📸 Processing {artist.title}: downloading from Spotify...")
+ print(f"Processing {artist.title}: downloading from Spotify...")
image_url = spotify_artist.image_url
@@ -48346,7 +48346,7 @@ class WebMetadataUpdateWorker:
if self.server_type == "jellyfin":
# For Jellyfin, use raw image data to preserve original format
image_data = response.content
- print(f"📸 Using raw image data for Jellyfin ({len(image_data)} bytes)")
+ print(f"Using raw image data for Jellyfin ({len(image_data)} bytes)")
else:
# For other servers, validate and convert
image_data = self.validate_and_convert_image(response.content)
@@ -48591,7 +48591,7 @@ class WebMetadataUpdateWorker:
jellyfin_token = jellyfin_config.get('api_key', '')
if not jellyfin_base_url or not jellyfin_token:
- print("❌ Jellyfin configuration missing for image upload")
+ print("Jellyfin configuration missing for image upload")
return False
upload_url = f"{jellyfin_base_url.rstrip('/')}/Items/{artist.ratingKey}/Images/Primary"
@@ -48706,7 +48706,7 @@ def start_oauth_callback_servers():
if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'):
spotify_enrichment_worker.client.reload_config()
spotify_enrichment_worker.client._invalidate_auth_cache()
- add_activity_item("✅", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now")
+ add_activity_item("", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
@@ -48717,7 +48717,7 @@ def start_oauth_callback_servers():
raise Exception("Failed to exchange authorization code for access token")
except Exception as e:
_oauth_logger.error(f"Spotify token processing error: {e}")
- add_activity_item("❌", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now")
+ add_activity_item("", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now")
self.send_response(400)
self.send_header('Content-type', 'text/html')
self.end_headers()
@@ -48726,7 +48726,7 @@ def start_oauth_callback_servers():
error = query_params['error'][0]
_oauth_logger.error(f"Spotify OAuth error returned by Spotify: {error}")
_oauth_logger.error(f"Full callback URL: {self.path}")
- add_activity_item("❌", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now")
+ add_activity_item("", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now")
self.send_response(400)
self.send_header('Content-type', 'text/html')
self.end_headers()
@@ -48769,26 +48769,26 @@ def start_oauth_callback_servers():
bind_addr = ('0.0.0.0', 8888)
spotify_server = HTTPServer(bind_addr, SpotifyCallbackHandler)
_oauth_logger.info(f"Spotify OAuth callback server listening on {bind_addr[0]}:{bind_addr[1]}")
- print(f"🎵 Started Spotify OAuth callback server on {bind_addr[0]}:{bind_addr[1]}")
+ print(f"Started Spotify OAuth callback server on {bind_addr[0]}:{bind_addr[1]}")
spotify_server.serve_forever()
except OSError as e:
_oauth_logger.error(f"Failed to start Spotify callback server on port 8888: {e} — port may already be in use")
- print(f"🔴 Failed to start Spotify callback server on port 8888: {e}")
+ print(f"Failed to start Spotify callback server on port 8888: {e}")
except Exception as e:
_oauth_logger.error(f"Failed to start Spotify callback server: {e}")
- print(f"🔴 Failed to start Spotify callback server: {e}")
+ print(f"Failed to start Spotify callback server: {e}")
# Tidal callback server
class TidalCallbackHandler(BaseHTTPRequestHandler):
def do_GET(self):
- print("🎶🎶🎶 TIDAL CALLBACK SERVER RECEIVED REQUEST 🎶🎶🎶")
+ print("TIDAL CALLBACK SERVER RECEIVED REQUEST ")
parsed_url = urllib.parse.urlparse(self.path)
query_params = urllib.parse.parse_qs(parsed_url.query)
- print(f"🎶 Callback path: {self.path}")
+ print(f"Callback path: {self.path}")
if 'code' in query_params:
auth_code = query_params['code'][0]
- print(f"🎶 Received Tidal authorization code: {auth_code[:10]}...")
+ print(f"Received Tidal authorization code: {auth_code[:10]}...")
# Exchange the authorization code for tokens
try:
@@ -48803,7 +48803,7 @@ def start_oauth_callback_servers():
temp_client.code_verifier = tidal_oauth_state["code_verifier"]
temp_client.code_challenge = tidal_oauth_state["code_challenge"]
- print(f"🔐 Restored PKCE - verifier: {temp_client.code_verifier[:20] if temp_client.code_verifier else 'None'}... challenge: {temp_client.code_challenge[:20] if temp_client.code_challenge else 'None'}...")
+ print(f"Restored PKCE - verifier: {temp_client.code_verifier[:20] if temp_client.code_verifier else 'None'}... challenge: {temp_client.code_challenge[:20] if temp_client.code_challenge else 'None'}...")
success = temp_client.fetch_token_from_code(auth_code)
@@ -48814,7 +48814,7 @@ def start_oauth_callback_servers():
if tidal_enrichment_worker:
tidal_enrichment_worker.client = tidal_client
- add_activity_item("✅", "Tidal Auth Complete", "Successfully authenticated with Tidal", "Now")
+ add_activity_item("", "Tidal Auth Complete", "Successfully authenticated with Tidal", "Now")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
@@ -48823,16 +48823,16 @@ def start_oauth_callback_servers():
raise Exception("Failed to exchange authorization code for tokens")
except Exception as e:
- print(f"🔴 Tidal token processing error: {e}")
- add_activity_item("❌", "Tidal Auth Failed", f"Token processing failed: {str(e)}", "Now")
+ print(f"Tidal token processing error: {e}")
+ add_activity_item("", "Tidal Auth Failed", f"Token processing failed: {str(e)}", "Now")
self.send_response(400)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(f'Tidal Authentication Failed
{str(e)}
'.encode())
else:
error = query_params.get('error', ['Unknown error'])[0]
- print(f"🔴 Tidal OAuth error: {error}")
- add_activity_item("❌", "Tidal Auth Failed", f"OAuth error: {error}", "Now")
+ print(f"Tidal OAuth error: {error}")
+ add_activity_item("", "Tidal Auth Failed", f"OAuth error: {error}", "Now")
self.send_response(400)
self.send_header('Content-type', 'text/html')
self.end_headers()
@@ -48844,13 +48844,13 @@ def start_oauth_callback_servers():
def run_tidal_server():
try:
tidal_server = HTTPServer(('0.0.0.0', 8889), TidalCallbackHandler)
- print("🎶 Started Tidal OAuth callback server on port 8889")
- print(f"🎶 Tidal server listening on all interfaces, port 8889")
+ print("Started Tidal OAuth callback server on port 8889")
+ print(f"Tidal server listening on all interfaces, port 8889")
tidal_server.serve_forever()
except Exception as e:
- print(f"🔴 Failed to start Tidal callback server: {e}")
+ print(f"Failed to start Tidal callback server: {e}")
import traceback
- print(f"🔴 Full error: {traceback.format_exc()}")
+ print(f"Full error: {traceback.format_exc()}")
# Start both servers in background threads
spotify_thread = threading.Thread(target=run_spotify_server, daemon=True)
@@ -48859,7 +48859,7 @@ def start_oauth_callback_servers():
spotify_thread.start()
tidal_thread.start()
- print("✅ OAuth callback servers started")
+ print("OAuth callback servers started")
# ===============================================
# Artist Detail Spotify Integration Functions
@@ -48870,7 +48870,7 @@ def get_spotify_artist_discography(artist_name):
try:
from core.matching_engine import MusicMatchingEngine
- print(f"🎵 Searching Spotify for artist: {artist_name}")
+ print(f"Searching Spotify for artist: {artist_name}")
# Reuse cached profile-aware Spotify client
spotify_client = get_spotify_client_for_profile()
@@ -48898,7 +48898,7 @@ def get_spotify_artist_discography(artist_name):
# Step 1: Try exact case-insensitive match
for spotify_artist in artists:
if artist_name.lower().strip() == spotify_artist.name.lower().strip():
- print(f"🎯 Exact match found: '{spotify_artist.name}'")
+ print(f"Exact match found: '{spotify_artist.name}'")
best_match = spotify_artist
highest_score = 1.0
break
@@ -48911,7 +48911,7 @@ def get_spotify_artist_discography(artist_name):
spotify_artist_normalized = matching_engine.normalize_string(spotify_artist.name)
score = matching_engine.similarity_score(db_artist_normalized, spotify_artist_normalized)
- print(f"🔍 Fuzzy match candidate: '{spotify_artist.name}' (score: {score:.3f})")
+ print(f"Fuzzy match candidate: '{spotify_artist.name}' (score: {score:.3f})")
if score > highest_score:
highest_score = score
@@ -48927,7 +48927,7 @@ def get_spotify_artist_discography(artist_name):
artist = best_match
spotify_artist_id = artist.id
- print(f"🎵 Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})")
+ print(f"Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})")
# Get all albums (albums, singles, and compilations)
all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation', limit=50)
@@ -48938,7 +48938,7 @@ def get_spotify_artist_discography(artist_name):
'error': f'No albums found for artist "{artist_name}"'
}
- print(f"📀 Found {len(all_albums)} releases on Spotify")
+ print(f"Found {len(all_albums)} releases on Spotify")
# Categorize releases
albums = []
@@ -49003,7 +49003,7 @@ def get_spotify_artist_discography(artist_name):
eps = _dedup_releases(eps)
singles = _dedup_releases(singles)
- print(f"📀 Categorized Spotify releases - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
+ print(f"Categorized Spotify releases - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
return {
'success': True,
@@ -49016,7 +49016,7 @@ def get_spotify_artist_discography(artist_name):
}
except Exception as e:
- print(f"❌ Error getting Spotify discography for {artist_name}: {e}")
+ print(f"Error getting Spotify discography for {artist_name}: {e}")
return {
'success': False,
'error': str(e)
@@ -49025,7 +49025,7 @@ def get_spotify_artist_discography(artist_name):
def merge_discography_data(owned_releases, spotify_discography, db=None, artist_name=None):
"""Build discography from Spotify data with 'checking' state - ownership is resolved via SSE stream"""
try:
- print("🔄 Building discography cards (fast path - no DB matching)...")
+ print("Building discography cards (fast path - no DB matching)...")
def build_category(spotify_category, category_name):
"""Build cards for a category with checking state"""
@@ -49052,7 +49052,7 @@ def merge_discography_data(owned_releases, spotify_discography, db=None, artist_
eps = build_category(spotify_discography['eps'], 'EPs')
singles = build_category(spotify_discography['singles'], 'Singles')
- print(f"✅ Built discography cards - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
+ print(f"Built discography cards - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
return {
'success': True,
@@ -49062,7 +49062,7 @@ def merge_discography_data(owned_releases, spotify_discography, db=None, artist_
}
except Exception as e:
- print(f"❌ Error building discography: {e}")
+ print(f"Error building discography: {e}")
import traceback
traceback.print_exc()
return {
@@ -49092,11 +49092,11 @@ try:
mb_worker.start()
if config_manager.get('musicbrainz_enrichment_paused', False):
mb_worker.pause()
- print("✅ MusicBrainz enrichment worker initialized (paused — restored from config)")
+ print("MusicBrainz enrichment worker initialized (paused — restored from config)")
else:
- print("✅ MusicBrainz enrichment worker initialized and started")
+ print("MusicBrainz enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ MusicBrainz worker initialization failed: {e}")
+ print(f"MusicBrainz worker initialization failed: {e}")
mb_worker = None
# --- MusicBrainz API Endpoints ---
@@ -49169,11 +49169,11 @@ try:
audiodb_worker.start()
if config_manager.get('audiodb_enrichment_paused', False):
audiodb_worker.pause()
- print("✅ AudioDB enrichment worker initialized (paused — restored from config)")
+ print("AudioDB enrichment worker initialized (paused — restored from config)")
else:
- print("✅ AudioDB enrichment worker initialized and started")
+ print("AudioDB enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ AudioDB worker initialization failed: {e}")
+ print(f"AudioDB worker initialization failed: {e}")
audiodb_worker = None
# --- AudioDB API Endpoints ---
@@ -49242,11 +49242,11 @@ try:
discogs_worker.start()
if config_manager.get('discogs_enrichment_paused', False):
discogs_worker.pause()
- print("✅ Discogs enrichment worker initialized (paused — restored from config)")
+ print("Discogs enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Discogs enrichment worker initialized and started")
+ print("Discogs enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Discogs worker initialization failed: {e}")
+ print(f"Discogs worker initialization failed: {e}")
discogs_worker = None
# --- Discogs API Endpoints ---
@@ -49304,11 +49304,11 @@ try:
deezer_worker.start()
if config_manager.get('deezer_enrichment_paused', False):
deezer_worker.pause()
- print("✅ Deezer enrichment worker initialized (paused — restored from config)")
+ print("Deezer enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Deezer enrichment worker initialized and started")
+ print("Deezer enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Deezer worker initialization failed: {e}")
+ print(f"Deezer worker initialization failed: {e}")
deezer_worker = None
# --- Deezer API Endpoints ---
@@ -49382,11 +49382,11 @@ try:
spotify_enrichment_worker.paused = True # Set BEFORE start() to prevent race condition
spotify_enrichment_worker.start()
if spotify_enrichment_worker.paused:
- print("✅ Spotify enrichment worker initialized (paused — restored from config)")
+ print("Spotify enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Spotify enrichment worker initialized and started")
+ print("Spotify enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Spotify enrichment worker initialization failed: {e}")
+ print(f"Spotify enrichment worker initialization failed: {e}")
spotify_enrichment_worker = None
# --- API Rate Monitor Endpoints ---
@@ -49481,11 +49481,11 @@ try:
itunes_enrichment_worker.start()
if config_manager.get('itunes_enrichment_paused', False):
itunes_enrichment_worker.pause()
- print("✅ iTunes enrichment worker initialized (paused — restored from config)")
+ print("iTunes enrichment worker initialized (paused — restored from config)")
else:
- print("✅ iTunes enrichment worker initialized and started")
+ print("iTunes enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ iTunes enrichment worker initialization failed: {e}")
+ print(f"iTunes enrichment worker initialization failed: {e}")
itunes_enrichment_worker = None
# --- iTunes API Endpoints ---
@@ -49557,11 +49557,11 @@ try:
lastfm_worker.start()
if config_manager.get('lastfm_enrichment_paused', False):
lastfm_worker.pause()
- print("✅ Last.fm enrichment worker initialized (paused — restored from config)")
+ print("Last.fm enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Last.fm enrichment worker initialized and started")
+ print("Last.fm enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Last.fm worker initialization failed: {e}")
+ print(f"Last.fm worker initialization failed: {e}")
lastfm_worker = None
# --- Last.fm API Endpoints ---
@@ -49700,11 +49700,11 @@ try:
genius_worker.paused = True
genius_worker.start()
if genius_worker.paused:
- print("✅ Genius enrichment worker initialized (paused — restored from config)")
+ print("Genius enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Genius enrichment worker initialized and started")
+ print("Genius enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Genius worker initialization failed: {e}")
+ print(f"Genius worker initialization failed: {e}")
genius_worker = None
# --- Genius API Endpoints ---
@@ -49777,11 +49777,11 @@ try:
tidal_enrichment_worker.start()
if config_manager.get('tidal_enrichment_paused', False):
tidal_enrichment_worker.pause()
- print("✅ Tidal enrichment worker initialized (paused — restored from config)")
+ print("Tidal enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Tidal enrichment worker initialized and started")
+ print("Tidal enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Tidal worker initialization failed: {e}")
+ print(f"Tidal worker initialization failed: {e}")
tidal_enrichment_worker = None
# --- Tidal Enrichment API Endpoints ---
@@ -49851,11 +49851,11 @@ try:
qobuz_enrichment_worker.start()
if config_manager.get('qobuz_enrichment_paused', False):
qobuz_enrichment_worker.pause()
- print("✅ Qobuz enrichment worker initialized (paused — restored from config)")
+ print("Qobuz enrichment worker initialized (paused — restored from config)")
else:
- print("✅ Qobuz enrichment worker initialized and started")
+ print("Qobuz enrichment worker initialized and started")
except Exception as e:
- print(f"⚠️ Qobuz worker initialization failed: {e}")
+ print(f"Qobuz worker initialization failed: {e}")
qobuz_enrichment_worker = None
# --- Qobuz Enrichment API Endpoints ---
@@ -49929,13 +49929,13 @@ try:
hydrabase_worker = HydrabaseWorker(get_ws_and_lock=_get_hydrabase_ws_and_lock)
hydrabase_worker.start()
hydrabase_client = HydrabaseClient(get_ws_and_lock=_get_hydrabase_ws_and_lock)
- print("✅ Hydrabase P2P mirror worker and metadata client initialized")
+ print("Hydrabase P2P mirror worker and metadata client initialized")
# Update API blueprint references
if hasattr(app, 'soulsync'):
app.soulsync['hydrabase_client'] = hydrabase_client
app.soulsync['hydrabase_worker'] = hydrabase_worker
except Exception as e:
- print(f"⚠️ Hydrabase initialization failed: {e}")
+ print(f"Hydrabase initialization failed: {e}")
hydrabase_worker = None
hydrabase_client = None
@@ -49952,9 +49952,9 @@ try:
_hydrabase_ws = _auto_ws
# Don't auto-enable dev mode — user must explicitly activate dev mode
# Auto-connect just establishes the WebSocket for fallback/search tab use
- print(f"✅ Hydrabase auto-connected to {_hydra_cfg['url']}")
+ print(f"Hydrabase auto-connected to {_hydra_cfg['url']}")
except Exception as e:
- print(f"⚠️ Hydrabase auto-reconnect failed: {e}")
+ print(f"Hydrabase auto-reconnect failed: {e}")
# --- Hydrabase Worker API Endpoints ---
@@ -50026,9 +50026,9 @@ try:
soulid_db = MusicDatabase()
soulid_worker = SoulIDWorker(database=soulid_db)
soulid_worker.start()
- print("✅ SoulID worker initialized and started")
+ print("SoulID worker initialized and started")
except Exception as e:
- print(f"⚠️ SoulID worker initialization failed: {e}")
+ print(f"SoulID worker initialization failed: {e}")
soulid_worker = None
@app.route('/api/soulid/status', methods=['GET'])
@@ -50060,9 +50060,9 @@ try:
navidrome_client=navidrome_client,
)
listening_stats_worker.start()
- print("✅ Listening stats worker initialized and started")
+ print("Listening stats worker initialized and started")
except Exception as e:
- print(f"⚠️ Listening stats worker initialization failed: {e}")
+ print(f"Listening stats worker initialization failed: {e}")
listening_stats_worker = None
# --- Stats API Endpoints ---
@@ -50353,13 +50353,13 @@ def listening_stats_sync():
import threading
def _do_sync():
try:
- print("🔄 [Stats Sync] Starting manual poll...")
+ print("[Stats Sync] Starting manual poll...")
listening_stats_worker._poll()
listening_stats_worker.stats['polls_completed'] += 1
listening_stats_worker.stats['last_poll'] = time.strftime('%Y-%m-%d %H:%M:%S')
- print("✅ [Stats Sync] Manual poll completed")
+ print("[Stats Sync] Manual poll completed")
except Exception as e:
- print(f"❌ [Stats Sync] Manual poll failed: {e}")
+ print(f"[Stats Sync] Manual poll failed: {e}")
import traceback
traceback.print_exc()
logger.error(f"Manual stats sync failed: {e}")
@@ -50449,9 +50449,9 @@ try:
repair_worker._progress_lock_ref = repair_job_progress_lock
repair_worker._progress_states_ref = repair_job_progress_states
repair_worker.start()
- print("✅ Repair worker initialized and started")
+ print("Repair worker initialized and started")
except Exception as e:
- print(f"⚠️ Repair worker initialization failed: {e}")
+ print(f"Repair worker initialization failed: {e}")
repair_worker = None
# --- Repair Worker API Endpoints ---
@@ -51297,7 +51297,7 @@ def import_album_process():
errors.append(err_msg)
logger.error(f"Import processing error: {err_msg}")
- add_activity_item("📥", "Album Imported", f"{album_name} by {artist_name} ({processed}/{len(matches)} tracks)", "Now")
+ add_activity_item("", "Album Imported", f"{album_name} by {artist_name} ({processed}/{len(matches)} tracks)", "Now")
# Emit events through automation engine — same chain as download batches
# batch_complete → auto-scan → library_scan_completed → auto-update DB
@@ -51557,7 +51557,7 @@ def import_singles_process():
errors.append(err_msg)
logger.error(f"Import single processing error: {err_msg}")
- add_activity_item("📥", "Singles Imported", f"{processed}/{len(files)} tracks processed", "Now")
+ add_activity_item("", "Singles Imported", f"{processed}/{len(files)} tracks processed", "Now")
# Emit events through automation engine — same chain as download batches
# batch_complete → auto-scan → library_scan_completed → auto-update DB
@@ -51815,13 +51815,13 @@ def _hydrabase_reconnect_loop():
)
_hydrabase_ws = ws
_consecutive_failures = 0
- print(f"🔄 [Hydrabase] Auto-reconnected to {hydra_cfg['url']}")
+ print(f"[Hydrabase] Auto-reconnected to {hydra_cfg['url']}")
except Exception as e:
_consecutive_failures += 1
if _consecutive_failures <= 3:
- print(f"⚠️ [Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}")
+ print(f"[Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}")
elif _consecutive_failures == 4:
- print(f"⚠️ [Hydrabase] Reconnect failing repeatedly — suppressing further logs until success")
+ print(f"[Hydrabase] Reconnect failing repeatedly — suppressing further logs until success")
except Exception:
pass # Don't crash the monitor loop
@@ -52363,30 +52363,30 @@ if __name__ == '__main__':
log_path = config_manager.get('logging.path', 'logs/app.log')
logger = setup_logging(log_level, log_path)
- print("🚀 Starting SoulSync Web UI Server...")
+ print("Starting SoulSync Web UI Server...")
print("Open your browser and navigate to http://127.0.0.1:8008")
# Start OAuth callback servers
- print("🔧 Starting OAuth callback servers...")
+ print("Starting OAuth callback servers...")
start_oauth_callback_servers()
# Startup diagnostics: Check and recover stuck flags
- print("🔍 Running startup diagnostics...")
+ print("Running startup diagnostics...")
stuck_flags_recovered = check_and_recover_stuck_flags()
if stuck_flags_recovered:
- print("⚠️ Recovered stuck flags from previous session")
+ print("Recovered stuck flags from previous session")
else:
- print("✅ No stuck flags detected - system healthy")
+ print("No stuck flags detected - system healthy")
# Start simple background monitor when server starts
- print("🔧 Starting simple background monitor...")
+ print("Starting simple background monitor...")
start_simple_background_monitor()
- print("✅ Simple background monitor started (includes automatic search cleanup)")
+ print("Simple background monitor started (includes automatic search cleanup)")
# Wishlist/watchlist timers are now managed by AutomationEngine system automations
# Pre-build import suggestions cache in background
- print("🔧 Pre-building import suggestions cache...")
+ print("Pre-building import suggestions cache...")
start_import_suggestions_cache()
# Initialize app start time for uptime tracking
@@ -52397,26 +52397,26 @@ if __name__ == '__main__':
_register_automation_handlers()
if automation_engine:
try:
- print("🔧 Starting automation engine...")
+ print("Starting automation engine...")
automation_engine.start()
- print("✅ Automation engine started")
+ print("Automation engine started")
try:
automation_engine.emit('app_started', {})
except Exception:
pass
except AttributeError as e:
- print(f"⚠️ Automation engine failed to start: {e}")
+ print(f"Automation engine failed to start: {e}")
print(" If using Docker, check that your volume mount is /app/data (not /app/database)")
logger.error(f"Automation engine start error (possible stale Docker volume): {e}")
except Exception as e:
- print(f"⚠️ Automation engine failed to start: {e}")
+ print(f"Automation engine failed to start: {e}")
logger.error(f"Automation engine start error: {e}")
# Add startup activity
- add_activity_item("🚀", "System Started", "SoulSync Web UI Server initialized", "Now")
+ add_activity_item("", "System Started", "SoulSync Web UI Server initialized", "Now")
# Start WebSocket background emitters
- print("🔧 Starting WebSocket background emitters...")
+ print("Starting WebSocket background emitters...")
# Phase 1: Global pollers
socketio.start_background_task(_emit_service_status_loop)
socketio.start_background_task(_emit_watchlist_count_loop)
@@ -52442,6 +52442,6 @@ if __name__ == '__main__':
socketio.start_background_task(_hydrabase_reconnect_loop)
# API Rate Monitor — 1s push for speedometer gauges
socketio.start_background_task(_emit_rate_monitor_loop)
- print("✅ WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor)")
+ print("WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor)")
socketio.run(app, host='0.0.0.0', port=8008, debug=False, allow_unsafe_werkzeug=True)