Update web_server.py

This commit is contained in:
Broque Thomas 2025-08-25 19:42:50 -07:00
parent 7817e40f31
commit ef6ad49fa3

View file

@ -120,6 +120,14 @@ download_tasks = {} # task_id -> task state dict
download_batches = {} # batch_id -> {queue, active_count, max_concurrent}
tasks_lock = threading.Lock()
# --- Album Grouping State Management (Ported from GUI) ---
# Thread-safe album grouping for consistent naming across tracks
album_cache_lock = threading.Lock()
album_groups = {} # album_key -> final_album_name
album_artists = {} # album_key -> artist_name
album_editions = {} # album_key -> "standard" or "deluxe"
album_name_cache = {} # album_key -> cached_final_name
def _prepare_stream_task(track_data):
"""
Background streaming task that downloads track to Stream folder and updates global state.
@ -1551,10 +1559,14 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
if download_id:
context_key = f"{username}::{filename}"
with matched_context_lock:
# Enhanced context storage with Spotify clean titles (GUI parity)
enhanced_context = individual_track_context.copy()
enhanced_context['spotify_clean_title'] = individual_track_context.get('title', '')
matched_downloads_context[context_key] = {
"spotify_artist": spotify_artist,
"spotify_album": spotify_album,
"original_search_result": individual_track_context, # Contains corrected data
"original_search_result": enhanced_context, # Contains corrected data + clean title
"is_album_download": True
}
print(f" + Queued track: {filename} (Matched to: '{corrected_meta.get('title')}')")
@ -1618,10 +1630,14 @@ def start_matched_download():
# THE FIX: We preserve the spotify_album context if it was provided.
# For a regular single, spotify_album will be None.
# For an album track, it will contain the album's data.
# Enhanced context storage with Spotify clean titles (GUI parity)
enhanced_payload = download_payload.copy()
enhanced_payload['spotify_clean_title'] = download_payload.get('title', '')
matched_downloads_context[context_key] = {
"spotify_artist": spotify_artist,
"spotify_album": spotify_album, # PRESERVE album context
"original_search_result": download_payload,
"original_search_result": enhanced_payload,
"is_album_download": False # It's a single track download, not a full album job.
}
return jsonify({"success": True, "message": "Matched download started"})
@ -1796,14 +1812,19 @@ def _search_track_in_album_context(original_search: dict, artist: dict) -> dict:
def _detect_album_info_web(context: dict, artist: dict) -> dict:
"""
This is the final, corrected version that ensures the official Spotify track
number from the context is always prioritized for matched album downloads,
fixing the track numbering issue by mirroring the logic from downloads.py.
Enhanced album detection with GUI parity - multi-priority logic.
(Updated to match GUI downloads.py logic exactly)
"""
try:
original_search = context.get("original_search_result", {})
spotify_album_context = context.get("spotify_album")
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" Has album attr: {bool(original_search.get('album'))}")
if original_search.get('album'):
print(f" Album value: '{original_search.get('album')}'")
# --- THIS IS THE CRITICAL FIX ---
# If this is part of a matched album download, we TRUST the context data completely.
@ -1826,40 +1847,114 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
'album_image_url': spotify_album_context.get('image_url')
}
# This fallback block handles single tracks. It was already working correctly.
# It performs a live Spotify search to determine if a single is part of an album.
print(" Single track context. Performing live Spotify search for album info.")
cleaned_title = _clean_track_title(original_search.get('title', ''), artist['name'])
query = f"artist:\"{artist['name']}\" track:\"{cleaned_title}\""
tracks = spotify_client.search_tracks(query, limit=1)
# PRIORITY 1: Try album-aware search if we have album context
if original_search.get('album') and original_search.get('album').strip() and original_search.get('album') != "Unknown Album":
print(f"🎯 ALBUM-AWARE SEARCH: Looking for '{original_search.get('title')}' in album '{original_search.get('album')}'")
album_result = _search_track_in_album_context_web(context, artist)
if album_result:
print(f"✅ Found track in album context - using album classification")
return album_result
else:
print(f"⚠️ Track not found in album context, falling back to individual search")
if not tracks:
print("⚠️ No Spotify match found, defaulting to single.")
return {'is_album': False, 'clean_track_name': cleaned_title, 'album_name': cleaned_title, 'track_number': 1}
# PRIORITY 2: Fallback to individual track search for clean metadata
print(f"🔍 Searching Spotify for individual track info (PRIORITY 2)...")
# Clean the track title before searching - remove artist prefix
clean_title = _clean_track_title_web(original_search.get('title', ''), artist_name)
print(f"🧹 Cleaned title: '{original_search.get('title')}' -> '{clean_title}'")
# Search for the track by artist and cleaned title
query = f"artist:{artist_name} track:{clean_title}"
tracks = spotify_client.search_tracks(query, limit=5)
# Find the best matching track
best_match = None
best_confidence = 0
if tracks:
from core.matching_engine import matching_engine
for track in tracks:
# Calculate confidence based on artist and title similarity
artist_confidence = matching_engine.similarity_score(
matching_engine.normalize_string(artist_name),
matching_engine.normalize_string(track.artists[0] if track.artists else '')
)
title_confidence = matching_engine.similarity_score(
matching_engine.normalize_string(clean_title),
matching_engine.normalize_string(track.name)
)
combined_confidence = (artist_confidence * 0.6 + title_confidence * 0.4)
if combined_confidence > best_confidence and combined_confidence > 0.6: # Lower threshold for better matches
best_match = track
best_confidence = combined_confidence
best_match = tracks[0]
detailed_track = spotify_client.get_track_details(best_match.id)
if not detailed_track:
print("⚠️ Could not get detailed track info, defaulting to single.")
return {'is_album': False, 'clean_track_name': best_match.name, 'album_name': best_match.name, 'track_number': 1}
api_album = detailed_track.get('album', {})
album_type = api_album.get('album_type', 'single')
total_tracks = api_album.get('total_tracks', 1)
is_album = (album_type == 'album' and total_tracks > 1 and matching_engine.similarity_score(api_album.get('name'), best_match.name) < 0.9)
album_image_url = api_album.get('images', [{}])[0].get('url') if api_album.get('images') else None
# If we found a good Spotify match, use it for clean metadata
if best_match and best_confidence > 0.6:
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}")
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")
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}'")
# Enhanced album detection using detailed API data
is_album = (
# Album type is 'album' (not 'single')
album_type == 'album' and
# Album has multiple tracks
total_tracks > 1 and
# Album name is different from track name (avoid self-titled singles)
matching_engine.similarity_score(album_name.lower(), clean_track_name.lower()) < 0.9
)
album_image_url = None
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})")
return {
'is_album': is_album,
'album_name': album_name,
'track_number': spotify_track_number,
'clean_track_name': clean_track_name,
'album_image_url': album_image_url,
'confidence': best_confidence,
'source': 'spotify_api_detailed'
}
# Fallback: Use original data with basic cleaning
print("⚠️ No good Spotify match found, using original data")
fallback_title = _clean_track_title_web(original_search.get('title', 'Unknown Track'), artist_name)
return {
'is_album': is_album,
'album_name': api_album.get('name', best_match.name),
'track_number': detailed_track.get('track_number', 1),
'clean_track_name': best_match.name,
'album_image_url': album_image_url
'is_album': False,
'clean_track_name': fallback_title,
'album_name': fallback_title,
'track_number': 1,
'confidence': 0.0,
'source': 'fallback_original'
}
except Exception as e:
print(f"❌ Error in _detect_album_info_web: {e}")
clean_title = _clean_track_title(context.get("original_search_result", {}).get('title', 'Unknown'), artist.get('name', ''))
clean_title = _clean_track_title_web(context.get("original_search_result", {}).get('title', 'Unknown'), artist.get('name', ''))
return {'is_album': False, 'clean_track_name': clean_title, 'album_name': clean_title, 'track_number': 1}
@ -1882,6 +1977,333 @@ def _cleanup_empty_directories(download_path, moved_file_path):
print(f"Warning: An error occurred during directory cleanup: {e}")
# ===================================================================
# ALBUM GROUPING SYSTEM (Ported from GUI downloads.py)
# ===================================================================
def _get_base_album_name(album_name: str) -> str:
"""
Extract the base album name without edition indicators.
E.g., 'good kid, m.A.A.d city (Deluxe Edition)' -> 'good kid, m.A.A.d city'
"""
import re
# Remove common edition suffixes
base_name = album_name
# Remove edition indicators in parentheses or brackets
base_name = re.sub(r'\s*[\[\(](deluxe|special|expanded|extended|bonus|remastered|anniversary|collectors?|limited).*?[\]\)]\s*$', '', base_name, flags=re.IGNORECASE)
# Remove standalone edition words at the end
base_name = re.sub(r'\s+(deluxe|special|expanded|extended|bonus|remastered|anniversary|collectors?|limited)\s*(edition)?\s*$', '', base_name, flags=re.IGNORECASE)
return base_name.strip()
def _detect_deluxe_edition(album_name: str) -> bool:
"""
Detect if an album name indicates a deluxe/special edition.
Returns True if it's a deluxe variant, False for standard.
"""
if not album_name:
return False
album_lower = album_name.lower()
# Check for deluxe indicators
deluxe_indicators = [
'deluxe',
'deluxe edition',
'special edition',
'expanded edition',
'extended edition',
'bonus',
'remastered',
'anniversary',
'collectors edition',
'limited edition'
]
for indicator in deluxe_indicators:
if indicator in album_lower:
print(f"🎯 Detected deluxe edition: '{album_name}' contains '{indicator}'")
return True
return False
def _normalize_base_album_name(base_album: str, artist_name: str) -> str:
"""
Normalize the base album name to handle case variations and known corrections.
"""
import re
# Apply known album corrections for consistent naming
normalized_lower = base_album.lower().strip()
# Handle common album title variations
known_corrections = {
# Add specific album name corrections here as needed
# Example: "good kid maad city": "good kid, m.A.A.d city"
}
# 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}'")
return correction
# Handle punctuation variations
normalized = base_album
# Normalize common punctuation patterns
normalized = re.sub(r'\s*&\s*', ' & ', normalized) # Standardize & spacing
normalized = re.sub(r'\s+', ' ', normalized) # Clean multiple spaces
normalized = normalized.strip()
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:
"""
Smart album grouping: Start with standard, upgrade to deluxe if ANY track is deluxe.
This ensures all tracks from the same album get the same folder name.
(Adapted from GUI downloads.py)
"""
try:
with album_cache_lock:
artist_name = spotify_artist["name"]
detected_album = album_info.get('album_name', '')
# Extract base album name (without edition indicators)
if detected_album:
base_album = _get_base_album_name(detected_album)
elif original_album:
# Clean the original Soulseek album name
cleaned_original = _clean_album_title_web(original_album, artist_name)
base_album = _get_base_album_name(cleaned_original)
else:
base_album = _get_base_album_name(detected_album)
# Normalize the base name (handle case variations, etc.)
base_album = _normalize_base_album_name(base_album, artist_name)
# Create a key for this album group (artist + base album)
album_key = f"{artist_name}::{base_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}'")
return cached_name
print(f"🔍 Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
# Check if this track indicates a deluxe edition
is_deluxe_track = False
if detected_album:
is_deluxe_track = _detect_deluxe_edition(detected_album)
elif original_album:
is_deluxe_track = _detect_deluxe_edition(original_album)
# Get current edition level for this album group (default to standard)
current_edition = album_editions.get(album_key, "standard")
# 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!")
album_editions[album_key] = "deluxe"
current_edition = "deluxe"
# Build final album name based on edition level
if current_edition == "deluxe":
final_album_name = f"{base_album} (Deluxe Edition)"
else:
final_album_name = base_album
# Store the resolution in both caches
album_groups[album_key] = final_album_name
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})")
return final_album_name
except Exception as 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:
"""Clean up album title by removing common prefixes, suffixes, and artist redundancy"""
import re
# Start with the original title
original = album_title.strip()
cleaned = original
print(f"🧹 Album Title Cleaning: '{original}' (artist: '{artist_name}')")
# Remove "Album - " prefix
cleaned = re.sub(r'^Album\s*-\s*', '', cleaned, flags=re.IGNORECASE)
# Remove artist name prefix if it appears at the beginning
# This handles cases like "Kendrick Lamar - good kid, m.A.A.d city"
artist_pattern = re.escape(artist_name) + r'\s*-\s*'
cleaned = re.sub(f'^{artist_pattern}', '', cleaned, flags=re.IGNORECASE)
# Remove common Soulseek suffixes in square brackets and parentheses
# Examples: [Deluxe Edition] [2012] [320 Kbps] [Album+iTunes+Bonus Tracks] [F10]
# (Deluxe Edition) (2012) (320 Kbps) etc.
# Remove year patterns like [2012], (2020), etc.
cleaned = re.sub(r'\s*[\[\(]\d{4}[\]\)]\s*', ' ', cleaned)
# Remove quality/format indicators
quality_patterns = [
r'\s*[\[\(].*?320.*?kbps.*?[\]\)]\s*',
r'\s*[\[\(].*?256.*?kbps.*?[\]\)]\s*',
r'\s*[\[\(].*?flac.*?[\]\)]\s*',
r'\s*[\[\(].*?mp3.*?[\]\)]\s*',
r'\s*[\[\(].*?itunes.*?[\]\)]\s*',
r'\s*[\[\(].*?web.*?[\]\)]\s*',
r'\s*[\[\(].*?cd.*?[\]\)]\s*'
]
for pattern in quality_patterns:
cleaned = re.sub(pattern, ' ', cleaned, flags=re.IGNORECASE)
# Remove common edition indicators (but preserve them for deluxe detection above)
# This happens AFTER deluxe detection to avoid interfering with that logic
# Clean up spacing
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
# Remove leading/trailing punctuation
cleaned = re.sub(r'^[-\s]+|[-\s]+$', '', 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:
"""
Search for a track within its album context to avoid promotional single confusion.
(Ported from GUI downloads.py)
"""
try:
from core.spotify_client import spotify_client
from core.matching_engine import matching_engine
# Get album and track info from context
original_search = context.get("original_search_result", {})
album_name = original_search.get("album")
track_title = original_search.get("title")
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})")
return None
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)
clean_track = _clean_track_title_web(track_title, artist_name)
# Search for the specific album first
album_query = f"album:{clean_album} artist:{artist_name}"
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}")
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)")
# 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}")
continue
# Check if our track is in this album
for track_data in album_tracks_data['items']:
track_name = track_data['name']
track_number = track_data['track_number']
# Calculate similarity between our track and this album track
similarity = matching_engine.similarity_score(
matching_engine.normalize_string(clean_track),
matching_engine.normalize_string(track_name)
)
# Use higher threshold for remix matching to ensure precision
is_remix = any(word in clean_track.lower() for word in ['remix', 'mix', 'edit', 'version'])
threshold = 0.9 if is_remix else 0.7 # Much stricter for remixes
if similarity > threshold:
print(f"✅ FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})")
print(f"🎯 Forcing album classification for track in '{album.name}'")
# Return album info - force album classification!
return {
'is_album': True, # Always true - we found it in an album!
'album_name': album.name,
'track_number': track_number,
'clean_track_name': clean_track, # Use the ORIGINAL download title, not the database match
'album_image_url': album.image_url,
'confidence': similarity,
'source': 'album_context_search'
}
print(f"❌ Track '{clean_track}' not found in album '{album.name}'")
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}")
return None
def _clean_track_title_web(track_title: str, artist_name: str) -> str:
"""Clean up track title by removing artist prefix and common patterns"""
import re
# Start with the original title
original = track_title.strip()
cleaned = original
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."
artist_pattern = re.escape(artist_name) + r'\s*-\s*'
cleaned = re.sub(f'^{artist_pattern}', '', cleaned, flags=re.IGNORECASE)
# Remove common prefixes
cleaned = re.sub(r'^Track\s*\d*\s*-\s*', '', cleaned, flags=re.IGNORECASE)
cleaned = re.sub(r'^\d+\.\s*', '', cleaned) # Remove track numbers like "01. "
# Remove quality/format indicators
quality_patterns = [
r'\s*[\[\(].*?320.*?kbps.*?[\]\)]\s*',
r'\s*[\[\(].*?256.*?kbps.*?[\]\)]\s*',
r'\s*[\[\(].*?flac.*?[\]\)]\s*',
r'\s*[\[\(].*?mp3.*?[\]\)]\s*',
r'\s*[\[\(].*?explicit.*?[\]\)]\s*'
]
for pattern in quality_patterns:
cleaned = re.sub(pattern, ' ', cleaned, flags=re.IGNORECASE)
# Clean up spacing
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
# Remove leading/trailing punctuation
cleaned = re.sub(r'^[-\s]+|[-\s]+$', '', cleaned)
print(f"🧹 Track Title Result: '{original}' -> '{cleaned}'")
return cleaned if cleaned else original
# ===================================================================
# METADATA & COVER ART HELPERS (Ported from downloads.py)
@ -2137,17 +2559,38 @@ def _post_process_matched_download(context_key, context, file_path):
print("✅ Matched Album context found. Building info directly from context.")
original_search = context.get("original_search_result", {})
spotify_album = context.get("spotify_album", {})
# Use Spotify clean title if available (GUI parity)
clean_track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track')
album_info = {
'is_album': True,
'album_name': spotify_album.get('name'),
'track_number': original_search.get('track_number', 1),
'clean_track_name': original_search.get('title', 'Unknown Track'),
'clean_track_name': clean_track_name,
'album_image_url': spotify_album.get('image_url')
}
else:
# For singles, we still need to detect if they belong to an album.
album_info = _detect_album_info_web(context, spotify_artist)
# --- CRITICAL FIX: Add GUI album grouping resolution ---
# This ensures consistent album naming across tracks like the GUI
if album_info and album_info['is_album']:
print(f"\n🎯 SMART 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
original_album = None
if context.get("original_search_result", {}).get("album"):
original_album = context["original_search_result"]["album"]
# Use the GUI's smart album grouping algorithm
consistent_album_name = _resolve_album_group(spotify_artist, album_info, original_album)
album_info['album_name'] = consistent_album_name
print(f" Final album name: '{consistent_album_name}'")
print(f"🔗 ✅ Album grouping complete!\n")
# 1. Get transfer path and create artist directory
transfer_dir = config_manager.get('soulseek.transfer_path', './Transfer')
artist_name_sanitized = _sanitize_filename(spotify_artist["name"])
@ -2156,10 +2599,28 @@ def _post_process_matched_download(context_key, context, file_path):
file_ext = os.path.splitext(file_path)[1]
# 2. Build the final path (this logic is now correct because album_info is correct)
# 2. Build the final path using GUI-style track naming with multiple fallback sources
if album_info and album_info['is_album']:
album_name_sanitized = _sanitize_filename(album_info['album_name'])
final_track_name_sanitized = _sanitize_filename(album_info['clean_track_name'])
# --- GUI PARITY: Use multiple sources for clean track name ---
original_search = context.get("original_search_result", {})
clean_track_name = album_info['clean_track_name']
# 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}'")
# 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}'")
# 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}'")
final_track_name_sanitized = _sanitize_filename(clean_track_name)
track_number = album_info['track_number']
# Fix: Handle None track_number
@ -2177,15 +2638,41 @@ def _post_process_matched_download(context_key, context, file_path):
album_dir = os.path.join(artist_dir, album_folder_name)
os.makedirs(album_dir, exist_ok=True)
# Create track filename with number (just track number + clean title, NO artist)
new_filename = f"{track_number:02d} - {final_track_name_sanitized}{file_ext}"
final_path = os.path.join(album_dir, new_filename)
print(f"📁 Album folder created: '{album_folder_name}'")
print(f"🎵 Track filename: '{new_filename}'")
else:
final_track_name_sanitized = _sanitize_filename(album_info['clean_track_name'])
# Single track structure: Transfer/ARTIST/ARTIST - SINGLE/SINGLE.ext
# --- GUI PARITY: Use multiple sources for clean track name ---
original_search = context.get("original_search_result", {})
clean_track_name = album_info['clean_track_name']
# 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}'")
# 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}'")
# 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}'")
final_track_name_sanitized = _sanitize_filename(clean_track_name)
single_folder_name = f"{artist_name_sanitized} - {final_track_name_sanitized}"
single_dir = os.path.join(artist_dir, single_folder_name)
os.makedirs(single_dir, exist_ok=True)
# Create single filename with clean track name
new_filename = f"{final_track_name_sanitized}{file_ext}"
final_path = os.path.join(single_dir, new_filename)
print(f"📁 Single track: {single_folder_name}/{new_filename}")
# 3. Enhance metadata, move file, download art, and cleanup
_enhance_file_metadata(file_path, context, spotify_artist, album_info)
@ -2706,10 +3193,18 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
# Store context for post-processing
context_key = f"{username}::{filename}"
with matched_context_lock:
# Enhanced context storage with Spotify clean titles (GUI parity)
enhanced_payload = download_payload.copy()
# Try to get clean title from track object if available
if track and hasattr(track, 'name'):
enhanced_payload['spotify_clean_title'] = track.name
else:
enhanced_payload['spotify_clean_title'] = enhanced_payload.get('title', '')
matched_downloads_context[context_key] = {
"spotify_artist": spotify_artist_context,
"spotify_album": spotify_album_context,
"original_search_result": download_payload,
"original_search_result": enhanced_payload,
"is_album_download": False
}