search...")
genre_links = menu.find_all('a', href=re.compile(r'/genre/[^/]+/\d+'))
if genre_links:
- print(f"Found {len(genre_links)} genre links in menu (fallback method)")
+ _beatport_log(f"Found {len(genre_links)} genre links in menu (fallback method)")
for link in genre_links:
href = link.get('href', '')
name_text = link.get_text(strip=True)
@@ -266,16 +301,16 @@ class BeatportUnifiedScraper:
'id': genre_id,
'url': urljoin(self.base_url, href)
})
- print(f" Added: {name} ({slug}/{genre_id})")
+ _beatport_log(f" Added: {name} ({slug}/{genre_id})")
else:
- print(f"No genre links found in dropdown_menu {menu_idx + 1}")
+ _beatport_log(f"No genre links found in dropdown_menu {menu_idx + 1}")
if genres:
- print(f"Successfully extracted {len(genres)} genres from dropdown menu")
+ _beatport_log(f"Successfully extracted {len(genres)} genres from dropdown menu")
else:
- print("No genre links found in dropdown menu structure")
+ _beatport_log("No genre links found in dropdown menu structure")
else:
- print("Could not find genres-dropdown-menu, trying fallback methods...")
+ _beatport_log("Could not find genres-dropdown-menu, trying fallback methods...")
# Fallback: Look for other potential dropdown structures
potential_dropdowns = [
@@ -289,11 +324,11 @@ class BeatportUnifiedScraper:
for dropdown in potential_dropdowns:
if dropdown:
- print(f"Found fallback dropdown: {dropdown.name} with class {dropdown.get('class')}")
+ _beatport_log(f"Found fallback dropdown: {dropdown.name} with class {dropdown.get('class')}")
genre_links = dropdown.find_all('a', href=re.compile(r'/genre/[^/]+/\d+'))
if genre_links:
- print(f"Found {len(genre_links)} genre links in fallback dropdown")
+ _beatport_log(f"Found {len(genre_links)} genre links in fallback dropdown")
for link in genre_links:
href = link.get('href', '')
name_text = link.get_text(strip=True)
@@ -313,14 +348,14 @@ class BeatportUnifiedScraper:
})
if genres:
- print(f"Successfully extracted {len(genres)} genres from fallback dropdown")
+ _beatport_log(f"Successfully extracted {len(genres)} genres from fallback dropdown")
break
# Method 2: Look for any genre links on the page
if not genres:
- print("Dropdown not found, searching for genre links...")
+ _beatport_log("Dropdown not found, searching for genre links...")
all_genre_links = soup.find_all('a', href=re.compile(r'/genre/[^/]+/\d+'))
- print(f"Found {len(all_genre_links)} potential genre links on page")
+ _beatport_log(f"Found {len(all_genre_links)} potential genre links on page")
seen_genres = set()
for link in all_genre_links:
@@ -343,18 +378,18 @@ class BeatportUnifiedScraper:
# Method 3: Try to find a genres page link and scrape from there
if not genres:
- print("Searching for genres page...")
+ _beatport_log("Searching for genres page...")
genres_page_link = soup.find('a', href=re.compile(r'/genres$')) or \
soup.find('a', href=re.compile(r'/browse.*genre', re.I))
if genres_page_link:
genres_page_url = urljoin(self.base_url, genres_page_link['href'])
- print(f"Found genres page: {genres_page_url}")
+ _beatport_log(f"Found genres page: {genres_page_url}")
genres_soup = self.get_page(genres_page_url)
if genres_soup:
genre_links = genres_soup.find_all('a', href=re.compile(r'/genre/[^/]+/\d+'))
- print(f"Found {len(genre_links)} genre links on genres page")
+ _beatport_log(f"Found {len(genre_links)} genre links on genres page")
seen_genres = set()
for link in genre_links:
@@ -386,19 +421,19 @@ class BeatportUnifiedScraper:
final_genres = list(unique_genres.values())
final_genres.sort(key=lambda x: x['name'])
- print(f"Discovered {len(final_genres)} unique genres from homepage")
+ _beatport_log(f"Discovered {len(final_genres)} unique genres from homepage")
return final_genres
else:
- print("No genres found, using fallback list")
+ _beatport_log("No genres found, using fallback list")
return self.fallback_genres
except Exception as e:
- print(f"Error discovering genres: {e}")
+ _beatport_log(f"Error discovering genres: {e}")
return self.fallback_genres
def discover_chart_sections(self) -> Dict[str, List[Dict]]:
"""Dynamically discover chart sections from homepage"""
- print("Discovering chart sections from Beatport homepage...")
+ _beatport_log("Discovering chart sections from Beatport homepage...")
soup = self.get_page(self.base_url)
if not soup:
@@ -411,7 +446,7 @@ class BeatportUnifiedScraper:
}
# Method 1: Find H2 section headings
- print(" Finding H2 section headings...")
+ _beatport_log(" Finding H2 section headings...")
h2_headings = soup.find_all('h2')
for heading in h2_headings:
@@ -426,10 +461,10 @@ class BeatportUnifiedScraper:
# Categorize into our three main groups
category = self._categorize_chart_section(text)
chart_sections[category].append(section_info)
- print(f" Found: '{text}' -> {category}")
+ _beatport_log(f" Found: '{text}' -> {category}")
# Method 2: Find specific chart links
- print(" Finding chart page links...")
+ _beatport_log(" Finding chart page links...")
chart_links = []
# Look for the specific links we discovered
@@ -450,10 +485,10 @@ class BeatportUnifiedScraper:
'expected': link_info['expected_href'],
'matches_expected': href == link_info['expected_href']
})
- print(f" Found: '{link.get_text(strip=True)}' -> {href}")
+ _beatport_log(f" Found: '{link.get_text(strip=True)}' -> {href}")
# Method 3: Count individual DJ charts
- print(" Counting individual DJ charts...")
+ _beatport_log(" Counting individual DJ charts...")
dj_chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
individual_dj_charts = []
@@ -467,7 +502,7 @@ class BeatportUnifiedScraper:
'full_url': urljoin(self.base_url, href)
})
- print(f" Found {len(dj_chart_links)} individual DJ charts")
+ _beatport_log(f" Found {len(dj_chart_links)} individual DJ charts")
return {
'sections': chart_sections,
@@ -529,20 +564,20 @@ class BeatportUnifiedScraper:
for img in artwork_imgs:
src = img.get('src', '')
if 'geo-media' in src and ('1050x508' in src or '500x500' in src):
- print(f" Found high-quality artwork image: {src}")
+ _beatport_log(f" Found high-quality artwork image: {src}")
return src
# Second, try any geo-media images in artwork containers
for img in artwork_imgs:
src = img.get('src', '')
if 'geo-media' in src:
- print(f" Found geo-media artwork image: {src}")
+ _beatport_log(f" Found geo-media artwork image: {src}")
return src
# Third, use any artwork image as fallback
first_artwork_src = artwork_imgs[0].get('src', '')
if first_artwork_src:
- print(f" Found artwork image (fallback): {first_artwork_src}")
+ _beatport_log(f" Found artwork image (fallback): {first_artwork_src}")
return first_artwork_src
# Priority 2: Original method - Look for hero release slideshow images
@@ -553,19 +588,19 @@ class BeatportUnifiedScraper:
for img in hero_images:
src = img.get('src', '')
if '1050x508' in src or '500x500' in src:
- print(f" Found high-quality hero image: {src}")
+ _beatport_log(f" Found high-quality hero image: {src}")
return src
# Fallback to any geo-media image
fallback_src = hero_images[0].get('src', '')
- print(f" Found hero image (fallback): {fallback_src}")
+ _beatport_log(f" Found hero image (fallback): {fallback_src}")
return fallback_src
- print(f" No suitable images found on page")
+ _beatport_log(f" No suitable images found on page")
return None
except Exception as e:
- print(f"Could not get image for {genre_url}: {e}")
+ _beatport_log(f"Could not get image for {genre_url}: {e}")
return None
def discover_genres_with_images(self, include_images: bool = False) -> List[Dict]:
@@ -573,16 +608,16 @@ class BeatportUnifiedScraper:
genres = self.discover_genres_from_homepage()
if include_images:
- print("Fetching genre images...")
+ _beatport_log("Fetching genre images...")
for i, genre in enumerate(genres[:10]): # Limit to first 10 for demo
- print(f"Getting image for {genre['name']} ({i+1}/{min(10, len(genres))})")
+ _beatport_log(f"Getting image for {genre['name']} ({i+1}/{min(10, len(genres))})")
# Check if genre has URL
if 'url' in genre and genre['url']:
image_url = self.get_genre_image(genre['url'])
genre['image_url'] = image_url
else:
- print(f" No URL available for {genre['name']}, skipping image")
+ _beatport_log(f" No URL available for {genre['name']}, skipping image")
genre['image_url'] = None
# Small delay to be respectful
@@ -649,7 +684,7 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting release data: {e}")
+ _beatport_log(f"Error extracting release data: {e}")
return None
def extract_chart_data_from_card(self, chart_card) -> Optional[Dict]:
@@ -695,7 +730,7 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting chart data: {e}")
+ _beatport_log(f"Error extracting chart data: {e}")
return None
def extract_tracks_from_page(self, soup: BeautifulSoup, list_name: str, limit: int = 100) -> List[Dict]:
@@ -708,7 +743,7 @@ class BeatportUnifiedScraper:
# Find all track links on the page
track_links = soup.find_all('a', href=re.compile(r'/track/'))
- print(f" Found {len(track_links)} track links on {list_name}")
+ _beatport_log(f" Found {len(track_links)} track links on {list_name}")
for i, link in enumerate(track_links[:limit]):
if len(tracks) >= limit:
@@ -800,7 +835,7 @@ class BeatportUnifiedScraper:
def scrape_top_100(self, limit: int = 100, enrich: bool = True) -> List[Dict]:
"""Scrape Beatport Top 100"""
- print("\nScraping Beatport Top 100...")
+ _beatport_log("\nScraping Beatport Top 100...")
soup = self.get_page(f"{self.base_url}/top-100")
tracks = self.extract_tracks_from_page(soup, "Top 100", limit)
@@ -809,12 +844,12 @@ class BeatportUnifiedScraper:
if tracks and enrich:
tracks = self.enrich_chart_tracks(tracks)
- print(f"Extracted {len(tracks)} tracks from Top 100")
+ _beatport_log(f"Extracted {len(tracks)} tracks from Top 100")
return tracks
def scrape_new_releases(self, limit: int = 40) -> List[Dict]:
"""Scrape individual tracks from Beatport New Releases using JSON extraction - ENHANCED"""
- print("\nš Scraping Beatport New Releases (individual tracks)...")
+ _beatport_log("\nš Scraping Beatport New Releases (individual tracks)...")
# Step 1: Get release URLs from homepage cards
release_urls = self.extract_new_releases_urls(limit)
@@ -824,7 +859,7 @@ class BeatportUnifiedScraper:
# Step 2: Extract individual tracks from each release
all_tracks = []
for i, release_url in enumerate(release_urls):
- print(f"\nProcessing release {i+1}/{len(release_urls)}")
+ _beatport_log(f"\nProcessing release {i+1}/{len(release_urls)}")
tracks = self.extract_tracks_from_release_json(release_url)
if tracks:
all_tracks.extend(tracks)
@@ -833,7 +868,7 @@ class BeatportUnifiedScraper:
import time
time.sleep(0.5)
- print(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} releases")
+ _beatport_log(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} releases")
return all_tracks
def extract_new_releases_urls(self, limit: int) -> List[str]:
@@ -844,7 +879,7 @@ class BeatportUnifiedScraper:
# Find New Releases section using data-testid
release_cards = soup.select('[data-testid="new-releases"]')
- print(f" Found {len(release_cards)} release cards in New Releases section")
+ _beatport_log(f" Found {len(release_cards)} release cards in New Releases section")
release_urls = []
for i, card in enumerate(release_cards[:limit]):
@@ -860,13 +895,13 @@ class BeatportUnifiedScraper:
if href.startswith('/'):
href = self.base_url + href
release_urls.append(href)
- print(f" {i+1}. Found release URL: {href}")
+ _beatport_log(f" {i+1}. Found release URL: {href}")
return release_urls
def extract_tracks_from_release_json(self, release_url: str) -> List[Dict]:
"""Extract individual tracks from a release page using JSON data"""
- print(f"Extracting tracks from: {release_url}")
+ _beatport_log(f"Extracting tracks from: {release_url}")
soup = self.get_page(release_url)
if not soup:
@@ -875,13 +910,13 @@ class BeatportUnifiedScraper:
# Extract JSON object from page
json_obj = self.extract_json_object_from_release_page(soup)
if not json_obj:
- print(" No JSON data found")
+ _beatport_log(" No JSON data found")
return []
# Filter tracks for this specific release
release_tracks = self.filter_tracks_for_specific_release(json_obj, release_url)
if not release_tracks:
- print(" No matching tracks found")
+ _beatport_log(" No matching tracks found")
return []
# Convert to our standard format
@@ -891,7 +926,7 @@ class BeatportUnifiedScraper:
if track:
converted_tracks.append(track)
- print(f" Extracted {len(converted_tracks)} tracks")
+ _beatport_log(f" Extracted {len(converted_tracks)} tracks")
return converted_tracks
def extract_json_object_from_release_page(self, soup):
@@ -930,7 +965,7 @@ class BeatportUnifiedScraper:
converted.append(track)
if len(converted) >= 5:
- print(f" JSON extraction found {len(converted)} tracks with rich metadata")
+ _beatport_log(f" JSON extraction found {len(converted)} tracks with rich metadata")
return converted
return []
@@ -954,12 +989,12 @@ class BeatportUnifiedScraper:
if results and isinstance(results, list) and len(results) > 0:
first = results[0] if isinstance(results[0], dict) else {}
if first.get('title') or first.get('name'):
- print(f" Found {len(results)} tracks in queries[{idx}].data.results")
+ _beatport_log(f" Found {len(results)} tracks in queries[{idx}].data.results")
return results
# Pattern 2: data itself is a single track object (track pages)
if isinstance(data, dict) and (data.get('title') or data.get('name')) and data.get('id'):
- print(f" Found single track in queries[{idx}].data")
+ _beatport_log(f" Found single track in queries[{idx}].data")
return [data]
# Pattern 3: data.tracks[]
@@ -968,11 +1003,11 @@ class BeatportUnifiedScraper:
if tracks and isinstance(tracks, list) and len(tracks) > 0:
first = tracks[0] if isinstance(tracks[0], dict) else {}
if first.get('title') or first.get('name'):
- print(f" Found {len(tracks)} tracks in queries[{idx}].data.tracks")
+ _beatport_log(f" Found {len(tracks)} tracks in queries[{idx}].data.tracks")
return tracks
except Exception as e:
- print(f" Error extracting tracks from JSON: {e}")
+ _beatport_log(f" Error extracting tracks from JSON: {e}")
return []
@@ -1065,7 +1100,7 @@ class BeatportUnifiedScraper:
return track
except Exception as e:
- print(f" Error converting chart JSON track: {e}")
+ _beatport_log(f" Error converting chart JSON track: {e}")
return None
def enrich_chart_tracks(self, tracks: List[Dict], progress_callback=None) -> List[Dict]:
@@ -1080,7 +1115,7 @@ class BeatportUnifiedScraper:
enriched = []
total = len(tracks)
- print(f" Enriching {total} chart tracks with per-track metadata...")
+ _beatport_log(f" Enriching {total} chart tracks with per-track metadata...")
for i, track in enumerate(tracks):
track_url = track.get('url', '')
@@ -1113,14 +1148,14 @@ class BeatportUnifiedScraper:
if not matched and json_tracks:
# Debug: show what IDs we have vs what we're looking for
sample_ids = [str(jt.get('id', '')) for jt in json_tracks[:5]]
- print(f" [{i+1}] No ID match for '{track_id_from_url}' in {sample_ids}... trying title match")
+ _beatport_log(f" [{i+1}] No ID match for '{track_id_from_url}' in {sample_ids}... trying title match")
# Fallback: match by title similarity
track_title = track.get('title', '').lower().strip()
for jt in json_tracks:
jt_title = (jt.get('title') or jt.get('name', '')).lower().strip()
if track_title and jt_title and (track_title in jt_title or jt_title in track_title):
matched = jt
- print(f" [{i+1}] Title matched: '{jt_title}'")
+ _beatport_log(f" [{i+1}] Title matched: '{jt_title}'")
break
# Fallback: use first track if only one result
@@ -1132,14 +1167,14 @@ class BeatportUnifiedScraper:
if rich:
enriched.append(rich)
if (i + 1) <= 3 or (i + 1) % 25 == 0:
- print(f" [{i+1}/{total}] {rich.get('artist', '?')} - {rich.get('title', '?')} | {rich.get('release_name', 'no release')}")
+ _beatport_log(f" [{i+1}/{total}] {rich.get('artist', '?')} - {rich.get('title', '?')} | {rich.get('release_name', 'no release')}")
else:
enriched.append(track)
else:
enriched.append(track)
except Exception as e:
- print(f" [{i+1}/{total}] Error enriching track: {e}")
+ _beatport_log(f" [{i+1}/{total}] Error enriching track: {e}")
enriched.append(track)
# Report progress (always runs ā success, failure, or exception)
@@ -1152,7 +1187,7 @@ class BeatportUnifiedScraper:
time.sleep(0.3)
enriched_count = sum(1 for t in enriched if t.get('release_name'))
- print(f" Enrichment complete: {enriched_count}/{total} tracks have release metadata")
+ _beatport_log(f" Enrichment complete: {enriched_count}/{total} tracks have release metadata")
return enriched
def filter_tracks_for_specific_release(self, json_obj: Dict, release_url: str) -> List[Dict]:
@@ -1182,7 +1217,7 @@ class BeatportUnifiedScraper:
return matching_tracks
except Exception as e:
- print(f" Error filtering tracks: {e}")
+ _beatport_log(f" Error filtering tracks: {e}")
return []
@@ -1246,7 +1281,7 @@ class BeatportUnifiedScraper:
return track
except Exception as e:
- print(f" Error converting track data: {e}")
+ _beatport_log(f" Error converting track data: {e}")
return None
def get_release_metadata(self, release_url: str) -> Dict:
@@ -1402,7 +1437,7 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error getting release metadata from {release_url}: {e}")
+ _beatport_log(f"Error getting release metadata from {release_url}: {e}")
import traceback
traceback.print_exc()
return {'success': False, 'error': str(e)}
@@ -1436,7 +1471,7 @@ class BeatportUnifiedScraper:
return tracks
except Exception as e:
- print(f" Error extracting tracks from {release_url}: {e}")
+ _beatport_log(f" Error extracting tracks from {release_url}: {e}")
return []
def scrape_multiple_releases(self, release_urls, source_name: str = "General Release Scraper") -> List[Dict]:
@@ -1456,65 +1491,65 @@ class BeatportUnifiedScraper:
# Validate input
if not release_urls or len(release_urls) == 0:
- print("No release URLs provided")
+ _beatport_log("No release URLs provided")
return []
- print(f"\nSCRAPING {len(release_urls)} RELEASE URL{'S' if len(release_urls) > 1 else ''}")
- print("=" * 60)
+ _beatport_log(f"\nSCRAPING {len(release_urls)} RELEASE URL{'S' if len(release_urls) > 1 else ''}")
+ _beatport_log("=" * 60)
all_tracks = []
for i, release_url in enumerate(release_urls, 1):
- print(f"\nProcessing release {i}/{len(release_urls)}: {release_url}")
+ _beatport_log(f"\nProcessing release {i}/{len(release_urls)}: {release_url}")
try:
tracks = self.extract_individual_tracks_from_release_url(release_url, source_name)
if tracks:
all_tracks.extend(tracks)
- print(f" Found {len(tracks)} tracks")
+ _beatport_log(f" Found {len(tracks)} tracks")
# Show first few tracks for verification
for j, track in enumerate(tracks[:3], 1):
title = track.get('title', 'Unknown')
artist = track.get('artist', 'Unknown')
label = track.get('label', 'Unknown')
- print(f" Track {j}: '{title}' by '{artist}' [{label}]")
+ _beatport_log(f" Track {j}: '{title}' by '{artist}' [{label}]")
if len(tracks) > 3:
- print(f" ... and {len(tracks) - 3} more tracks")
+ _beatport_log(f" ... and {len(tracks) - 3} more tracks")
else:
- print(f" No tracks found")
+ _beatport_log(f" No tracks found")
except Exception as e:
- print(f" Error processing release: {e}")
+ _beatport_log(f" Error processing release: {e}")
continue
# Small delay between requests to be respectful
if i < len(release_urls):
time.sleep(0.5)
- print(f"\n" + "=" * 60)
- print(f"SCRAPING COMPLETE")
- print(f" Total releases processed: {len(release_urls)}")
- print(f" Total tracks extracted: {len(all_tracks)}")
+ _beatport_log(f"\n" + "=" * 60)
+ _beatport_log(f"SCRAPING COMPLETE")
+ _beatport_log(f" Total releases processed: {len(release_urls)}")
+ _beatport_log(f" Total tracks extracted: {len(all_tracks)}")
return all_tracks
def scrape_hype_top_100(self, limit: int = 100, enrich: bool = True) -> List[Dict]:
"""Scrape Beatport Hype Top 100 - Fixed URL based on parser discovery"""
- print("\nScraping Beatport Hype Top 100...")
+ _beatport_log("\nScraping Beatport Hype Top 100...")
# Use the correct URL discovered by parser
soup = self.get_page(f"{self.base_url}/hype-100")
if soup:
tracks = self.extract_tracks_from_page(soup, "Hype Top 100", limit)
if tracks and enrich:
- print(f" Enriching {len(tracks)} Hype Top 100 tracks with per-track metadata...")
+ _beatport_log(f" Enriching {len(tracks)} Hype Top 100 tracks with per-track metadata...")
tracks = self.enrich_chart_tracks(tracks)
- print(f"Extracted {len(tracks)} tracks from Hype Top 100")
+ _beatport_log(f"Extracted {len(tracks)} tracks from Hype Top 100")
return tracks
else:
- print("Could not access /hype-100, trying homepage Hype Picks section...")
+ _beatport_log("Could not access /hype-100, trying homepage Hype Picks section...")
# Fallback to homepage section
soup = self.get_page(self.base_url)
if soup:
@@ -1534,7 +1569,7 @@ class BeatportUnifiedScraper:
else:
tracks = []
- print(f"Extracted {len(tracks)} tracks from Hype Top 100 (fallback)")
+ _beatport_log(f"Extracted {len(tracks)} tracks from Hype Top 100 (fallback)")
return tracks
def extract_releases_from_page(self, soup: BeautifulSoup, list_name: str, limit: int = 100) -> List[Dict]:
@@ -1546,7 +1581,7 @@ class BeatportUnifiedScraper:
# Find table rows - each track/release is in a table row
table_rows = soup.find_all('div', class_=re.compile(r'Table-style__TableRow'))
- print(f" Found {len(table_rows)} table rows on {list_name}")
+ _beatport_log(f" Found {len(table_rows)} table rows on {list_name}")
for i, row in enumerate(table_rows[:limit]):
if len(releases) >= limit:
@@ -1557,13 +1592,13 @@ class BeatportUnifiedScraper:
title_element = row.find('span', class_=re.compile(r'Tables-shared-style__ReleaseName'))
if not title_element:
if len(releases) < 5:
- print(f" Row {i+1}: No release title found")
+ _beatport_log(f" Row {i+1}: No release title found")
continue
release_title = title_element.get_text(strip=True)
if not release_title:
if len(releases) < 5:
- print(f" Row {i+1}: Empty release title")
+ _beatport_log(f" Row {i+1}: Empty release title")
continue
# Find the release URL from the title link
@@ -1601,23 +1636,23 @@ class BeatportUnifiedScraper:
# Debug print for first few items
if len(releases) <= 5:
- print(f" Release {len(releases)}: '{release_title}' by '{artist_text}' (found {len(artists)} artists)")
+ _beatport_log(f" Release {len(releases)}: '{release_title}' by '{artist_text}' (found {len(artists)} artists)")
except Exception as e:
- print(f" Error extracting row {i+1}: {e}")
+ _beatport_log(f" Error extracting row {i+1}: {e}")
continue
- print(f" Successfully extracted {len(releases)} releases from {len(table_rows)} rows")
+ _beatport_log(f" Successfully extracted {len(releases)} releases from {len(table_rows)} rows")
return releases
def scrape_top_100_releases(self, limit: int = 100) -> List[Dict]:
"""Scrape Beatport Top 100 Releases - Extract individual tracks using URL crawling"""
- print("\nScraping Beatport Top 100 Releases...")
+ _beatport_log("\nScraping Beatport Top 100 Releases...")
# Step 1: Extract release URLs from Top 100 page
soup = self.get_page(f"{self.base_url}/top-100-releases")
if not soup:
- print(" Could not access /top-100-releases page")
+ _beatport_log(" Could not access /top-100-releases page")
return []
# Look for rows with release links (Top 100 uses [class*="row"] elements, not tables)
@@ -1626,7 +1661,7 @@ class BeatportUnifiedScraper:
# Top 100 page uses row-based layout, not table structure
table_rows = soup.select('[class*="row"]')
- print(f" Found {len(table_rows)} rows on Top 100 page")
+ _beatport_log(f" Found {len(table_rows)} rows on Top 100 page")
release_urls = []
urls_found = 0
@@ -1638,39 +1673,39 @@ class BeatportUnifiedScraper:
release_url = urljoin(self.base_url, link_elem.get('href'))
release_urls.append(release_url)
urls_found += 1
- print(f" {urls_found}. Found Top 100 release URL: {release_url}")
+ _beatport_log(f" {urls_found}. Found Top 100 release URL: {release_url}")
# Stop when we've found enough URLs
if urls_found >= limit:
break
if not release_urls:
- print(" No Top 100 release URLs found")
+ _beatport_log(" No Top 100 release URLs found")
return []
# Step 2: Crawl each release URL to extract individual tracks
all_individual_tracks = []
for i, release_url in enumerate(release_urls):
- print(f" Processing Top 100 release {i+1}/{len(release_urls)}: {release_url}")
+ _beatport_log(f" Processing Top 100 release {i+1}/{len(release_urls)}: {release_url}")
# Extract individual tracks from this release
tracks = self.extract_individual_tracks_from_release_url(release_url, "Top 100 Releases")
if tracks:
- print(f" Found {len(tracks)} individual tracks")
+ _beatport_log(f" Found {len(tracks)} individual tracks")
all_individual_tracks.extend(tracks)
else:
- print(f" No tracks found")
+ _beatport_log(f" No tracks found")
# Add delay between requests to be respectful
if i < len(release_urls) - 1:
time.sleep(0.5)
- print(f"Extracted {len(all_individual_tracks)} individual tracks from {len(release_urls)} Top 100 releases")
+ _beatport_log(f"Extracted {len(all_individual_tracks)} individual tracks from {len(release_urls)} Top 100 releases")
return all_individual_tracks
def scrape_dj_charts(self, limit: int = 20) -> List[Dict]:
"""Scrape Beatport DJ Charts from homepage section - Improved reliability"""
- print("\nScraping Beatport DJ Charts...")
+ _beatport_log("\nScraping Beatport DJ Charts...")
soup = self.get_page(self.base_url)
if not soup:
@@ -1681,7 +1716,7 @@ class BeatportUnifiedScraper:
# Method 1: Find DJ Charts H2 section on homepage
dj_charts_heading = soup.find(['h1', 'h2', 'h3'], string=re.compile(r'DJ Charts', re.I))
if dj_charts_heading:
- print(" Found DJ Charts section heading")
+ _beatport_log(" Found DJ Charts section heading")
# Get the section content after the heading
section_container = dj_charts_heading.find_parent()
if section_container:
@@ -1689,7 +1724,7 @@ class BeatportUnifiedScraper:
if content_area:
# Look for individual chart links within this section
chart_links = content_area.find_all('a', href=re.compile(r'/chart/'))
- print(f" Found {len(chart_links)} individual DJ chart links")
+ _beatport_log(f" Found {len(chart_links)} individual DJ chart links")
for chart_link in chart_links[:limit]:
chart_name = chart_link.get_text(strip=True)
@@ -1710,9 +1745,9 @@ class BeatportUnifiedScraper:
# Method 2: If no section found, look for chart links across entire homepage
if not charts:
- print(" DJ Charts section not found, scanning entire homepage...")
+ _beatport_log(" DJ Charts section not found, scanning entire homepage...")
all_chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
- print(f" Found {len(all_chart_links)} total chart links on homepage")
+ _beatport_log(f" Found {len(all_chart_links)} total chart links on homepage")
for chart_link in all_chart_links[:limit]:
chart_name = chart_link.get_text(strip=True)
@@ -1730,12 +1765,12 @@ class BeatportUnifiedScraper:
}
charts.append(chart_info)
- print(f"Extracted {len(charts)} DJ charts")
+ _beatport_log(f"Extracted {len(charts)} DJ charts")
return charts
def scrape_featured_charts(self, limit: int = 20) -> List[Dict]:
"""Scrape Beatport Featured Charts from homepage section - FIXED"""
- print("\nScraping Beatport Featured Charts...")
+ _beatport_log("\nScraping Beatport Featured Charts...")
soup = self.get_page(self.base_url)
if not soup:
@@ -1743,7 +1778,7 @@ class BeatportUnifiedScraper:
# Find Featured Charts section using data-testid
chart_cards = soup.select('[data-testid="featured-charts"]')
- print(f" Found {len(chart_cards)} chart cards in Featured Charts section")
+ _beatport_log(f" Found {len(chart_cards)} chart cards in Featured Charts section")
charts = []
for i, card in enumerate(chart_cards[:limit]):
@@ -1765,12 +1800,12 @@ class BeatportUnifiedScraper:
}
charts.append(track_data)
- print(f"Extracted {len(charts)} charts from Featured Charts")
+ _beatport_log(f"Extracted {len(charts)} charts from Featured Charts")
return charts
def scrape_hype_picks_homepage(self, limit: int = 40) -> List[Dict]:
"""Scrape individual tracks from Beatport Hype Picks using JSON extraction - ENHANCED"""
- print("\nScraping Beatport Hype Picks (individual tracks)...")
+ _beatport_log("\nScraping Beatport Hype Picks (individual tracks)...")
# Step 1: Get release URLs from homepage cards
release_urls = self.extract_hype_picks_urls(limit)
@@ -1780,7 +1815,7 @@ class BeatportUnifiedScraper:
# Step 2: Extract individual tracks from each release
all_tracks = []
for i, release_url in enumerate(release_urls):
- print(f"\nProcessing release {i+1}/{len(release_urls)}")
+ _beatport_log(f"\nProcessing release {i+1}/{len(release_urls)}")
tracks = self.extract_tracks_from_hype_picks_release_json(release_url)
if tracks:
all_tracks.extend(tracks)
@@ -1789,7 +1824,7 @@ class BeatportUnifiedScraper:
import time
time.sleep(0.5)
- print(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} hype picks releases")
+ _beatport_log(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} hype picks releases")
return all_tracks
def extract_hype_picks_urls(self, limit: int) -> List[str]:
@@ -1800,7 +1835,7 @@ class BeatportUnifiedScraper:
# Find Hype Picks section using data-testid
hype_cards = soup.select('[data-testid="hype-picks"]')
- print(f" Found {len(hype_cards)} hype picks cards in section")
+ _beatport_log(f" Found {len(hype_cards)} hype picks cards in section")
release_urls = []
for i, card in enumerate(hype_cards[:limit]):
@@ -1816,13 +1851,13 @@ class BeatportUnifiedScraper:
if href.startswith('/'):
href = self.base_url + href
release_urls.append(href)
- print(f" {i+1}. Found release URL: {href}")
+ _beatport_log(f" {i+1}. Found release URL: {href}")
return release_urls
def extract_tracks_from_hype_picks_release_json(self, release_url: str) -> List[Dict]:
"""Extract individual tracks from a hype picks release page using JSON data"""
- print(f"Extracting tracks from: {release_url}")
+ _beatport_log(f"Extracting tracks from: {release_url}")
soup = self.get_page(release_url)
if not soup:
@@ -1831,13 +1866,13 @@ class BeatportUnifiedScraper:
# Extract JSON object from page (same method as New Releases)
json_obj = self.extract_json_object_from_release_page(soup)
if not json_obj:
- print(" No JSON data found")
+ _beatport_log(" No JSON data found")
return []
# Filter tracks for this specific release (same method as New Releases)
release_tracks = self.filter_tracks_for_specific_release(json_obj, release_url)
if not release_tracks:
- print(" No matching tracks found")
+ _beatport_log(" No matching tracks found")
return []
# Convert to our standard format (with Hype Picks branding)
@@ -1847,7 +1882,7 @@ class BeatportUnifiedScraper:
if track:
converted_tracks.append(track)
- print(f" Extracted {len(converted_tracks)} tracks")
+ _beatport_log(f" Extracted {len(converted_tracks)} tracks")
return converted_tracks
def convert_hype_picks_json_to_track_format(self, track_data: Dict, release_url: str, position: int):
@@ -1912,12 +1947,12 @@ class BeatportUnifiedScraper:
return track
except Exception as e:
- print(f" Error converting track data: {e}")
+ _beatport_log(f" Error converting track data: {e}")
return None
def scrape_homepage_top10_lists(self) -> Dict[str, List[Dict]]:
"""Scrape Top 10 Lists from homepage - Beatport Top 10 and Hype Top 10"""
- print("\nScraping Top 10 Lists from homepage...")
+ _beatport_log("\nScraping Top 10 Lists from homepage...")
soup = self.get_page(self.base_url)
if not soup:
@@ -1925,7 +1960,7 @@ class BeatportUnifiedScraper:
# Extract Beatport Top 10 tracks
beatport_top10_items = soup.select('[data-testid="top-10-item"]')
- print(f" Found {len(beatport_top10_items)} Beatport Top 10 items")
+ _beatport_log(f" Found {len(beatport_top10_items)} Beatport Top 10 items")
beatport_tracks = []
for i, item in enumerate(beatport_top10_items, 1):
@@ -1934,11 +1969,11 @@ class BeatportUnifiedScraper:
if track_data:
beatport_tracks.append(track_data)
except Exception as e:
- print(f" Error extracting Beatport track {i}: {e}")
+ _beatport_log(f" Error extracting Beatport track {i}: {e}")
# Extract Hype Top 10 tracks
hype_top10_items = soup.select('[data-testid="hype-top-10-item"]')
- print(f" Found {len(hype_top10_items)} Hype Top 10 items")
+ _beatport_log(f" Found {len(hype_top10_items)} Hype Top 10 items")
hype_tracks = []
for i, item in enumerate(hype_top10_items, 1):
@@ -1947,9 +1982,9 @@ class BeatportUnifiedScraper:
if track_data:
hype_tracks.append(track_data)
except Exception as e:
- print(f" Error extracting Hype track {i}: {e}")
+ _beatport_log(f" Error extracting Hype track {i}: {e}")
- print(f"Extracted {len(beatport_tracks)} Beatport Top 10 + {len(hype_tracks)} Hype Top 10 tracks")
+ _beatport_log(f"Extracted {len(beatport_tracks)} Beatport Top 10 + {len(hype_tracks)} Hype Top 10 tracks")
return {
"beatport_top10": beatport_tracks,
@@ -2031,24 +2066,24 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting track data: {e}")
+ _beatport_log(f"Error extracting track data: {e}")
return None
def scrape_homepage_top10_releases(self) -> List[Dict]:
"""Scrape Top 10 Releases from homepage - FIXED VERSION"""
- print("\nFIXED: Scraping Top 10 Releases from homepage...")
+ _beatport_log("\nFIXED: Scraping Top 10 Releases from homepage...")
soup = self.get_page(self.base_url)
if not soup:
- print(" Could not get homepage")
+ _beatport_log(" Could not get homepage")
return []
# Extract Top 10 Releases items - EXACT same as test script
top10_releases_items = soup.select('[data-testid="top-10-releases-item"]')
- print(f" FOUND {len(top10_releases_items)} Top 10 Releases items")
+ _beatport_log(f" FOUND {len(top10_releases_items)} Top 10 Releases items")
if len(top10_releases_items) == 0:
- print(" No items found - trying alternatives")
+ _beatport_log(" No items found - trying alternatives")
return []
releases = []
@@ -2058,13 +2093,13 @@ class BeatportUnifiedScraper:
release_data = self.extract_release_from_item_FIXED(item, i)
if release_data:
releases.append(release_data)
- print(f" {i}. {release_data['artist']} - {release_data['title']}")
+ _beatport_log(f" {i}. {release_data['artist']} - {release_data['title']}")
else:
- print(f" {i}. No data extracted")
+ _beatport_log(f" {i}. No data extracted")
except Exception as e:
- print(f" Error extracting release {i}: {e}")
+ _beatport_log(f" Error extracting release {i}: {e}")
- print(f"FINAL: Extracted {len(releases)} Top 10 Releases")
+ _beatport_log(f"FINAL: Extracted {len(releases)} Top 10 Releases")
return releases
def extract_release_from_item_FIXED(self, item, rank):
@@ -2177,7 +2212,7 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting release data: {e}")
+ _beatport_log(f"Error extracting release data: {e}")
return None
def extract_release_from_top10_item(self, item, rank):
@@ -2288,12 +2323,12 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting release data: {e}")
+ _beatport_log(f"Error extracting release data: {e}")
return None
def scrape_new_on_beatport_hero(self, limit: int = 10) -> List[Dict]:
"""Scrape the 'New on Beatport' hero slideshow from homepage using data-testid standard"""
- print("\nScraping 'New on Beatport' hero slideshow...")
+ _beatport_log("\nScraping 'New on Beatport' hero slideshow...")
soup = self.get_page(self.base_url)
if not soup:
@@ -2304,7 +2339,7 @@ class BeatportUnifiedScraper:
# Method 1 (PRIMARY): Use data-testid standard like all other rebuild functions
hero_items = soup.select('[data-testid="new-on-beatport"]')
if hero_items:
- print(f" Found {len(hero_items)} items using data-testid='new-on-beatport'")
+ _beatport_log(f" Found {len(hero_items)} items using data-testid='new-on-beatport'")
for i, item in enumerate(hero_items[:limit]):
track_data = self._extract_track_from_slide(item, f"Hero Item {i+1}")
if track_data and track_data.get('url'):
@@ -2314,14 +2349,14 @@ class BeatportUnifiedScraper:
if len(tracks) < 5:
hero_wrapper = soup.select_one('[class*="Homepage-style__NewOnBeatportWrapper"]')
if hero_wrapper:
- print(" Found Homepage NewOnBeatportWrapper (fallback)")
+ _beatport_log(" Found Homepage NewOnBeatportWrapper (fallback)")
tracks.extend(self._extract_from_hero_wrapper(hero_wrapper, limit))
# Method 3 (FALLBACK): Look for carousel with aria attributes
if len(tracks) < 5:
carousel = soup.find('div', {'aria-roledescription': 'carousel', 'aria-label': 'Carousel'})
if carousel:
- print(" Found carousel with aria-roledescription and aria-label (fallback)")
+ _beatport_log(" Found carousel with aria-roledescription and aria-label (fallback)")
additional_tracks = self._extract_from_carousel(carousel, limit)
# Merge without duplicates
existing_urls = {track.get('url') for track in tracks}
@@ -2331,9 +2366,9 @@ class BeatportUnifiedScraper:
# Method 4 (LAST RESORT): Look for individual slide items more broadly
if len(tracks) < 5:
- print(" Looking for individual carousel items (last resort)...")
+ _beatport_log(" Looking for individual carousel items (last resort)...")
carousel_items = soup.find_all(['div', 'article'], class_=re.compile(r'carousel.*item|item.*carousel|slide', re.I))
- print(f" Found {len(carousel_items)} potential carousel items")
+ _beatport_log(f" Found {len(carousel_items)} potential carousel items")
for i, item in enumerate(carousel_items[:limit * 2]): # Check more items
track_data = self._extract_track_from_slide(item, f"Carousel Item {i+1}")
@@ -2343,7 +2378,7 @@ class BeatportUnifiedScraper:
if track_data['url'] not in existing_urls:
tracks.append(track_data)
- print(f" Extracted {len(tracks)} tracks from New on Beatport hero")
+ _beatport_log(f" Extracted {len(tracks)} tracks from New on Beatport hero")
return tracks[:limit]
def _extract_from_hero_wrapper(self, wrapper, limit: int) -> List[Dict]:
@@ -2529,21 +2564,21 @@ class BeatportUnifiedScraper:
if (not title or not artist or
title.lower() in ['no title', 'unknown title', 'unknown', ''] or
artist.lower() in ['no artist', 'unknown artist', 'unknown', 'various artists', '']):
- print(f" {context}: Filtered out invalid track - '{title}' by '{artist}'")
+ _beatport_log(f" {context}: Filtered out invalid track - '{title}' by '{artist}'")
return None
# Only return if we found meaningful data
if track_data.get('url') or track_data.get('image_url'):
track_data['source'] = f"New on Beatport Hero - {context}"
track_data['scraped_at'] = time.time()
- print(f" {context}: {title} - {artist}")
+ _beatport_log(f" {context}: {title} - {artist}")
return track_data
else:
- print(f" {context}: No usable data found")
+ _beatport_log(f" {context}: No usable data found")
return None
except Exception as e:
- print(f" Error extracting from {context}: {e}")
+ _beatport_log(f" Error extracting from {context}: {e}")
return None
def _extract_title_artist_from_url(self, url: str) -> Dict[str, str]:
@@ -2792,7 +2827,7 @@ class BeatportUnifiedScraper:
def scrape_top_10_releases_homepage(self, limit: int = 10) -> List[Dict]:
"""Scrape Top 10 Releases from homepage - Extract individual tracks using URL crawling"""
- print("\nScraping Top 10 Releases from homepage...")
+ _beatport_log("\nScraping Top 10 Releases from homepage...")
soup = self.get_page(self.base_url)
if not soup:
@@ -2800,7 +2835,7 @@ class BeatportUnifiedScraper:
# Step 1: Extract release URLs from Top 10 section
release_items = soup.select('[data-testid="top-10-releases-item"]')
- print(f" Found {len(release_items)} release items in Top 10 Releases section")
+ _beatport_log(f" Found {len(release_items)} release items in Top 10 Releases section")
release_urls = []
for i, item in enumerate(release_items[:limit]):
@@ -2809,30 +2844,30 @@ class BeatportUnifiedScraper:
if link_elem and link_elem.get('href'):
release_url = urljoin(self.base_url, link_elem.get('href'))
release_urls.append(release_url)
- print(f" {i+1}. Found Top 10 release URL: {release_url}")
+ _beatport_log(f" {i+1}. Found Top 10 release URL: {release_url}")
if not release_urls:
- print(" No Top 10 release URLs found")
+ _beatport_log(" No Top 10 release URLs found")
return []
# Step 2: Crawl each release URL to extract individual tracks
all_individual_tracks = []
for i, release_url in enumerate(release_urls):
- print(f" Processing Top 10 release {i+1}/{len(release_urls)}: {release_url}")
+ _beatport_log(f" Processing Top 10 release {i+1}/{len(release_urls)}: {release_url}")
# Extract individual tracks from this release
tracks = self.extract_individual_tracks_from_release_url(release_url, "Top 10 Releases")
if tracks:
- print(f" Found {len(tracks)} individual tracks")
+ _beatport_log(f" Found {len(tracks)} individual tracks")
all_individual_tracks.extend(tracks)
else:
- print(f" No tracks found")
+ _beatport_log(f" No tracks found")
# Add delay between requests to be respectful
if i < len(release_urls) - 1:
time.sleep(0.5)
- print(f"Extracted {len(all_individual_tracks)} individual tracks from {len(release_urls)} Top 10 releases")
+ _beatport_log(f"Extracted {len(all_individual_tracks)} individual tracks from {len(release_urls)} Top 10 releases")
return all_individual_tracks
def scrape_genre_charts(self, genre: Dict, limit: int = 100, enrich: bool = True) -> List[Dict]:
@@ -2852,20 +2887,20 @@ class BeatportUnifiedScraper:
]
for chart_url in chart_urls_to_try:
- print(f" Trying chart URL: {chart_url}")
+ _beatport_log(f" Trying chart URL: {chart_url}")
soup = self.get_page(chart_url)
if soup:
tracks = self.extract_tracks_from_page(soup, f"{genre['name']} Top 100", limit)
if tracks and len(tracks) >= min(limit, 50):
- print(f" Successfully extracted {len(tracks)} tracks from {chart_url}")
+ _beatport_log(f" Successfully extracted {len(tracks)} tracks from {chart_url}")
break
elif tracks:
- print(f" Only found {len(tracks)} tracks at {chart_url}, trying next URL...")
+ _beatport_log(f" Only found {len(tracks)} tracks at {chart_url}, trying next URL...")
else:
- print(f" No tracks found at {chart_url}")
+ _beatport_log(f" No tracks found at {chart_url}")
if tracks and enrich:
- print(f" Enriching {len(tracks)} {genre['name']} chart tracks with per-track metadata...")
+ _beatport_log(f" Enriching {len(tracks)} {genre['name']} chart tracks with per-track metadata...")
tracks = self.enrich_chart_tracks(tracks)
return tracks
@@ -2892,7 +2927,7 @@ class BeatportUnifiedScraper:
]
for release_url in release_urls_to_try:
- print(f" Trying release URL: {release_url}")
+ _beatport_log(f" Trying release URL: {release_url}")
soup = self.get_page(release_url)
if soup:
# Try to find releases section on the page
@@ -2900,19 +2935,19 @@ class BeatportUnifiedScraper:
# If no releases found with release extraction, try track extraction
if not releases:
- print(f" No releases found with release method, trying track method for {genre['name']}")
+ _beatport_log(f" No releases found with release method, trying track method for {genre['name']}")
releases = self.extract_tracks_from_page(soup, f"{genre['name']} Top Releases", limit)
# Mark these as releases
for release in releases:
release['type'] = 'release'
if releases and len(releases) >= min(limit, 30): # If we got a decent number of releases
- print(f" Successfully extracted {len(releases)} releases from {release_url}")
+ _beatport_log(f" Successfully extracted {len(releases)} releases from {release_url}")
break
elif releases:
- print(f" Only found {len(releases)} releases at {release_url}, trying next URL...")
+ _beatport_log(f" Only found {len(releases)} releases at {release_url}, trying next URL...")
else:
- print(f" No releases found at {release_url}")
+ _beatport_log(f" No releases found at {release_url}")
return releases
@@ -2933,22 +2968,22 @@ class BeatportUnifiedScraper:
]
for hype_url in hype_urls_to_try:
- print(f" Trying hype URL: {hype_url}")
+ _beatport_log(f" Trying hype URL: {hype_url}")
soup = self.get_page(hype_url)
if soup:
# Use the new dedicated hype extraction method
tracks = self.extract_hype_tracks_from_beatport_page(soup, f"{genre['name']} Hype Charts", limit)
if tracks and len(tracks) >= min(limit, 10): # If we got a decent number of tracks
- print(f" Successfully extracted {len(tracks)} hype tracks from {hype_url}")
+ _beatport_log(f" Successfully extracted {len(tracks)} hype tracks from {hype_url}")
break
elif tracks:
- print(f" Only found {len(tracks)} hype tracks at {hype_url}, trying next URL...")
+ _beatport_log(f" Only found {len(tracks)} hype tracks at {hype_url}, trying next URL...")
else:
- print(f" No hype tracks found at {hype_url}")
+ _beatport_log(f" No hype tracks found at {hype_url}")
# If no dedicated hype page found, try main genre page for hype content
if not tracks:
- print(f" No dedicated hype page found, looking for hype content on main page...")
+ _beatport_log(f" No dedicated hype page found, looking for hype content on main page...")
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
soup = self.get_page(genre_url)
if soup:
@@ -2958,7 +2993,7 @@ class BeatportUnifiedScraper:
def scrape_genre_hype_picks(self, genre: Dict, limit: int = 100) -> List[Dict]:
"""Scrape individual tracks from Genre Hype Picks using JSON extraction - ENHANCED (same pattern as Latest Releases)"""
- print(f"\nScraping {genre['name']} Hype Picks (individual tracks)...")
+ _beatport_log(f"\nScraping {genre['name']} Hype Picks (individual tracks)...")
# Step 1: Get release URLs from genre Hype Picks carousel (same logic as Latest Releases)
release_urls = self.extract_genre_hype_picks_urls(genre, limit)
@@ -2968,7 +3003,7 @@ class BeatportUnifiedScraper:
# Step 2: Extract individual tracks from each release (same method as Latest Releases)
all_tracks = []
for i, release_url in enumerate(release_urls):
- print(f"\nProcessing {genre['name']} hype pick {i+1}/{len(release_urls)}")
+ _beatport_log(f"\nProcessing {genre['name']} hype pick {i+1}/{len(release_urls)}")
tracks = self.extract_tracks_from_release_json(release_url)
if tracks:
# Update list_name to match genre context
@@ -2980,7 +3015,7 @@ class BeatportUnifiedScraper:
import time
time.sleep(0.5)
- print(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} {genre['name']} hype picks")
+ _beatport_log(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} {genre['name']} hype picks")
return all_tracks
def extract_genre_hype_picks_urls(self, genre: Dict, limit: int) -> List[str]:
@@ -2998,16 +3033,16 @@ class BeatportUnifiedScraper:
h2 = container.select_one('h2')
if h2 and 'hype' in h2.get_text().lower() and 'pick' in h2.get_text().lower():
hype_container = container
- print(f" Found Hype Picks section: '{h2.get_text().strip()}'")
+ _beatport_log(f" Found Hype Picks section: '{h2.get_text().strip()}'")
break
if not hype_container:
- print(f" Could not find Hype Picks section for {genre['name']}")
+ _beatport_log(f" Could not find Hype Picks section for {genre['name']}")
return []
# Extract release URLs from ALL releases in Hype Picks section (same as Latest Releases)
release_links = hype_container.select('a[href*="/release/"]')
- print(f" Found {len(release_links)} release links in Hype Picks section")
+ _beatport_log(f" Found {len(release_links)} release links in Hype Picks section")
release_urls = []
seen_urls = set()
@@ -3024,7 +3059,7 @@ class BeatportUnifiedScraper:
if href not in seen_urls:
release_urls.append(href)
seen_urls.add(href)
- print(f" {len(release_urls)}. Found hype pick URL: {href}")
+ _beatport_log(f" {len(release_urls)}. Found hype pick URL: {href}")
# Stop when we reach the desired number of unique releases
if len(release_urls) >= limit:
@@ -3041,7 +3076,7 @@ class BeatportUnifiedScraper:
string=re.compile(r'hype', re.I))
for heading in hype_headings:
- print(f" Found hype heading: {heading.get_text(strip=True)}")
+ _beatport_log(f" Found hype heading: {heading.get_text(strip=True)}")
# Get the section after this heading
section_container = heading.find_parent()
@@ -3146,7 +3181,7 @@ class BeatportUnifiedScraper:
}
tracks.append(track_data)
- print(f" Release Track: {artist_text} - {track_title}")
+ _beatport_log(f" Release Track: {artist_text} - {track_title}")
except Exception:
continue
@@ -3171,7 +3206,7 @@ class BeatportUnifiedScraper:
string=re.compile(rf'{section_name}', re.I))
if section_heading:
- print(f" Found hype picks section: {section_heading.get_text(strip=True)}")
+ _beatport_log(f" Found hype picks section: {section_heading.get_text(strip=True)}")
section_container = section_heading.find_parent()
if section_container:
content_area = section_container.find_next_sibling()
@@ -3193,7 +3228,7 @@ class BeatportUnifiedScraper:
if not soup:
return tracks
- print(f" Looking for HYPE labeled tracks on page...")
+ _beatport_log(f" Looking for HYPE labeled tracks on page...")
# Look for elements containing "HYPE" text
hype_elements = soup.find_all(text=re.compile(r'HYPE', re.I))
@@ -3261,7 +3296,7 @@ class BeatportUnifiedScraper:
# Avoid duplicates
if not any(existing['url'] == track_data['url'] for existing in tracks):
tracks.append(track_data)
- print(f" Found HYPE track: {track_data['artist']} - {track_data['title']}")
+ _beatport_log(f" Found HYPE track: {track_data['artist']} - {track_data['title']}")
except Exception as e:
continue
@@ -3269,7 +3304,7 @@ class BeatportUnifiedScraper:
except Exception as e:
continue
- print(f" Extracted {len(tracks)} HYPE labeled tracks")
+ _beatport_log(f" Extracted {len(tracks)} HYPE labeled tracks")
return tracks
def extract_hype_tracks_from_beatport_page(self, soup: BeautifulSoup, list_name: str, limit: int = 100) -> List[Dict]:
@@ -3279,7 +3314,7 @@ class BeatportUnifiedScraper:
if not soup:
return tracks
- print(f" Extracting hype tracks from Beatport page...")
+ _beatport_log(f" Extracting hype tracks from Beatport page...")
# Method 1: Extract from Hype Picks carousel (release cards with HYPE badges)
hype_picks_tracks = self.extract_hype_picks_from_carousel(soup, list_name, limit)
@@ -3295,7 +3330,7 @@ class BeatportUnifiedScraper:
hype_table_tracks = self.extract_hype_from_track_table(soup, list_name, limit - len(tracks))
tracks.extend(hype_table_tracks)
- print(f" Extracted {len(tracks)} hype tracks using actual Beatport structure")
+ _beatport_log(f" Extracted {len(tracks)} hype tracks using actual Beatport structure")
return tracks[:limit]
def extract_hype_picks_from_carousel(self, soup: BeautifulSoup, list_name: str, limit: int) -> List[Dict]:
@@ -3342,7 +3377,7 @@ class BeatportUnifiedScraper:
}
tracks.append(track_data)
- print(f" Hype Pick: {artist_text} - {release_title}")
+ _beatport_log(f" Hype Pick: {artist_text} - {release_title}")
except Exception as e:
continue
@@ -3395,7 +3430,7 @@ class BeatportUnifiedScraper:
}
tracks.append(track_data)
- print(f" Hype Track {position}: {artist_text} - {track_title}")
+ _beatport_log(f" Hype Track {position}: {artist_text} - {track_title}")
except Exception as e:
continue
@@ -3452,7 +3487,7 @@ class BeatportUnifiedScraper:
}
tracks.append(track_data)
- print(f" Hype Track {position}: {artist_text} - {track_title}")
+ _beatport_log(f" Hype Track {position}: {artist_text} - {track_title}")
except Exception as e:
continue
@@ -3461,7 +3496,7 @@ class BeatportUnifiedScraper:
def scrape_genre_staff_picks(self, genre: Dict, limit: int = 100) -> List[Dict]:
"""Scrape individual tracks from Genre Staff Picks using JSON extraction - ENHANCED (same pattern as Latest Releases)"""
- print(f"\nScraping {genre['name']} Staff Picks (individual tracks)...")
+ _beatport_log(f"\nScraping {genre['name']} Staff Picks (individual tracks)...")
# Step 1: Get release URLs from genre Staff Picks carousel (same logic as Latest Releases)
release_urls = self.extract_genre_staff_picks_urls(genre, limit)
@@ -3471,7 +3506,7 @@ class BeatportUnifiedScraper:
# Step 2: Extract individual tracks from each release (same method as Latest Releases)
all_tracks = []
for i, release_url in enumerate(release_urls):
- print(f"\nProcessing {genre['name']} staff pick {i+1}/{len(release_urls)}")
+ _beatport_log(f"\nProcessing {genre['name']} staff pick {i+1}/{len(release_urls)}")
tracks = self.extract_tracks_from_release_json(release_url)
if tracks:
# Update list_name to match genre context
@@ -3483,7 +3518,7 @@ class BeatportUnifiedScraper:
import time
time.sleep(0.5)
- print(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} {genre['name']} staff picks")
+ _beatport_log(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} {genre['name']} staff picks")
return all_tracks
def extract_genre_staff_picks_urls(self, genre: Dict, limit: int) -> List[str]:
@@ -3501,16 +3536,16 @@ class BeatportUnifiedScraper:
h2 = container.select_one('h2')
if h2 and 'staff' in h2.get_text().lower() and 'pick' in h2.get_text().lower():
staff_container = container
- print(f" Found Staff Picks section: '{h2.get_text().strip()}'")
+ _beatport_log(f" Found Staff Picks section: '{h2.get_text().strip()}'")
break
if not staff_container:
- print(f" Could not find Staff Picks section for {genre['name']}")
+ _beatport_log(f" Could not find Staff Picks section for {genre['name']}")
return []
# Extract release URLs from ALL releases in Staff Picks section (same as Latest Releases)
release_links = staff_container.select('a[href*="/release/"]')
- print(f" Found {len(release_links)} release links in Staff Picks section")
+ _beatport_log(f" Found {len(release_links)} release links in Staff Picks section")
release_urls = []
seen_urls = set()
@@ -3527,7 +3562,7 @@ class BeatportUnifiedScraper:
if href not in seen_urls:
release_urls.append(href)
seen_urls.add(href)
- print(f" {len(release_urls)}. Found staff pick URL: {href}")
+ _beatport_log(f" {len(release_urls)}. Found staff pick URL: {href}")
# Stop when we reach the desired number of unique releases
if len(release_urls) >= limit:
@@ -3537,7 +3572,7 @@ class BeatportUnifiedScraper:
def scrape_genre_latest_releases(self, genre: Dict, limit: int = 100) -> List[Dict]:
"""Scrape individual tracks from Genre Latest Releases using JSON extraction - ENHANCED (same pattern as homepage)"""
- print(f"\nš Scraping {genre['name']} Latest Releases (individual tracks)...")
+ _beatport_log(f"\nš Scraping {genre['name']} Latest Releases (individual tracks)...")
# Step 1: Get release URLs from genre Latest Releases carousel (same logic as homepage)
release_urls = self.extract_genre_latest_releases_urls(genre, limit)
@@ -3547,7 +3582,7 @@ class BeatportUnifiedScraper:
# Step 2: Extract individual tracks from each release (same method as homepage)
all_tracks = []
for i, release_url in enumerate(release_urls):
- print(f"\nProcessing {genre['name']} latest release {i+1}/{len(release_urls)}")
+ _beatport_log(f"\nProcessing {genre['name']} latest release {i+1}/{len(release_urls)}")
tracks = self.extract_tracks_from_release_json(release_url)
if tracks:
# Update list_name to match genre context
@@ -3559,7 +3594,7 @@ class BeatportUnifiedScraper:
import time
time.sleep(0.5)
- print(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} latest {genre['name']} releases")
+ _beatport_log(f"Extracted {len(all_tracks)} individual tracks from {len(release_urls)} latest {genre['name']} releases")
return all_tracks
def extract_genre_latest_releases_urls(self, genre: Dict, limit: int) -> List[str]:
@@ -3577,16 +3612,16 @@ class BeatportUnifiedScraper:
h2 = container.select_one('h2')
if h2 and 'latest' in h2.get_text().lower() and 'release' in h2.get_text().lower():
latest_container = container
- print(f" Found Latest Releases section: '{h2.get_text().strip()}'")
+ _beatport_log(f" Found Latest Releases section: '{h2.get_text().strip()}'")
break
if not latest_container:
- print(f" Could not find Latest Releases section for {genre['name']}")
+ _beatport_log(f" Could not find Latest Releases section for {genre['name']}")
return []
# Extract release URLs from ALL releases in Latest Releases section (same as homepage gets all cards)
release_links = latest_container.select('a[href*="/release/"]')
- print(f" Found {len(release_links)} release links in Latest Releases section")
+ _beatport_log(f" Found {len(release_links)} release links in Latest Releases section")
release_urls = []
seen_urls = set()
@@ -3603,7 +3638,7 @@ class BeatportUnifiedScraper:
if href not in seen_urls:
release_urls.append(href)
seen_urls.add(href)
- print(f" {len(release_urls)}. Found latest release URL: {href}")
+ _beatport_log(f" {len(release_urls)}. Found latest release URL: {href}")
# Stop when we reach the desired number of unique releases
if len(release_urls) >= limit:
@@ -3622,7 +3657,7 @@ class BeatportUnifiedScraper:
charts = []
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
- print(f" Found {len(chart_links)} chart links on genre page")
+ _beatport_log(f" Found {len(chart_links)} chart links on genre page")
for chart_link in chart_links[:limit]:
chart_name = chart_link.get_text(strip=True)
@@ -3642,9 +3677,9 @@ class BeatportUnifiedScraper:
}
charts.append(chart_info)
- print(f" Chart {len(charts)}: {chart_name}")
+ _beatport_log(f" Chart {len(charts)}: {chart_name}")
- print(f" Found {len(charts)} charts in New Charts Collection")
+ _beatport_log(f" Found {len(charts)} charts in New Charts Collection")
return charts[:limit]
def extract_tracks_from_chart(self, chart_url: str, chart_name: str, limit: int) -> List[Dict]:
@@ -3656,14 +3691,14 @@ class BeatportUnifiedScraper:
if not soup:
return tracks
- print(f" Extracting tracks from chart page: {chart_url}")
- print(f" Chart name: {chart_name}")
+ _beatport_log(f" Extracting tracks from chart page: {chart_url}")
+ _beatport_log(f" Chart name: {chart_name}")
# Step 1: Get basic track list from HTML
tracks = self.extract_tracks_from_chart_table(soup, chart_name, limit)
if len(tracks) < 10:
- print(f" Chart table extraction found {len(tracks)} tracks, trying general extraction...")
+ _beatport_log(f" Chart table extraction found {len(tracks)} tracks, trying general extraction...")
general_tracks = self.extract_tracks_from_page(soup, f"New Chart: {chart_name}", limit)
if len(general_tracks) > len(tracks):
tracks = general_tracks
@@ -3673,7 +3708,7 @@ class BeatportUnifiedScraper:
if len(table_tracks) > len(tracks):
tracks = table_tracks
- print(f" Found {len(tracks)} tracks, enriching with per-track metadata...")
+ _beatport_log(f" Found {len(tracks)} tracks, enriching with per-track metadata...")
# Step 2: Enrich each track by visiting its individual page
if tracks:
@@ -3682,47 +3717,47 @@ class BeatportUnifiedScraper:
return tracks
except Exception as e:
- print(f" Error extracting tracks from chart {chart_name}: {e}")
+ _beatport_log(f" Error extracting tracks from chart {chart_name}: {e}")
return []
def extract_tracks_from_chart_table(self, soup, chart_name: str, limit: int) -> List[Dict]:
"""Extract tracks from Beatport chart table structure (tracks-table class)"""
tracks = []
- print(f" DEBUG: Looking for tracks-table container...")
+ _beatport_log(f" DEBUG: Looking for tracks-table container...")
# Look for the tracks table container
tracks_table = soup.find(class_=re.compile(r'tracks-table'))
if not tracks_table:
- print(f" No tracks-table container found")
+ _beatport_log(f" No tracks-table container found")
# Debug: Let's see what table classes ARE available
all_tables = soup.find_all(['table', 'div'], class_=re.compile(r'table|Table', re.I))
- print(f" DEBUG: Found {len(all_tables)} table-like elements")
+ _beatport_log(f" DEBUG: Found {len(all_tables)} table-like elements")
for i, table in enumerate(all_tables[:5]):
classes = table.get('class', [])
- print(f" Table {i+1}: {' '.join(classes)}")
+ _beatport_log(f" Table {i+1}: {' '.join(classes)}")
return tracks
- print(f" Found tracks-table container with classes: {tracks_table.get('class', [])}")
+ _beatport_log(f" Found tracks-table container with classes: {tracks_table.get('class', [])}")
# Find all track rows using data-testid or table row classes
track_rows_testid = tracks_table.find_all(['div', 'tr'], attrs={'data-testid': 'tracks-table-row'})
track_rows_class = tracks_table.find_all(class_=re.compile(r'Table.*Row.*tracks-table'))
track_rows_generic = tracks_table.find_all(class_=re.compile(r'Table.*Row'))
- print(f" DEBUG: Track rows found:")
- print(f" - By data-testid='tracks-table-row': {len(track_rows_testid)}")
- print(f" - By class pattern 'Table.*Row.*tracks-table': {len(track_rows_class)}")
- print(f" - By generic 'Table.*Row': {len(track_rows_generic)}")
+ _beatport_log(f" DEBUG: Track rows found:")
+ _beatport_log(f" - By data-testid='tracks-table-row': {len(track_rows_testid)}")
+ _beatport_log(f" - By class pattern 'Table.*Row.*tracks-table': {len(track_rows_class)}")
+ _beatport_log(f" - By generic 'Table.*Row': {len(track_rows_generic)}")
# Use the best available option
track_rows = track_rows_testid or track_rows_class or track_rows_generic
if not track_rows:
- print(f" No track rows found in any format")
+ _beatport_log(f" No track rows found in any format")
return tracks
- print(f" Using {len(track_rows)} track rows for extraction")
+ _beatport_log(f" Using {len(track_rows)} track rows for extraction")
for i, row in enumerate(track_rows[:limit]):
try:
@@ -3760,11 +3795,11 @@ class BeatportUnifiedScraper:
# DEBUG: Print track details for first few
if len(tracks) < 3:
- print(f" DEBUG Track {len(tracks)+1}:")
- print(f" Title: '{track_title}'")
- print(f" Artist: '{artist_text}'")
- print(f" URL: {track_url}")
- print(f" Track link href: {track_link.get('href', 'NO HREF')}")
+ _beatport_log(f" DEBUG Track {len(tracks)+1}:")
+ _beatport_log(f" Title: '{track_title}'")
+ _beatport_log(f" Artist: '{artist_text}'")
+ _beatport_log(f" URL: {track_url}")
+ _beatport_log(f" Track link href: {track_link.get('href', 'NO HREF')}")
# Extract track number if available
track_no_elem = row.find(class_=re.compile(r'TrackNo'))
@@ -3783,13 +3818,13 @@ class BeatportUnifiedScraper:
# Debug output for first few tracks
if len(tracks) <= 5:
- print(f" Track {len(tracks)}: {artist_text} - {track_title}")
+ _beatport_log(f" Track {len(tracks)}: {artist_text} - {track_title}")
except Exception as e:
- print(f" Error parsing track row {i+1}: {e}")
+ _beatport_log(f" Error parsing track row {i+1}: {e}")
continue
- print(f" Chart table extraction completed: {len(tracks)} tracks found")
+ _beatport_log(f" Chart table extraction completed: {len(tracks)} tracks found")
return tracks
def extract_tracks_from_table_format(self, soup, chart_name: str, limit: int) -> List[Dict]:
@@ -3799,7 +3834,7 @@ class BeatportUnifiedScraper:
# Look for table rows containing track data
table_rows = soup.find_all('tr') + soup.find_all('div', class_=re.compile(r'Table.*Row|track.*row', re.I))
- print(f" Found {len(table_rows)} potential table rows")
+ _beatport_log(f" Found {len(table_rows)} potential table rows")
for i, row in enumerate(table_rows[:limit]):
try:
@@ -3837,7 +3872,7 @@ class BeatportUnifiedScraper:
tracks.append(track_data)
if len(tracks) <= 3: # Debug first few
- print(f" Track {len(tracks)}: {artist_text} - {track_title}")
+ _beatport_log(f" Track {len(tracks)}: {artist_text} - {track_title}")
except Exception as e:
continue
@@ -3848,7 +3883,7 @@ class BeatportUnifiedScraper:
"""Analyze a genre page to discover all available sections"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
- print(f"Discovering sections for {genre['name']} genre page...")
+ _beatport_log(f"Discovering sections for {genre['name']} genre page...")
soup = self.get_page(genre_url)
if not soup:
@@ -3886,17 +3921,17 @@ class BeatportUnifiedScraper:
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
sections['chart_count'] = len(chart_links)
- print(f"Discovered sections for {genre['name']}:")
+ _beatport_log(f"Discovered sections for {genre['name']}:")
for section_type, items in sections.items():
if items and section_type != 'chart_count':
- print(f" ⢠{section_type}: {len(items)} sections")
- print(f" ⢠Individual charts found: {sections['chart_count']}")
+ _beatport_log(f" ⢠{section_type}: {len(items)} sections")
+ _beatport_log(f" ⢠Individual charts found: {sections['chart_count']}")
return sections
def scrape_genre_hero_slider(self, genre_slug: str, genre_id: str) -> List[Dict]:
"""Scrape hero slider data from a genre page"""
- print(f"\nScraping hero slider for {genre_slug}...")
+ _beatport_log(f"\nScraping hero slider for {genre_slug}...")
genre_url = f"{self.base_url}/genre/{genre_slug}/{genre_id}"
soup = self.get_page(genre_url)
@@ -3906,18 +3941,18 @@ class BeatportUnifiedScraper:
# Find the main section container
main_section = soup.find('div', class_=re.compile(r'Genre-style__MainSection'))
if not main_section:
- print(f" Main section not found for {genre_slug}")
+ _beatport_log(f" Main section not found for {genre_slug}")
return []
# Find the hero slider
hero_slider = main_section.find('div', class_='hero-slider')
if not hero_slider:
- print(f" Hero slider not found for {genre_slug}")
+ _beatport_log(f" Hero slider not found for {genre_slug}")
return []
# Extract all hero releases
hero_releases = hero_slider.find_all(class_='hero-release')
- print(f" Found {len(hero_releases)} hero releases")
+ _beatport_log(f" Found {len(hero_releases)} hero releases")
releases_data = []
for i, release in enumerate(hero_releases):
@@ -3925,18 +3960,18 @@ class BeatportUnifiedScraper:
release_data = self.extract_hero_release_data(release)
if release_data and release_data.get('url'):
releases_data.append(release_data)
- print(f" Extracted: {release_data.get('title', 'Unknown')} by {release_data.get('artists_string', 'Unknown')}")
+ _beatport_log(f" Extracted: {release_data.get('title', 'Unknown')} by {release_data.get('artists_string', 'Unknown')}")
else:
- print(f" Skipped release {i+1} - incomplete data")
+ _beatport_log(f" Skipped release {i+1} - incomplete data")
except Exception as e:
- print(f" Error extracting release {i+1}: {e}")
+ _beatport_log(f" Error extracting release {i+1}: {e}")
- print(f" Successfully extracted {len(releases_data)} hero releases")
+ _beatport_log(f" Successfully extracted {len(releases_data)} hero releases")
return releases_data
def scrape_genre_top10_tracks(self, genre_slug, genre_id):
"""Scrape Top 10 tracks lists from genre page (Beatport Top 10 + Hype Top 10 if available)"""
- print(f"Scraping Top 10 tracks for {genre_slug} (ID: {genre_id})")
+ _beatport_log(f"Scraping Top 10 tracks for {genre_slug} (ID: {genre_id})")
genre_url = f"https://www.beatport.com/genre/{genre_slug}/{genre_id}"
@@ -3948,7 +3983,7 @@ class BeatportUnifiedScraper:
track_items = soup.find_all(attrs={'data-testid': 'tracks-list-item'})
if not track_items:
- print(f"No tracks-list-item elements found on {genre_url}")
+ _beatport_log(f"No tracks-list-item elements found on {genre_url}")
return {
'beatport_top10': [],
'hype_top10': [],
@@ -3956,7 +3991,7 @@ class BeatportUnifiedScraper:
'has_hype_section': False
}
- print(f"Found {len(track_items)} total track items")
+ _beatport_log(f"Found {len(track_items)} total track items")
# Extract track data from all items
all_tracks = []
@@ -3983,7 +4018,7 @@ class BeatportUnifiedScraper:
has_hype_section = len(all_tracks) > 10
- print(f"Extracted {len(beatport_top10)} Beatport Top 10 + {len(hype_top10)} Hype Top 10 tracks")
+ _beatport_log(f"Extracted {len(beatport_top10)} Beatport Top 10 + {len(hype_top10)} Hype Top 10 tracks")
return {
'beatport_top10': beatport_top10,
@@ -4059,12 +4094,12 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting track data: {e}")
+ _beatport_log(f"Error extracting track data: {e}")
return None
def scrape_genre_top10_releases(self, genre_slug, genre_id):
"""Scrape Top 10 releases from genre page using .partial-artwork elements"""
- print(f"Scraping Top 10 releases for {genre_slug} (ID: {genre_id})")
+ _beatport_log(f"Scraping Top 10 releases for {genre_slug} (ID: {genre_id})")
genre_url = f"https://www.beatport.com/genre/{genre_slug}/{genre_id}"
@@ -4076,10 +4111,10 @@ class BeatportUnifiedScraper:
partial_artwork_elements = soup.find_all(class_='partial-artwork')
if not partial_artwork_elements:
- print(f"No .partial-artwork elements found on {genre_url}")
+ _beatport_log(f"No .partial-artwork elements found on {genre_url}")
return []
- print(f"Found {len(partial_artwork_elements)} .partial-artwork elements")
+ _beatport_log(f"Found {len(partial_artwork_elements)} .partial-artwork elements")
# Extract release data from each element
releases = []
@@ -4088,7 +4123,7 @@ class BeatportUnifiedScraper:
if release_data:
releases.append(release_data)
- print(f"Extracted {len(releases)} Top 10 releases")
+ _beatport_log(f"Extracted {len(releases)} Top 10 releases")
return releases
def extract_release_data_from_partial_artwork(self, artwork_element, rank):
@@ -4144,7 +4179,7 @@ class BeatportUnifiedScraper:
artist = self.clean_beatport_text(artist) if artist != "Unknown Artist" else artist
label = self.clean_beatport_text(label) if label != "Unknown Label" else label
- print(f" Release #{rank}: '{title}' by '{artist}' [{label}]")
+ _beatport_log(f" Release #{rank}: '{title}' by '{artist}' [{label}]")
return {
'title': title,
@@ -4158,7 +4193,7 @@ class BeatportUnifiedScraper:
}
except Exception as e:
- print(f"Error extracting release data from .partial-artwork: {e}")
+ _beatport_log(f"Error extracting release data from .partial-artwork: {e}")
return None
def extract_hero_release_data(self, release_element) -> Dict:
@@ -4234,7 +4269,7 @@ class BeatportUnifiedScraper:
return data
except Exception as e:
- print(f"Error extracting hero release data: {e}")
+ _beatport_log(f"Error extracting hero release data: {e}")
return {}
def scrape_all_genres(self, tracks_per_genre: int = 100, max_workers: int = 5, include_images: bool = False) -> Dict[str, List[Dict]]:
@@ -4243,7 +4278,7 @@ class BeatportUnifiedScraper:
if not self.all_genres:
self.all_genres = self.discover_genres_with_images(include_images=include_images)
- print(f"\nScraping {len(self.all_genres)} genres...")
+ _beatport_log(f"\nScraping {len(self.all_genres)} genres...")
all_results = {}
completed = 0
@@ -4251,14 +4286,14 @@ class BeatportUnifiedScraper:
def scrape_single_genre(genre):
nonlocal completed
- print(f"Scraping {genre['name']}...")
+ _beatport_log(f"Scraping {genre['name']}...")
tracks = self.scrape_genre_charts(genre, tracks_per_genre)
with self.results_lock:
if tracks: # Only store genres that have tracks
all_results[genre['name']] = tracks
completed += 1
- print(f"{genre['name']}: {len(tracks)} tracks ({completed}/{len(self.all_genres)} complete)")
+ _beatport_log(f"{genre['name']}: {len(tracks)} tracks ({completed}/{len(self.all_genres)} complete)")
return genre['name'], tracks
@@ -4273,7 +4308,7 @@ class BeatportUnifiedScraper:
try:
future.result()
except Exception as e:
- print(f"Error processing {genre['name']}: {e}")
+ _beatport_log(f"Error processing {genre['name']}: {e}")
return all_results
@@ -4304,119 +4339,119 @@ class BeatportUnifiedScraper:
def test_dynamic_genre_discovery():
"""Test the dynamic genre discovery functionality"""
- print("Dynamic Genre Discovery Test")
- print("=" * 80)
+ _beatport_log("Dynamic Genre Discovery Test")
+ _beatport_log("=" * 80)
scraper = BeatportUnifiedScraper()
# Test genre discovery
- print("\nTEST 1: Genre Discovery")
+ _beatport_log("\nTEST 1: Genre Discovery")
genres = scraper.discover_genres_from_homepage()
- print(f"\nDiscovered {len(genres)} genres:")
+ _beatport_log(f"\nDiscovered {len(genres)} genres:")
for i, genre in enumerate(genres[:10]): # Show first 10
- print(f" {i+1:2}. {genre['name']} -> {genre['slug']} (ID: {genre['id']})")
+ _beatport_log(f" {i+1:2}. {genre['name']} -> {genre['slug']} (ID: {genre['id']})")
if 'url' in genre:
- print(f" URL: {genre['url']}")
+ _beatport_log(f" URL: {genre['url']}")
if len(genres) > 10:
- print(f" ... and {len(genres) - 10} more genres")
+ _beatport_log(f" ... and {len(genres) - 10} more genres")
# Test with images (limit to 3 for demo)
- print("\nTEST 2: Genre Discovery with Images (Sample)")
+ _beatport_log("\nTEST 2: Genre Discovery with Images (Sample)")
genres_with_images = scraper.discover_genres_with_images(include_images=True)
- print(f"\nSample genres with images:")
+ _beatport_log(f"\nSample genres with images:")
for genre in genres_with_images[:3]:
- print(f" ⢠{genre['name']}: {genre.get('image_url', 'No image')}")
+ _beatport_log(f" ⢠{genre['name']}: {genre.get('image_url', 'No image')}")
# Test a few genre scrapes
- print("\nTEST 3: Sample Genre Chart Scraping")
+ _beatport_log("\nTEST 3: Sample Genre Chart Scraping")
sample_genres = genres[:3]
for genre in sample_genres:
- print(f"\nTesting {genre['name']}...")
+ _beatport_log(f"\nTesting {genre['name']}...")
tracks = scraper.scrape_genre_charts(genre, limit=3)
if tracks:
- print(f" Found {len(tracks)} tracks:")
+ _beatport_log(f" Found {len(tracks)} tracks:")
for track in tracks:
- print(f" ⢠{track['artist']} - {track['title']}")
+ _beatport_log(f" ⢠{track['artist']} - {track['title']}")
else:
- print(f" No tracks found")
+ _beatport_log(f" No tracks found")
return genres
def test_improved_chart_sections():
"""Test the improved chart section discovery and scraping"""
- print("Testing Improved Chart Section Discovery & Scraping")
- print("=" * 80)
+ _beatport_log("Testing Improved Chart Section Discovery & Scraping")
+ _beatport_log("=" * 80)
scraper = BeatportUnifiedScraper()
# Test 1: Chart Section Discovery
- print("\nTEST 1: Chart Section Discovery")
+ _beatport_log("\nTEST 1: Chart Section Discovery")
chart_discovery = scraper.discover_chart_sections()
- print(f"\nDiscovery Results:")
+ _beatport_log(f"\nDiscovery Results:")
summary = chart_discovery.get('summary', {})
- print(f" ⢠Top Charts sections: {summary.get('top_charts_sections', 0)}")
- print(f" ⢠Staff Picks sections: {summary.get('staff_picks_sections', 0)}")
- print(f" ⢠Other sections: {summary.get('other_sections', 0)}")
- print(f" ⢠Main chart links: {summary.get('main_chart_links', 0)}")
- print(f" ⢠Individual DJ charts: {summary.get('individual_dj_charts', 0)}")
+ _beatport_log(f" ⢠Top Charts sections: {summary.get('top_charts_sections', 0)}")
+ _beatport_log(f" ⢠Staff Picks sections: {summary.get('staff_picks_sections', 0)}")
+ _beatport_log(f" ⢠Other sections: {summary.get('other_sections', 0)}")
+ _beatport_log(f" ⢠Main chart links: {summary.get('main_chart_links', 0)}")
+ _beatport_log(f" ⢠Individual DJ charts: {summary.get('individual_dj_charts', 0)}")
# Test 2: New/Improved Scraping Methods
- print("\nTEST 2: Improved Chart Scraping Methods")
+ _beatport_log("\nTEST 2: Improved Chart Scraping Methods")
# Test Hype Top 100 (fixed URL)
- print("\n2a. Testing Hype Top 100 (fixed URL)...")
+ _beatport_log("\n2a. Testing Hype Top 100 (fixed URL)...")
hype_tracks = scraper.scrape_hype_top_100(limit=5)
if hype_tracks:
- print(f" Found {len(hype_tracks)} tracks:")
+ _beatport_log(f" Found {len(hype_tracks)} tracks:")
for track in hype_tracks[:3]:
- print(f" ⢠{track['artist']} - {track['title']}")
+ _beatport_log(f" ⢠{track['artist']} - {track['title']}")
else:
- print(" No tracks found")
+ _beatport_log(" No tracks found")
# Test Top 100 Releases (new method)
- print("\n2b. Testing Top 100 Releases (new method)...")
+ _beatport_log("\n2b. Testing Top 100 Releases (new method)...")
releases_tracks = scraper.scrape_top_100_releases(limit=5)
if releases_tracks:
- print(f" Found {len(releases_tracks)} tracks:")
+ _beatport_log(f" Found {len(releases_tracks)} tracks:")
for track in releases_tracks[:3]:
- print(f" ⢠{track['artist']} - {track['title']}")
+ _beatport_log(f" ⢠{track['artist']} - {track['title']}")
else:
- print(" No tracks found")
+ _beatport_log(" No tracks found")
# Test Improved New Releases
- print("\n2c. Testing Improved New Releases...")
+ _beatport_log("\n2c. Testing Improved New Releases...")
new_releases = scraper.scrape_new_releases(limit=5)
if new_releases:
- print(f" Found {len(new_releases)} tracks:")
+ _beatport_log(f" Found {len(new_releases)} tracks:")
for track in new_releases[:3]:
- print(f" ⢠{track['artist']} - {track['title']}")
+ _beatport_log(f" ⢠{track['artist']} - {track['title']}")
else:
- print(" No tracks found")
+ _beatport_log(" No tracks found")
# Test Improved DJ Charts
- print("\n2d. Testing Improved DJ Charts...")
+ _beatport_log("\n2d. Testing Improved DJ Charts...")
dj_charts = scraper.scrape_dj_charts(limit=5)
if dj_charts:
- print(f" Found {len(dj_charts)} charts:")
+ _beatport_log(f" Found {len(dj_charts)} charts:")
for chart in dj_charts[:3]:
- print(f" ⢠{chart['title']} by {chart['artist']}")
+ _beatport_log(f" ⢠{chart['title']} by {chart['artist']}")
else:
- print(" No charts found")
+ _beatport_log(" No charts found")
# Test Improved Featured Charts
- print("\n2e. Testing Improved Featured Charts...")
+ _beatport_log("\n2e. Testing Improved Featured Charts...")
featured_charts = scraper.scrape_featured_charts(limit=5)
if featured_charts:
- print(f" Found {len(featured_charts)} items:")
+ _beatport_log(f" Found {len(featured_charts)} items:")
for item in featured_charts[:3]:
- print(f" ⢠{item['title']} by {item['artist']}")
+ _beatport_log(f" ⢠{item['title']} by {item['artist']}")
else:
- print(" No items found")
+ _beatport_log(" No items found")
return {
'chart_discovery': chart_discovery,
@@ -4429,66 +4464,66 @@ def test_improved_chart_sections():
def main():
"""Test the unified Beatport scraper"""
- print("Beatport Unified Scraper - Improved Chart Discovery")
- print("=" * 80)
+ _beatport_log("Beatport Unified Scraper - Improved Chart Discovery")
+ _beatport_log("=" * 80)
scraper = BeatportUnifiedScraper()
# Test New on Beatport Hero first
- print("\nNEW ON BEATPORT HERO TEST")
+ _beatport_log("\nNEW ON BEATPORT HERO TEST")
hero_tracks = scraper.scrape_new_on_beatport_hero(limit=10)
if hero_tracks:
- print(f"Successfully extracted {len(hero_tracks)} tracks from hero slideshow")
+ _beatport_log(f"Successfully extracted {len(hero_tracks)} tracks from hero slideshow")
for i, track in enumerate(hero_tracks[:3]): # Show first 3
- print(f" {i+1}. {track.get('title', 'No title')} - {track.get('artist', 'No artist')}")
- print(f" URL: {track.get('url', 'No URL')}")
- print(f" Classes: {track.get('element_classes', 'No classes')}")
+ _beatport_log(f" {i+1}. {track.get('title', 'No title')} - {track.get('artist', 'No artist')}")
+ _beatport_log(f" URL: {track.get('url', 'No URL')}")
+ _beatport_log(f" Classes: {track.get('element_classes', 'No classes')}")
else:
- print("No tracks found in hero slideshow")
+ _beatport_log("No tracks found in hero slideshow")
# Test improved chart sections
- print("\nš IMPROVED CHART SECTIONS TEST")
+ _beatport_log("\nš IMPROVED CHART SECTIONS TEST")
improved_results = test_improved_chart_sections()
# Test dynamic genre discovery (existing)
- print("\n\nš DYNAMIC GENRE DISCOVERY TEST")
+ _beatport_log("\n\nš DYNAMIC GENRE DISCOVERY TEST")
discovered_genres = test_dynamic_genre_discovery()
# Update scraper with discovered genres
scraper.all_genres = discovered_genres
# Test 1: Top 100
- print("\nTEST 1: Top 100 Chart")
+ _beatport_log("\nTEST 1: Top 100 Chart")
top_100 = scraper.scrape_top_100(limit=10) # Test with 10 for now
if top_100:
- print(f"\nTop 100 Sample (showing first 5):")
+ _beatport_log(f"\nTop 100 Sample (showing first 5):")
for track in top_100[:5]:
- print(f" {track['position']}. {track['artist']} - {track['title']}")
+ _beatport_log(f" {track['position']}. {track['artist']} - {track['title']}")
quality = scraper.test_data_quality(top_100)
- print(f"\nData Quality: {quality['quality_score']:.1f}% ({quality['valid_tracks']}/{quality['total_tracks']} tracks)")
+ _beatport_log(f"\nData Quality: {quality['quality_score']:.1f}% ({quality['valid_tracks']}/{quality['total_tracks']} tracks)")
else:
- print("Failed to extract Top 100")
+ _beatport_log("Failed to extract Top 100")
# Test 2: Sample of discovered genres
- print("\nTEST 2: Dynamic Genre Charts Sample")
+ _beatport_log("\nTEST 2: Dynamic Genre Charts Sample")
test_genres = scraper.all_genres[:5] # Test first 5 discovered genres
- print(f"Testing {len(test_genres)} dynamically discovered genres...")
+ _beatport_log(f"Testing {len(test_genres)} dynamically discovered genres...")
genre_results = {}
for genre in test_genres:
tracks = scraper.scrape_genre_charts(genre, limit=5) # 5 tracks per genre for testing
if tracks:
genre_results[genre['name']] = tracks
- print(f"\n{genre['name']} Top 5:")
+ _beatport_log(f"\n{genre['name']} Top 5:")
for track in tracks[:3]:
- print(f" ⢠{track['artist']} - {track['title']}")
+ _beatport_log(f" ⢠{track['artist']} - {track['title']}")
# Test 3: Full genre scraping (smaller sample)
- print("\nTEST 3: Full Multi-Genre Scraping")
- print("Testing parallel scraping of 10 genres...")
+ _beatport_log("\nTEST 3: Full Multi-Genre Scraping")
+ _beatport_log("Testing parallel scraping of 10 genres...")
sample_genres = scraper.all_genres[:10]
scraper.all_genres = sample_genres # Temporarily limit for testing
@@ -4496,29 +4531,29 @@ def main():
all_genre_results = scraper.scrape_all_genres(tracks_per_genre=5, max_workers=3)
# Results summary
- print("\n" + "=" * 80)
- print("FINAL RESULTS SUMMARY")
- print("=" * 80)
+ _beatport_log("\n" + "=" * 80)
+ _beatport_log("FINAL RESULTS SUMMARY")
+ _beatport_log("=" * 80)
total_tracks = len(top_100) if top_100 else 0
total_genres = len(all_genre_results)
total_genre_tracks = sum(len(tracks) for tracks in all_genre_results.values())
- print(f"⢠Top 100 tracks extracted: {total_tracks}")
- print(f"⢠Genres successfully scraped: {total_genres}")
- print(f"⢠Total genre tracks: {total_genre_tracks}")
- print(f"⢠Grand total tracks: {total_tracks + total_genre_tracks}")
+ _beatport_log(f"⢠Top 100 tracks extracted: {total_tracks}")
+ _beatport_log(f"⢠Genres successfully scraped: {total_genres}")
+ _beatport_log(f"⢠Total genre tracks: {total_genre_tracks}")
+ _beatport_log(f"⢠Grand total tracks: {total_tracks + total_genre_tracks}")
# Data quality assessment
all_tracks = (top_100 or []) + [track for tracks in all_genre_results.values() for track in tracks]
if all_tracks:
overall_quality = scraper.test_data_quality(all_tracks)
- print(f"\nOVERALL DATA QUALITY")
- print(f"⢠Quality Score: {overall_quality['quality_score']:.1f}%")
- print(f"⢠Valid Tracks: {overall_quality['valid_tracks']}/{overall_quality['total_tracks']}")
+ _beatport_log(f"\nOVERALL DATA QUALITY")
+ _beatport_log(f"⢠Quality Score: {overall_quality['quality_score']:.1f}%")
+ _beatport_log(f"⢠Valid Tracks: {overall_quality['valid_tracks']}/{overall_quality['total_tracks']}")
if overall_quality['issues']:
- print(f"⢠Issues Found: {len(overall_quality['issues'])}")
+ _beatport_log(f"⢠Issues Found: {len(overall_quality['issues'])}")
# Save results
results = {
@@ -4536,28 +4571,28 @@ def main():
try:
with open('beatport_unified_results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
- print(f"\nResults saved to beatport_unified_results.json")
+ _beatport_log(f"\nResults saved to beatport_unified_results.json")
except Exception as e:
- print(f"Failed to save results: {e}")
+ _beatport_log(f"Failed to save results: {e}")
# Virtual playlist possibilities
if overall_quality['quality_score'] > 70:
- print(f"\nSUCCESS! Ready for virtual playlist creation")
- print(f"You can now create playlists for:")
- print(f" ⢠Beatport Top 100")
+ _beatport_log(f"\nSUCCESS! Ready for virtual playlist creation")
+ _beatport_log(f"You can now create playlists for:")
+ _beatport_log(f" ⢠Beatport Top 100")
for genre_name in list(all_genre_results.keys())[:5]:
- print(f" ⢠{genre_name} Top 100")
+ _beatport_log(f" ⢠{genre_name} Top 100")
if len(all_genre_results) > 5:
- print(f" ⢠...and {len(all_genre_results) - 5} more genres!")
+ _beatport_log(f" ⢠...and {len(all_genre_results) - 5} more genres!")
- print(f"\nIntegration Notes:")
- print(f" ⢠Artist and title data is clean and ready")
- print(f" ⢠{total_genres} genres confirmed working")
- print(f" ⢠Data quality: {overall_quality['quality_score']:.1f}%")
+ _beatport_log(f"\nIntegration Notes:")
+ _beatport_log(f" ⢠Artist and title data is clean and ready")
+ _beatport_log(f" ⢠{total_genres} genres confirmed working")
+ _beatport_log(f" ⢠Data quality: {overall_quality['quality_score']:.1f}%")
else:
- print(f"\nData quality needs improvement ({overall_quality['quality_score']:.1f}%)")
- print(f"Consider refining extraction methods")
+ _beatport_log(f"\nData quality needs improvement ({overall_quality['quality_score']:.1f}%)")
+ _beatport_log(f"Consider refining extraction methods")
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/config/settings.py b/config/settings.py
index e304f1aa..19b24e6c 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -5,8 +5,14 @@ import sqlite3
from typing import Dict, Any, Optional
from cryptography.fernet import Fernet, InvalidToken
from pathlib import Path
+from utils.logging_config import get_logger
+
+
+logger = get_logger("config")
class ConfigManager:
+ _VALID_LOG_LEVELS = frozenset({"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"})
+
def __init__(self, config_path: str = "config/config.json"):
# Determine strict absolute path to settings.py directory to help resolve config.json
# This handles cases where CWD is different (e.g. running from /Users vs /Users/project)
@@ -33,7 +39,7 @@ class ConfigManager:
# Default to project path even if it doesn't exist yet (for creation/fallback)
self.config_path = project_path
- print(f"ConfigManager initialized with path: {self.config_path}")
+ logger.info(f"ConfigManager initialized with path: {self.config_path}")
self.config_data: Dict[str, Any] = {}
self._fernet: Optional[Fernet] = None
@@ -45,7 +51,7 @@ class ConfigManager:
else:
self.database_path = self.base_dir / "database" / "music_library.db"
- print(f"Database path set to: {self.database_path}")
+ logger.info(f"Database path set to: {self.database_path}")
self.load_config(str(self.config_path))
@@ -107,7 +113,7 @@ class ConfigManager:
try:
import shutil
shutil.move(str(old_key_file), str(key_file))
- print(f"[MIGRATE] Moved encryption key to {key_file}")
+ logger.info(f"Moved encryption key to {key_file}")
except Exception:
key_file = old_key_file # Fall back to old location
if key_file.exists():
@@ -155,8 +161,10 @@ class ConfigManager:
return decrypted
except InvalidToken:
# Key mismatch ā encrypted with a different key (key file deleted/replaced)
- print(f"[ERROR] Failed to decrypt a config value ā encryption key may have changed. "
- f"Re-enter credentials in Settings or restore the original .encryption_key file.")
+ logger.error(
+ "Failed to decrypt a config value ā encryption key may have changed. "
+ "Re-enter credentials in Settings or restore the original .encryption_key file."
+ )
return value
except Exception:
return value
@@ -243,11 +251,11 @@ class ConfigManager:
needs_migration = True
break
if needs_migration:
- print("[MIGRATE] Encrypting sensitive config values at rest...")
+ logger.info("Encrypting sensitive config values at rest...")
self._save_to_database(self.config_data)
- print("[OK] Sensitive config values encrypted successfully")
+ logger.info("Sensitive config values encrypted successfully")
except Exception as e:
- print(f"[WARN] Could not migrate encryption: {e}")
+ logger.warning(f"Could not migrate encryption: {e}")
def _ensure_database_exists(self):
"""Ensure database file and metadata table exist"""
@@ -271,7 +279,7 @@ class ConfigManager:
conn.commit()
conn.close()
except Exception as e:
- print(f"Warning: Could not ensure database exists: {e}")
+ logger.warning(f"Could not ensure database exists: {e}")
def _load_from_database(self) -> Optional[Dict[str, Any]]:
"""Load configuration from database, decrypting sensitive values."""
@@ -289,18 +297,70 @@ class ConfigManager:
config_data = json.loads(row[0])
# Decrypt sensitive values (gracefully handles plaintext migration)
config_data = self._decrypt_sensitive(config_data)
- print("[OK] Configuration loaded from database")
+ logger.info("Configuration loaded from database")
return config_data
else:
return None
except Exception as e:
- print(f"Warning: Could not load config from database: {e}")
+ logger.warning(f"Could not load config from database: {e}")
+ return None
+ finally:
+ if conn:
+ conn.close()
+
+ def _load_stored_log_level(self) -> Optional[str]:
+ """Load the persisted UI log level preference, if one exists."""
+ conn = None
+ try:
+ self._ensure_database_exists()
+ conn = sqlite3.connect(str(self.database_path), timeout=30.0)
+ conn.execute("PRAGMA journal_mode=WAL")
+ cursor = conn.cursor()
+ cursor.execute("SELECT value FROM metadata WHERE key = 'log_level'")
+ row = cursor.fetchone()
+ if not row or not row[0]:
+ return None
+
+ level = str(row[0]).upper()
+ if level not in self._VALID_LOG_LEVELS:
+ logger.warning(f"Ignoring invalid stored log level: {row[0]}")
+ return None
+ return level
+ except Exception as e:
+ logger.warning(f"Could not load stored log level from database: {e}")
return None
finally:
if conn:
conn.close()
+ def _load_env_log_level(self) -> Optional[str]:
+ """Load the log level override from the environment, if one exists."""
+ raw_level = os.environ.get("SOULSYNC_LOG_LEVEL")
+ if not raw_level:
+ return None
+
+ level = raw_level.upper()
+ if level not in self._VALID_LOG_LEVELS:
+ logger.warning(f"Ignoring invalid SOULSYNC_LOG_LEVEL value: {raw_level}")
+ return None
+
+ return level
+
+ def _apply_log_level_overrides(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
+ """Overlay env and persisted log level preferences onto the loaded config."""
+ env_level = self._load_env_log_level()
+ if env_level:
+ config_data.setdefault("logging", {})["level"] = env_level
+ logger.info(f"Using log level from SOULSYNC_LOG_LEVEL: {env_level}")
+ return config_data
+
+ stored_level = self._load_stored_log_level()
+ if stored_level:
+ config_data.setdefault("logging", {})["level"] = stored_level
+ logger.info(f"Using stored logging level from database: {stored_level}")
+ return config_data
+
def _save_to_database(self, config_data: Dict[str, Any]) -> bool:
"""Save configuration to database, encrypting sensitive values."""
conn = None
@@ -325,7 +385,7 @@ class ConfigManager:
return True
except Exception as e:
- print(f"Error: Could not save config to database: {e}")
+ logger.error(f"Could not save config to database: {e}")
return False
finally:
if conn:
@@ -337,12 +397,12 @@ class ConfigManager:
if self.config_path.exists():
with open(self.config_path, 'r') as f:
config_data = json.load(f)
- print(f"[OK] Configuration loaded from {self.config_path}")
+ logger.info(f"Configuration loaded from {self.config_path}")
return config_data
else:
return None
except Exception as e:
- print(f"Warning: Could not load config from file: {e}")
+ logger.warning(f"Could not load config from file: {e}")
return None
def _get_default_config(self) -> Dict[str, Any]:
@@ -506,45 +566,45 @@ class ConfigManager:
2. config.json (migration from file-based config)
3. Defaults (fresh install)
"""
- print(f"Loading configuration...")
+ logger.info("Loading configuration...")
# Try loading from database first
config_data = self._load_from_database()
if config_data:
# Configuration exists in database
- self.config_data = config_data
+ self.config_data = self._apply_log_level_overrides(config_data)
# Ensure sensitive values are encrypted at rest (one-time migration)
self._migrate_encrypt_if_needed()
return
# Database is empty - try migration from config.json
- print(f"Configuration not found in database. Attempting migration from: {self.config_path}")
+ logger.info(f"Configuration not found in database. Attempting migration from: {self.config_path}")
config_data = self._load_from_config_file()
if config_data:
# Migrate from config.json to database
- print("[MIGRATE] Migrating configuration from config.json to database...")
+ logger.info("Migrating configuration from config.json to database...")
if self._save_to_database(config_data):
- print("[OK] Configuration migrated successfully to database.")
- self.config_data = config_data
+ logger.info("Configuration migrated successfully to database.")
+ self.config_data = self._apply_log_level_overrides(config_data)
return
else:
- print("[WARN] Migration failed - using file-based config temporarily.")
- self.config_data = config_data
+ logger.warning("Migration failed - using file-based config temporarily.")
+ self.config_data = self._apply_log_level_overrides(config_data)
return
# No config.json either - use defaults
- print("[INFO] ā¹ļø No existing configuration found (DB or File) - using defaults")
+ logger.info("No existing configuration found (DB or File) - using defaults")
config_data = self._get_default_config()
# Try to save defaults to database
if self._save_to_database(config_data):
- print("[OK] Default configuration saved to database")
+ logger.info("Default configuration saved to database")
else:
- print("[WARN] Could not save defaults to database - using in-memory config")
+ logger.warning("Could not save defaults to database - using in-memory config")
- self.config_data = config_data
+ self.config_data = self._apply_log_level_overrides(config_data)
def _save_config(self):
"""Save configuration to database with retry on lock."""
@@ -558,14 +618,14 @@ class ConfigManager:
if not success:
# Fallback: Try to save to config.json if database fails
- print("[WARN] Database save failed - attempting file fallback")
+ logger.warning("Database save failed - attempting file fallback")
try:
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w') as f:
json.dump(self.config_data, f, indent=2)
- print("[OK] Configuration saved to config.json as fallback")
+ logger.info("Configuration saved to config.json as fallback")
except Exception as e:
- print(f"[ERROR] Failed to save configuration: {e}")
+ logger.error(f"Failed to save configuration: {e}")
def get(self, key: str, default: Any = None) -> Any:
keys = key.split('.')
diff --git a/core/acoustid_client.py b/core/acoustid_client.py
index 80c64709..a0edb64b 100644
--- a/core/acoustid_client.py
+++ b/core/acoustid_client.py
@@ -20,6 +20,7 @@ from typing import Dict, List, Optional, Any, Tuple
from pathlib import Path
import os
import shutil
+import logging
import logging.handlers
from utils.logging_config import get_logger
@@ -29,22 +30,23 @@ from config.settings import config_manager
FPCALC_BIN_DIR = Path(__file__).parent.parent / "bin"
CHROMAPRINT_VERSION = "1.5.1"
-# Set up dedicated AcoustID logger with its own file
-logger = get_logger("acoustid_client")
-
-# Add dedicated file handler for AcoustID logs
-_acoustid_log_path = Path(__file__).parent.parent / "logs" / "acoustid.log"
+_acoustid_logger = logging.getLogger("soulsync.acoustid")
+_acoustid_logger.setLevel(logging.DEBUG)
+_acoustid_log_path = Path(config_manager.get('logging.path', 'logs/app.log')).parent / "acoustid.log"
_acoustid_log_path.parent.mkdir(parents=True, exist_ok=True)
-_acoustid_file_handler = logging.handlers.RotatingFileHandler(
- _acoustid_log_path, encoding='utf-8', maxBytes=5*1024*1024, backupCount=2
-)
-_acoustid_file_handler.setLevel(logging.DEBUG)
-_acoustid_file_handler.setFormatter(logging.Formatter(
- fmt='%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
-))
-logger.addHandler(_acoustid_file_handler)
-logging.getLogger("newmusic.acoustid_verification").addHandler(_acoustid_file_handler)
+if not _acoustid_logger.handlers:
+ _acoustid_file_handler = logging.handlers.RotatingFileHandler(
+ _acoustid_log_path, encoding='utf-8', maxBytes=5*1024*1024, backupCount=2
+ )
+ _acoustid_file_handler.setLevel(logging.DEBUG)
+ _acoustid_file_handler.setFormatter(logging.Formatter(
+ fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+ ))
+ _acoustid_logger.addHandler(_acoustid_file_handler)
+ _acoustid_logger.propagate = False
+
+logger = get_logger("acoustid.client")
# Check if pyacoustid is available
try:
@@ -194,7 +196,7 @@ class AcoustIDClient:
result = client.fingerprint_and_lookup("/path/to/audio.mp3")
if result:
for mbid in result['recording_mbids']:
- print(f"Match: {mbid}")
+ logger.info(f"Match: {mbid}")
"""
def __init__(self):
diff --git a/core/acoustid_verification.py b/core/acoustid_verification.py
index 5bfb6f92..ee4100b5 100644
--- a/core/acoustid_verification.py
+++ b/core/acoustid_verification.py
@@ -17,7 +17,7 @@ from utils.logging_config import get_logger
from core.acoustid_client import AcoustIDClient
from core.musicbrainz_client import MusicBrainzClient
-logger = get_logger("acoustid_verification")
+logger = get_logger("acoustid.verification")
# Thresholds
MIN_ACOUSTID_SCORE = 0.80 # Minimum AcoustID fingerprint score to trust
diff --git a/core/api_call_tracker.py b/core/api_call_tracker.py
index 104e1a29..357ba4e4 100644
--- a/core/api_call_tracker.py
+++ b/core/api_call_tracker.py
@@ -12,6 +12,11 @@ import threading
import time
from collections import deque, defaultdict
+from utils.logging_config import get_logger
+
+
+logger = get_logger("api_call_tracker")
+
# Known rate limits per service (calls/minute)
RATE_LIMITS = {
@@ -281,13 +286,16 @@ class ApiCallTracker:
with open(_PERSIST_PATH, 'w') as f:
json.dump({'ts': now, 'history': data, 'events': events}, f)
except Exception as e:
- print(f"[ApiCallTracker] Failed to save history: {e}")
+ logger.error(f"[ApiCallTracker] Failed to save history: {e}")
def _load(self):
"""Restore 24h minute history from disk. Called on init."""
try:
if not os.path.exists(_PERSIST_PATH):
return
+ if os.path.getsize(_PERSIST_PATH) == 0:
+ logger.info(f"[ApiCallTracker] History file is empty, starting fresh: {_PERSIST_PATH}")
+ return
with open(_PERSIST_PATH, 'r') as f:
raw = json.load(f)
saved_ts = raw.get('ts', 0)
@@ -305,9 +313,11 @@ class ApiCallTracker:
for e in events:
if e.get('ts', 0) >= cutoff:
self._events.append(e)
- print(f"[ApiCallTracker] Restored history for {len(history)} services, {len(events)} events")
+ logger.info(f"[ApiCallTracker] Restored history for {len(history)} services, {len(events)} events")
+ except json.JSONDecodeError as e:
+ logger.warning(f"[ApiCallTracker] History file is not valid JSON, starting fresh: {_PERSIST_PATH} ({e})")
except Exception as e:
- print(f"[ApiCallTracker] Failed to load history: {e}")
+ logger.error(f"[ApiCallTracker] Failed to load history: {e}")
# Singleton instance
diff --git a/core/database_update_worker.py b/core/database_update_worker.py
index 77c23436..e0645e2d 100644
--- a/core/database_update_worker.py
+++ b/core/database_update_worker.py
@@ -930,10 +930,16 @@ class DatabaseUpdateWorker:
if (db_track.title != current_title or
db_track.artist_name != current_artist or
db_track.album_title != current_album):
- logger.debug(f"Metadata change detected for track ID {track_id}:")
- logger.debug(f" Title: '{db_track.title}' ā '{current_title}'")
- logger.debug(f" Artist: '{db_track.artist_name}' ā '{current_artist}'")
- logger.debug(f" Album: '{db_track.album_title}' ā '{current_album}'")
+ logger.debug(
+ "Metadata change detected for track %s: title=%rā%r artist=%rā%r album=%rā%r",
+ track_id,
+ db_track.title,
+ current_title,
+ db_track.artist_name,
+ current_artist,
+ db_track.album_title,
+ current_album,
+ )
changes_detected += 1
except Exception as e:
diff --git a/core/download_orchestrator.py b/core/download_orchestrator.py
index f91a9c55..5f120309 100644
--- a/core/download_orchestrator.py
+++ b/core/download_orchestrator.py
@@ -61,10 +61,12 @@ class DownloadOrchestrator:
logger.info(f"Download Orchestrator initialized - Mode: {self.mode}")
if self.mode == 'hybrid':
- if self.hybrid_order:
- logger.info(f" Source priority: {' ā '.join(self.hybrid_order)}")
- else:
- logger.info(f" Primary: {self.hybrid_primary}, Fallback: {self.hybrid_secondary}")
+ logger.info(
+ "Hybrid source order: order=%s primary=%s secondary=%s",
+ " ā ".join(self.hybrid_order) if self.hybrid_order else "default",
+ self.hybrid_primary,
+ self.hybrid_secondary,
+ )
def _safe_init(self, name, cls):
"""Initialize a download client, returning None on failure instead of crashing."""
@@ -154,8 +156,10 @@ class DownloadOrchestrator:
except Exception:
results[source] = False
- status_parts = [f"{s}: {'' if ok else ''}" for s, ok in results.items()]
- logger.info(f" {' | '.join(status_parts)}")
+ logger.info(
+ "Hybrid connection check: %s",
+ " | ".join(f"{source}={'ok' if ok else 'fail'}" for source, ok in results.items()),
+ )
return any(results.values())
diff --git a/core/jellyfin_client.py b/core/jellyfin_client.py
index dac2212a..dcd4f14d 100644
--- a/core/jellyfin_client.py
+++ b/core/jellyfin_client.py
@@ -1633,14 +1633,14 @@ class JellyfinClient:
def is_library_scanning(self, library_name: str = "Music") -> bool:
"""Check if Jellyfin library is currently scanning"""
if not self.ensure_connection():
- logger.debug("DEBUG: Not connected to Jellyfin, cannot check scan status")
+ logger.debug("Not connected to Jellyfin, cannot check scan status")
return False
try:
# Check scheduled tasks for library scan activities
response = self._make_request('/ScheduledTasks')
if not response:
- logger.debug("DEBUG: Could not get scheduled tasks")
+ logger.debug("Could not get scheduled tasks")
return False
for task in response:
@@ -1650,10 +1650,14 @@ class JellyfinClient:
# Look for library scan related tasks that are running
if ('scan' in task_name or 'refresh' in task_name or 'library' in task_name):
if task_state in ['Running', 'Cancelling']:
- logger.debug(f"DEBUG: Found running scan task: {task.get('Name')} (State: {task_state})")
+ logger.debug(
+ "Found running scan task: name=%s state=%s",
+ task.get('Name'),
+ task_state,
+ )
return True
- logger.debug("DEBUG: No active scan tasks detected")
+ logger.debug("No active scan tasks detected")
return False
except Exception as e:
@@ -1819,4 +1823,4 @@ class JellyfinClient:
return True
except Exception as e:
logger.error(f"Error setting metadata-only mode: {e}")
- return False
\ No newline at end of file
+ return False
diff --git a/core/matching_engine.py b/core/matching_engine.py
index d38a5916..da1fb9bc 100644
--- a/core/matching_engine.py
+++ b/core/matching_engine.py
@@ -421,7 +421,7 @@ class MusicMatchingEngine:
if is_likely_album and 4 <= len(potential_album_part) <= 30:
cleaned_title = re.sub(dash_pattern, '', track_title).strip()
- print(f"Heuristic album detection: '{original_title}' ā '{cleaned_title}' (removed: '{potential_album_part}')")
+ logger.debug(f"Heuristic album detection: '{original_title}' ā '{cleaned_title}' (removed: '{potential_album_part}')")
return cleaned_title, True
return track_title, False
@@ -1004,13 +1004,13 @@ class MusicMatchingEngine:
# Debug logging for troubleshooting
if scored_results and not confident_results:
- print(f"DEBUG: Found {len(scored_results)} scored results but none met confidence threshold 0.58")
+ logger.debug(f"Found {len(scored_results)} scored results but none met confidence threshold 0.58")
for i, result in enumerate(sorted_results[:3]): # Show top 3
- print(f" {i+1}. {result.confidence:.3f} - {getattr(result, 'version_type', 'unknown')} - {result.filename[:60]}...")
+ logger.debug(f" {i+1}. {result.confidence:.3f} - {getattr(result, 'version_type', 'unknown')} - {result.filename[:60]}...")
elif confident_results:
- print(f"DEBUG: {len(confident_results)} results passed confidence threshold 0.58")
+ logger.debug(f"{len(confident_results)} results passed confidence threshold 0.58")
for i, result in enumerate(confident_results[:3]): # Show top 3
- print(f" {i+1}. {result.confidence:.3f} - {getattr(result, 'version_type', 'unknown')} - {result.filename[:60]}...")
+ logger.debug(f" {i+1}. {result.confidence:.3f} - {getattr(result, 'version_type', 'unknown')} - {result.filename[:60]}...")
return confident_results
diff --git a/core/metadata_service.py b/core/metadata_service.py
index cf1f749f..efe4c982 100644
--- a/core/metadata_service.py
+++ b/core/metadata_service.py
@@ -1052,7 +1052,7 @@ def check_album_completion(
if total_tracks == 0 and album_id:
logger.debug("No track count found for '%s' (%s)", album_name, album_id)
- print(f"Checking album: '{album_name}' ({total_tracks} tracks)")
+ logger.debug(f"Checking album: '{album_name}' ({total_tracks} tracks)")
formats = []
# Check if album exists in database with completeness info
@@ -1068,7 +1068,7 @@ def check_album_completion(
candidate_albums=candidate_albums
)
except Exception as db_error:
- print(f"Database error for album '{album_name}': {db_error}")
+ logger.error(f"Database error for album '{album_name}': {db_error}")
return {
"id": album_id,
"name": album_name,
@@ -1096,7 +1096,14 @@ def check_album_completion(
else:
status = "missing"
- print(f" Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
+ logger.debug(
+ "Album completion result: owned=%s expected=%s total=%s completion=%.1f status=%s",
+ owned_tracks,
+ expected_tracks or total_tracks,
+ total_tracks,
+ completion_percentage,
+ status,
+ )
return {
"id": album_id,
@@ -1111,7 +1118,7 @@ def check_album_completion(
}
except Exception as e:
- print(f"Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
+ logger.error(f"Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
return {
"id": album_data.get('id', ''),
"name": album_data.get('name', 'Unknown'),
@@ -1153,7 +1160,12 @@ def check_single_completion(
if total_tracks == 0:
total_tracks = _resolve_completion_track_total(single_data, source_chain) or 1
- print(f"Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
+ logger.debug(
+ "Checking %s: name=%r tracks=%s",
+ album_type,
+ single_name,
+ total_tracks,
+ )
if album_type == 'ep' or total_tracks > 1:
try:
@@ -1168,7 +1180,7 @@ def check_single_completion(
candidate_albums=candidate_albums
)
except Exception as db_error:
- print(f"Database error for EP '{single_name}': {db_error}")
+ logger.error(f"Database error for EP '{single_name}': {db_error}")
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
db_album = None
@@ -1184,7 +1196,14 @@ def check_single_completion(
else:
status = "missing"
- print(f" EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
+ logger.debug(
+ "EP completion result: owned=%s expected=%s total=%s completion=%.1f status=%s",
+ owned_tracks,
+ expected_tracks or total_tracks,
+ total_tracks,
+ completion_percentage,
+ status,
+ )
return {
"id": single_id,
@@ -1210,7 +1229,7 @@ def check_single_completion(
candidate_tracks=candidate_tracks
)
except Exception as db_error:
- print(f"Database error for single '{single_name}': {db_error}")
+ logger.error(f"Database error for single '{single_name}': {db_error}")
db_track, confidence = None, 0.0
owned_tracks = 1 if db_track else 0
@@ -1226,7 +1245,12 @@ def check_single_completion(
elif ext:
formats = [ext]
- print(f" Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
+ logger.debug(
+ "Single completion result: owned=%s expected=1 completion=%.1f status=%s",
+ owned_tracks,
+ completion_percentage,
+ status,
+ )
return {
"id": single_id,
@@ -1242,7 +1266,7 @@ def check_single_completion(
}
except Exception as e:
- print(f"Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
+ logger.error(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'),
diff --git a/core/plex_client.py b/core/plex_client.py
index 39e0e9f4..dc97968b 100644
--- a/core/plex_client.py
+++ b/core/plex_client.py
@@ -937,7 +937,7 @@ class PlexClient:
def is_library_scanning(self, library_name: str = "Music") -> bool:
"""Check if Plex library is currently scanning"""
if not self.ensure_connection():
- logger.debug(f"DEBUG: Not connected to Plex, cannot check scan status")
+ logger.debug("Not connected to Plex, cannot check scan status")
return False
try:
@@ -946,31 +946,31 @@ class PlexClient:
# Check if library has a scanning attribute or is refreshing
# The Plex API exposes this through the library's refreshing property
refreshing = hasattr(library, 'refreshing') and library.refreshing
- logger.debug(f"DEBUG: Library.refreshing = {refreshing}")
+ logger.debug("Library.refreshing = %s", refreshing)
if refreshing:
- logger.debug(f"DEBUG: Library is refreshing")
+ logger.debug("Library is refreshing")
return True
# Alternative method: Check server activities for scanning
try:
activities = self.server.activities()
- logger.debug(f"DEBUG: Found {len(activities)} server activities")
+ logger.debug("Found %s server activities", len(activities))
for activity in activities:
# Look for library scan activities
activity_type = getattr(activity, 'type', 'unknown')
activity_title = getattr(activity, 'title', 'unknown')
- logger.debug(f"DEBUG: Activity - type: {activity_type}, title: {activity_title}")
+ logger.debug("Activity - type=%s title=%s", activity_type, activity_title)
if (activity_type in ['library.scan', 'library.refresh'] and
library_name.lower() in activity_title.lower()):
- logger.debug(f"DEBUG: Found matching scan activity: {activity_title}")
+ logger.debug("Found matching scan activity: %s", activity_title)
return True
except Exception as activities_error:
logger.debug(f"Could not check server activities: {activities_error}")
- logger.debug(f"DEBUG: No scan activity detected")
+ logger.debug("No scan activity detected")
return False
except Exception as e:
diff --git a/core/soulid_worker.py b/core/soulid_worker.py
index cc800460..5dc011bb 100644
--- a/core/soulid_worker.py
+++ b/core/soulid_worker.py
@@ -455,11 +455,23 @@ class SoulIDWorker:
matching.normalize_string(db_name)
)
if score >= self.album_match_threshold:
- logger.debug(f" {source_name}: matched '{artist.name}' via album '{api_name}' ā '{db_name}' (score={score:.2f})")
+ logger.debug(
+ "%s matched artist=%r via album api=%r db=%r score=%.2f",
+ source_name,
+ artist.name,
+ api_name,
+ db_name,
+ score,
+ )
return discog
except Exception as e:
- logger.debug(f" {source_name}: discography fetch failed for '{artist.name}': {e}")
+ logger.debug(
+ "%s discography fetch failed for artist=%r: %s",
+ source_name,
+ artist.name,
+ e,
+ )
continue
return None
diff --git a/core/tag_writer.py b/core/tag_writer.py
index 9ea89a34..cbdfdf11 100644
--- a/core/tag_writer.py
+++ b/core/tag_writer.py
@@ -16,7 +16,7 @@ from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm
from mutagen.oggvorbis import OggVorbis
from mutagen.apev2 import APEv2, APENoHeaderError
-logger = logging.getLogger("newmusic.tag_writer")
+logger = logging.getLogger("tag_writer")
# Supported extensions
SUPPORTED_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.oga', '.opus', '.m4a', '.mp4'}
diff --git a/core/wishlist_service.py b/core/wishlist_service.py
index b48e8ba5..0885bf09 100644
--- a/core/wishlist_service.py
+++ b/core/wishlist_service.py
@@ -311,15 +311,17 @@ class WishlistService:
def _spotify_track_object_to_dict(self, spotify_track) -> Dict[str, Any]:
"""Convert a Spotify track object or TrackResult object to a dictionary"""
try:
- # Add debug logging to see what we're dealing with
- logger.info(f"DEBUG: Converting track object to dict. Type: {type(spotify_track)}")
- logger.info(f"DEBUG: Has 'title' attribute: {hasattr(spotify_track, 'title')}")
- logger.info(f"DEBUG: Has 'artist' attribute: {hasattr(spotify_track, 'artist')}")
- logger.info(f"DEBUG: Has 'id' attribute: {hasattr(spotify_track, 'id')}")
+ logger.debug(
+ "Converting track object to dict: type=%s has_title=%s has_artist=%s has_id=%s",
+ type(spotify_track),
+ hasattr(spotify_track, 'title'),
+ hasattr(spotify_track, 'artist'),
+ hasattr(spotify_track, 'id'),
+ )
# Check if this is a TrackResult object (has title/artist but no id)
if hasattr(spotify_track, 'title') and hasattr(spotify_track, 'artist') and not hasattr(spotify_track, 'id'):
- logger.info("DEBUG: Detected TrackResult object, converting...")
+ logger.debug("Detected TrackResult object, converting")
# Handle TrackResult objects - these don't have Spotify IDs
album_name = getattr(spotify_track, 'album', '') or getattr(spotify_track, 'title', 'Unknown Album')
result = {
@@ -333,19 +335,23 @@ class WishlistService:
'popularity': 0,
'source': 'trackresult'
}
- logger.info(f"DEBUG: TrackResult converted successfully: {result['name']} by {result['artists'][0]['name']}")
+ logger.debug(
+ "TrackResult converted successfully: name=%s artist=%s",
+ result['name'],
+ result['artists'][0]['name'],
+ )
return result
# Handle regular Spotify Track objects
- logger.info("DEBUG: Processing as Spotify Track object")
+ logger.debug("Processing as Spotify Track object")
# Handle artists list carefully to avoid TrackResult serialization issues
artists_list = []
raw_artists = getattr(spotify_track, 'artists', [])
- logger.info(f"DEBUG: Raw artists: {raw_artists}, type: {type(raw_artists)}")
+ logger.debug("Raw artists: %r (type=%s)", raw_artists, type(raw_artists))
for artist in raw_artists:
- logger.info(f"DEBUG: Processing artist: {artist}, type: {type(artist)}")
+ logger.debug("Processing artist: %r (type=%s)", artist, type(artist))
if hasattr(artist, 'name'):
artists_list.append({'name': artist.name})
elif isinstance(artist, str):
@@ -375,16 +381,20 @@ class WishlistService:
'disc_number': getattr(spotify_track, 'disc_number', 1)
}
- logger.info(f"DEBUG: Spotify Track converted: {result['name']} by {[a['name'] for a in result['artists']]}")
+ logger.debug(
+ "Spotify Track converted: name=%s artists=%s",
+ result['name'],
+ [a['name'] for a in result['artists']],
+ )
# Test JSON serialization before returning to catch any remaining issues
try:
import json
json.dumps(result)
- logger.info("DEBUG: Conversion result is JSON serializable")
+ logger.debug("Conversion result is JSON serializable")
except Exception as json_error:
- logger.error(f"DEBUG: Conversion result is NOT JSON serializable: {json_error}")
- logger.error(f"DEBUG: Result content: {result}")
+ logger.error("Conversion result is NOT JSON serializable: %s", json_error)
+ logger.error("Conversion result content: %r", result)
# Return a safe fallback
return {
'id': f"fallback_{hash(str(spotify_track))}",
@@ -413,4 +423,4 @@ def get_wishlist_service() -> WishlistService:
global _wishlist_service
if _wishlist_service is None:
_wishlist_service = WishlistService()
- return _wishlist_service
\ No newline at end of file
+ return _wishlist_service
diff --git a/database/music_database.py b/database/music_database.py
index 72d4f030..f806ef27 100644
--- a/database/music_database.py
+++ b/database/music_database.py
@@ -26,8 +26,6 @@ try:
except ImportError:
logger.warning("Could not import MusicMatchingEngine, falling back to basic similarity")
_matching_engine = None
-# Temporarily enable debug logging for edition matching
-logger.setLevel(logging.DEBUG)
@dataclass
class DatabaseArtist:
diff --git a/gunicorn.conf.py b/gunicorn.conf.py
index 0500e468..f33ead35 100644
--- a/gunicorn.conf.py
+++ b/gunicorn.conf.py
@@ -11,7 +11,9 @@ timeout = 120
# Keep shutdowns under Docker's stop window so container restarts stay graceful.
graceful_timeout = 8
-# Logging goes to stdout/stderr so Docker can collect it.
+# Logging goes to stdout/stderr and is filtered by the custom logger class.
accesslog = "-"
errorlog = "-"
+access_log_format = '%(h)s - - "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
loglevel = "info"
+logger_class = "utils.gunicorn_logger.FilteredGunicornLogger"
diff --git a/gunicorn.dev.conf.py b/gunicorn.dev.conf.py
index e62ce120..5bf2132e 100644
--- a/gunicorn.dev.conf.py
+++ b/gunicorn.dev.conf.py
@@ -13,7 +13,10 @@ timeout = 120
# Don't let local reloads wait too long for shutdown.
graceful_timeout = 1
-# Logging goes to stdout/stderr so the shell launcher can collect it.
+# Logging goes to stdout/stderr and is filtered by the custom logger class.
accesslog = "-"
errorlog = "-"
+# Mimic process log format
+access_log_format = '%(h)s - - "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
loglevel = "info"
+logger_class = "utils.gunicorn_logger.FilteredGunicornLogger"
diff --git a/scripts/system_info.py b/scripts/system_info.py
index d3229caf..c594e9e6 100644
--- a/scripts/system_info.py
+++ b/scripts/system_info.py
@@ -1,12 +1,18 @@
#!/usr/bin/env python3
"""Reports basic system info ā useful for debugging Docker setups."""
+import logging
import os
import platform
import shutil
-print(f"Platform: {platform.system()} {platform.release()}")
-print(f"Python: {platform.python_version()}")
-print(f"Working Dir: {os.getcwd()}")
+if not logging.getLogger().handlers:
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
+
+logger = logging.getLogger("system_info")
+
+logger.info(f"Platform: {platform.system()} {platform.release()}")
+logger.info(f"Python: {platform.python_version()}")
+logger.info(f"Working Dir: {os.getcwd()}")
# Disk usage for common SoulSync paths
for path in ['/app/downloads', '/app/Transfer', '/app/data', './downloads', './Transfer']:
@@ -14,4 +20,4 @@ for path in ['/app/downloads', '/app/Transfer', '/app/data', './downloads', './T
usage = shutil.disk_usage(path)
free_gb = usage.free / (1024**3)
total_gb = usage.total / (1024**3)
- print(f"Disk {path}: {free_gb:.1f} GB free / {total_gb:.1f} GB total")
+ logger.info(f"Disk {path}: {free_gb:.1f} GB free / {total_gb:.1f} GB total")
diff --git a/tools/diagnose_itunes_discover.py b/tools/diagnose_itunes_discover.py
index f3b0a15b..ff047c26 100644
--- a/tools/diagnose_itunes_discover.py
+++ b/tools/diagnose_itunes_discover.py
@@ -7,14 +7,12 @@ Run this script to identify issues with iTunes data population:
- Discovery pool tracks by source
- Recent albums by source
- Curated playlists status
-
-Usage:
- python tools/diagnose_itunes_discover.py
"""
-import sys
-import os
import json
+import logging
+import os
+import sys
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -22,36 +20,42 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from database.music_database import MusicDatabase
+if not logging.getLogger().handlers:
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
+
+logger = logging.getLogger("diagnose_itunes_discover")
+
+
+def _section(title: str) -> None:
+ logger.info("")
+ logger.info(title)
+ logger.info("-" * 40)
+
+
def diagnose_itunes_discover():
"""Run diagnostic checks for iTunes discover data."""
- print("=" * 60)
- print("iTunes Discover Page Diagnostic Report")
- print("=" * 60)
+ logger.info("=" * 60)
+ logger.info("iTunes Discover Page Diagnostic Report")
+ logger.info("=" * 60)
db = MusicDatabase()
# 1. Check Similar Artists
- print("\n[1] SIMILAR ARTISTS")
- print("-" * 40)
-
+ _section("[1] SIMILAR ARTISTS")
try:
with db._get_connection() as conn:
cursor = conn.cursor()
- # Total similar artists
cursor.execute("SELECT COUNT(*) as total FROM similar_artists")
total = cursor.fetchone()['total']
- # With iTunes IDs
cursor.execute("SELECT COUNT(*) as count FROM similar_artists WHERE similar_artist_itunes_id IS NOT NULL")
with_itunes = cursor.fetchone()['count']
- # With Spotify IDs
cursor.execute("SELECT COUNT(*) as count FROM similar_artists WHERE similar_artist_spotify_id IS NOT NULL")
with_spotify = cursor.fetchone()['count']
- # With both
cursor.execute("""
SELECT COUNT(*) as count FROM similar_artists
WHERE similar_artist_itunes_id IS NOT NULL
@@ -59,34 +63,29 @@ def diagnose_itunes_discover():
""")
with_both = cursor.fetchone()['count']
- print(f" Total similar artists: {total}")
- print(f" With iTunes ID: {with_itunes} ({100*with_itunes/total:.1f}%)" if total > 0 else " With iTunes ID: 0")
- print(f" With Spotify ID: {with_spotify} ({100*with_spotify/total:.1f}%)" if total > 0 else " With Spotify ID: 0")
- print(f" With BOTH IDs: {with_both} ({100*with_both/total:.1f}%)" if total > 0 else " With BOTH IDs: 0")
+ logger.info(f" Total similar artists: {total}")
+ logger.info(f" With iTunes ID: {with_itunes} ({100 * with_itunes / total:.1f}%)" if total > 0 else " With iTunes ID: 0")
+ logger.info(f" With Spotify ID: {with_spotify} ({100 * with_spotify / total:.1f}%)" if total > 0 else " With Spotify ID: 0")
+ logger.info(f" With BOTH IDs: {with_both} ({100 * with_both / total:.1f}%)" if total > 0 else " With BOTH IDs: 0")
if with_itunes == 0 and total > 0:
- print(" [CRITICAL] No similar artists have iTunes IDs - Hero section will be empty!")
+ logger.critical("No similar artists have iTunes IDs - Hero section will be empty!")
elif with_itunes < total * 0.5:
- print(" [WARNING] Less than 50% of similar artists have iTunes IDs")
+ logger.warning("Less than 50% of similar artists have iTunes IDs")
else:
- print(" [OK] iTunes coverage is adequate")
-
+ logger.info("iTunes coverage is adequate")
except Exception as e:
- print(f" [ERROR] Could not check similar artists: {e}")
+ logger.error(f"Could not check similar artists: {e}")
# 2. Check Discovery Pool
- print("\n[2] DISCOVERY POOL")
- print("-" * 40)
-
+ _section("[2] DISCOVERY POOL")
try:
with db._get_connection() as conn:
cursor = conn.cursor()
- # Total tracks
cursor.execute("SELECT COUNT(*) as total FROM discovery_pool")
total = cursor.fetchone()['total']
- # By source
cursor.execute("""
SELECT source, COUNT(*) as count
FROM discovery_pool
@@ -94,33 +93,28 @@ def diagnose_itunes_discover():
""")
source_counts = {row['source']: row['count'] for row in cursor.fetchall()}
- print(f" Total tracks: {total}")
- print(f" Spotify tracks: {source_counts.get('spotify', 0)}")
- print(f" iTunes tracks: {source_counts.get('itunes', 0)}")
+ logger.info(f" Total tracks: {total}")
+ logger.info(f" Spotify tracks: {source_counts.get('spotify', 0)}")
+ logger.info(f" iTunes tracks: {source_counts.get('itunes', 0)}")
if source_counts.get('itunes', 0) == 0 and total > 0:
- print(" [CRITICAL] No iTunes tracks in discovery pool - Fresh Tape/Archives will be empty!")
+ logger.critical("No iTunes tracks in discovery pool - Fresh Tape/Archives will be empty!")
elif source_counts.get('itunes', 0) < total * 0.3:
- print(" [WARNING] Low iTunes track count in discovery pool")
+ logger.warning("Low iTunes track count in discovery pool")
else:
- print(" [OK] iTunes tracks present")
-
+ logger.info("iTunes tracks present")
except Exception as e:
- print(f" [ERROR] Could not check discovery pool: {e}")
+ logger.error(f"Could not check discovery pool: {e}")
# 3. Check Recent Albums
- print("\n[3] RECENT ALBUMS CACHE")
- print("-" * 40)
-
+ _section("[3] RECENT ALBUMS CACHE")
try:
with db._get_connection() as conn:
cursor = conn.cursor()
- # Total albums
cursor.execute("SELECT COUNT(*) as total FROM discovery_recent_albums")
total = cursor.fetchone()['total']
- # By source
cursor.execute("""
SELECT source, COUNT(*) as count
FROM discovery_recent_albums
@@ -128,24 +122,21 @@ def diagnose_itunes_discover():
""")
source_counts = {row['source']: row['count'] for row in cursor.fetchall()}
- print(f" Total recent albums: {total}")
- print(f" Spotify albums: {source_counts.get('spotify', 0)}")
- print(f" iTunes albums: {source_counts.get('itunes', 0)}")
+ logger.info(f" Total recent albums: {total}")
+ logger.info(f" Spotify albums: {source_counts.get('spotify', 0)}")
+ logger.info(f" iTunes albums: {source_counts.get('itunes', 0)}")
if source_counts.get('itunes', 0) == 0 and total > 0:
- print(" [CRITICAL] No iTunes albums cached - Recent Releases section will be empty!")
+ logger.critical("No iTunes albums cached - Recent Releases section will be empty!")
elif source_counts.get('itunes', 0) < 5:
- print(" [WARNING] Very few iTunes albums cached")
+ logger.warning("Very few iTunes albums cached")
else:
- print(" [OK] iTunes albums cached")
-
+ logger.info("iTunes albums cached")
except Exception as e:
- print(f" [ERROR] Could not check recent albums: {e}")
+ logger.error(f"Could not check recent albums: {e}")
# 4. Check Curated Playlists
- print("\n[4] CURATED PLAYLISTS")
- print("-" * 40)
-
+ _section("[4] CURATED PLAYLISTS")
try:
with db._get_connection() as conn:
cursor = conn.cursor()
@@ -156,7 +147,7 @@ def diagnose_itunes_discover():
'release_radar_itunes',
'discovery_weekly',
'discovery_weekly_spotify',
- 'discovery_weekly_itunes'
+ 'discovery_weekly_itunes',
]
for playlist_type in playlists_to_check:
@@ -174,9 +165,8 @@ def diagnose_itunes_discover():
else:
status = "[NOT FOUND]"
- print(f" {playlist_type}: {status}")
+ logger.info(f" {playlist_type}: {status}")
- # Check iTunes-specific playlists
cursor.execute("""
SELECT track_ids_json FROM discovery_curated_playlists
WHERE playlist_type = 'release_radar_itunes'
@@ -190,49 +180,43 @@ def diagnose_itunes_discover():
itunes_dw = cursor.fetchone()
if not itunes_rr or len(json.loads(itunes_rr['track_ids_json'])) == 0:
- print("\n [CRITICAL] release_radar_itunes is empty or missing!")
+ logger.critical("release_radar_itunes is empty or missing!")
if not itunes_dw or len(json.loads(itunes_dw['track_ids_json'])) == 0:
- print(" [CRITICAL] discovery_weekly_itunes is empty or missing!")
-
+ logger.critical("discovery_weekly_itunes is empty or missing!")
except Exception as e:
- print(f" [ERROR] Could not check curated playlists: {e}")
+ logger.error(f"Could not check curated playlists: {e}")
# 5. Check Watchlist Artists
- print("\n[5] WATCHLIST ARTISTS")
- print("-" * 40)
-
+ _section("[5] WATCHLIST ARTISTS")
try:
with db._get_connection() as conn:
cursor = conn.cursor()
- # Total artists
cursor.execute("SELECT COUNT(*) as total FROM watchlist_artists")
total = cursor.fetchone()['total']
- # With iTunes IDs
cursor.execute("SELECT COUNT(*) as count FROM watchlist_artists WHERE itunes_artist_id IS NOT NULL")
with_itunes = cursor.fetchone()['count']
- # With Spotify IDs
cursor.execute("SELECT COUNT(*) as count FROM watchlist_artists WHERE spotify_artist_id IS NOT NULL")
with_spotify = cursor.fetchone()['count']
- print(f" Total watchlist artists: {total}")
- print(f" With iTunes ID: {with_itunes} ({100*with_itunes/total:.1f}%)" if total > 0 else " With iTunes ID: 0")
- print(f" With Spotify ID: {with_spotify} ({100*with_spotify/total:.1f}%)" if total > 0 else " With Spotify ID: 0")
+ logger.info(f" Total watchlist artists: {total}")
+ logger.info(f" With iTunes ID: {with_itunes} ({100 * with_itunes / total:.1f}%)" if total > 0 else " With iTunes ID: 0")
+ logger.info(f" With Spotify ID: {with_spotify} ({100 * with_spotify / total:.1f}%)" if total > 0 else " With Spotify ID: 0")
if with_itunes == 0 and total > 0:
- print(" [WARNING] No watchlist artists have iTunes IDs - source artist data limited")
-
+ logger.warning("No watchlist artists have iTunes IDs - source artist data limited")
except Exception as e:
- print(f" [ERROR] Could not check watchlist artists: {e}")
+ logger.error(f"Could not check watchlist artists: {e}")
- # Summary
- print("\n" + "=" * 60)
- print("SUMMARY & RECOMMENDED ACTIONS")
- print("=" * 60)
- print("""
-If you see [CRITICAL] or [WARNING] messages above, follow these steps:
+ logger.info("")
+ logger.info("=" * 60)
+ logger.info("SUMMARY & RECOMMENDED ACTIONS")
+ logger.info("=" * 60)
+ logger.info(
+ """
+If you see critical or warning messages above, follow these steps:
QUICK FIX - Force Refresh Discover Data:
-----------------------------------------
@@ -263,7 +247,8 @@ ROOT CAUSE NOTES:
The discover page will now fall back to watchlist artists if similar
artists are not available, so basic functionality should still work.
-""")
+""".strip()
+ )
if __name__ == '__main__':
diff --git a/utils/gunicorn_logger.py b/utils/gunicorn_logger.py
new file mode 100644
index 00000000..0dade80b
--- /dev/null
+++ b/utils/gunicorn_logger.py
@@ -0,0 +1,83 @@
+"""Gunicorn logger tweaks for SoulSync."""
+
+from __future__ import annotations
+
+import logging
+
+from gunicorn.glogging import Logger as GunicornLogger
+from utils.logging_config import ColoredFormatter
+
+
+class FilteredGunicornLogger(GunicornLogger):
+ """Gunicorn logger that skips noisy static and Socket.IO access logs."""
+
+ _STATIC_PREFIXES = (
+ "/static/",
+ "/assets/",
+ "/socket.io",
+ "/favicon.ico",
+ "/robots.txt",
+ )
+
+ _STATIC_SUFFIXES = (
+ ".css",
+ ".js",
+ ".map",
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ".gif",
+ ".svg",
+ ".ico",
+ ".woff",
+ ".woff2",
+ ".ttf",
+ ".eot",
+ )
+
+ _HEALTHCHECK_USER_AGENTS = (
+ "curl/",
+ )
+
+ def _should_skip_access_log(self, environ) -> bool:
+ path = environ.get("PATH_INFO") or ""
+ if not path:
+ return False
+
+ normalized = path if path.startswith("/") else f"/{path}"
+ lower_path = normalized.lower()
+
+ if any(
+ lower_path == prefix.rstrip("/") or lower_path.startswith(prefix)
+ for prefix in self._STATIC_PREFIXES
+ ):
+ return True
+
+ if lower_path == "/":
+ user_agent = (environ.get("HTTP_USER_AGENT") or "").lower()
+ if any(token in user_agent for token in self._HEALTHCHECK_USER_AGENTS):
+ return True
+
+ return any(lower_path.endswith(suffix) for suffix in self._STATIC_SUFFIXES)
+
+ def access(self, resp, req, environ, request_time):
+ if self._should_skip_access_log(environ):
+ return
+ super().access(resp, req, environ, request_time)
+
+ def setup(self, cfg):
+ super().setup(cfg)
+
+ app_like_formatter = ColoredFormatter(
+ fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+ error_level = getattr(logging, cfg.loglevel.upper(), logging.INFO)
+
+ for handler in self.access_log.handlers:
+ handler.setFormatter(app_like_formatter)
+ handler.setLevel(logging.INFO)
+
+ for handler in self.error_log.handlers:
+ handler.setFormatter(app_like_formatter)
+ handler.setLevel(error_level)
diff --git a/utils/logging_config.py b/utils/logging_config.py
index b29af7ef..f7f36d0e 100644
--- a/utils/logging_config.py
+++ b/utils/logging_config.py
@@ -6,6 +6,8 @@ from pathlib import Path
from datetime import datetime
from typing import Optional
+LOGGER_NAMESPACE = "soulsync"
+
class SafeFormatter(logging.Formatter):
"""Formatter that handles Unicode characters safely on Windows"""
@@ -52,7 +54,7 @@ class ColoredFormatter(SafeFormatter):
def setup_logging(level: str = "INFO", log_file: Optional[str] = None) -> logging.Logger:
log_level = getattr(logging, level.upper(), logging.INFO)
- logger = logging.getLogger("newmusic")
+ logger = logging.getLogger(LOGGER_NAMESPACE)
logger.setLevel(log_level)
if logger.handlers:
@@ -91,15 +93,15 @@ def setup_logging(level: str = "INFO", log_file: Optional[str] = None) -> loggin
return logger
def get_logger(name: str) -> logging.Logger:
- return logging.getLogger(f"newmusic.{name}")
+ return logging.getLogger(f"{LOGGER_NAMESPACE}.{name}")
def set_log_level(level: str) -> bool:
"""Dynamically change the log level for all loggers without restart"""
try:
log_level = getattr(logging, level.upper(), logging.INFO)
- # Get the root "newmusic" logger
- root_logger = logging.getLogger("newmusic")
+ # Get the root "soulsync" logger
+ root_logger = logging.getLogger(LOGGER_NAMESPACE)
root_logger.setLevel(log_level)
# Update all handlers
@@ -109,12 +111,12 @@ def set_log_level(level: str) -> bool:
root_logger.info(f"Log level changed to: {level.upper()}")
return True
except Exception as e:
- print(f"Error setting log level: {e}")
+ logging.getLogger(LOGGER_NAMESPACE).error(f"Error setting log level: {e}")
return False
def get_current_log_level() -> str:
"""Get the current log level"""
- root_logger = logging.getLogger("newmusic")
+ root_logger = logging.getLogger(LOGGER_NAMESPACE)
return logging.getLevelName(root_logger.level)
-main_logger = get_logger("main")
\ No newline at end of file
+main_logger = get_logger("main")
diff --git a/web_server.py b/web_server.py
index 1e7140b7..bbc600f5 100644
--- a/web_server.py
+++ b/web_server.py
@@ -33,31 +33,31 @@ from config.settings import config_manager
# Setup logging early to avoid any import-time logs from being swallowed
_log_level = config_manager.get('logging.level', 'INFO')
_log_path = config_manager.get('logging.path', 'logs/app.log')
+_log_dir = Path(_log_path).parent
logger = setup_logging(_log_level, _log_path)
# App version ā single source of truth for backup metadata, version-info endpoint, etc.
SOULSYNC_VERSION = "2.35"
-# Dedicated source reuse logger ā writes to logs/source_reuse.log
+# Dedicated source reuse logger ā writes alongside app.log in the configured log directory
import logging as _logging
import logging.handlers as _logging_handlers
source_reuse_logger = _logging.getLogger("source_reuse")
source_reuse_logger.setLevel(_logging.DEBUG)
if not source_reuse_logger.handlers:
- os.makedirs("logs", exist_ok=True)
_sr_handler = _logging_handlers.RotatingFileHandler(
- "logs/source_reuse.log", encoding="utf-8", maxBytes=5*1024*1024, backupCount=2
+ _log_dir / "source_reuse.log", encoding="utf-8", maxBytes=5*1024*1024, backupCount=2
)
_sr_handler.setFormatter(_logging.Formatter("%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
source_reuse_logger.addHandler(_sr_handler)
source_reuse_logger.propagate = False
-# Dedicated post-processing logger (failures only) ā writes to logs/post_processing.log
+# Dedicated post-processing logger (failures only) ā writes alongside app.log in the configured log directory
pp_logger = _logging.getLogger("post_processing")
pp_logger.setLevel(_logging.DEBUG)
if not pp_logger.handlers:
_pp_handler = _logging_handlers.RotatingFileHandler(
- "logs/post_processing.log", encoding="utf-8", maxBytes=5*1024*1024, backupCount=2
+ _log_dir / "post_processing.log", encoding="utf-8", maxBytes=5*1024*1024, backupCount=2
)
_pp_handler.setFormatter(_logging.Formatter("%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
pp_logger.addHandler(_pp_handler)
@@ -81,17 +81,14 @@ from services.sync_service import PlaylistSyncService
# Pre-v1.3 docker-compose files mounted soulsync_database:/app/database, which overlays
# the Python package with stale volume contents. Detect this after import.
if not hasattr(MusicDatabase, 'get_system_automation_by_action'):
- print("=" * 70)
- print("ERROR: Stale database module detected!")
- print(" MusicDatabase is missing required methods. This usually means")
- print(" your docker-compose.yml has an outdated volume mount:")
- print("")
- print(" FIX: Change your docker-compose.yml volume:")
- print(" OLD: soulsync_database:/app/database")
- print(" NEW: soulsync_database:/app/data")
- print("")
- print(" Then run: docker compose down && docker compose up -d")
- print("=" * 70)
+ logger.error(
+ "Stale database module detected!\n"
+ "MusicDatabase is missing required methods. This usually means your docker-compose.yml has an outdated volume mount.\n"
+ "Fix:\n"
+ " OLD: soulsync_database:/app/database\n"
+ " NEW: soulsync_database:/app/data\n"
+ "Then run: docker compose down && docker compose up -d"
+ )
from datetime import datetime, timezone
import yt_dlp
from core.matching_engine import MusicMatchingEngine
@@ -119,7 +116,7 @@ DEV_STATIC_NO_CACHE = os.environ.get('SOULSYNC_WEB_DEV_NO_CACHE', '0').lower() i
env_config_path = os.environ.get('SOULSYNC_CONFIG_PATH')
if env_config_path:
config_path = env_config_path
- print(f"Using config path from environment: {config_path}")
+ logger.info(f"Using config path from environment: {config_path}")
else:
config_path = os.path.join(project_root, 'config', 'config.json')
@@ -133,20 +130,20 @@ if os.path.exists(config_path):
current_loaded_path = current_loaded_path.resolve()
if current_loaded_path == target_path and config_manager.config_data:
- print(f"Web server configuration already loaded from: {config_path}")
+ logger.info(f"Web server configuration already loaded from: {config_path}")
else:
- print(f"Found config file at: {config_path}")
+ logger.info(f"Found config file at: {config_path}")
# Load configuration into the existing singleton instance
if hasattr(config_manager, 'load_config'):
config_manager.load_config(config_path)
else:
# Fallback for older settings.py in Docker volumes
- print("Legacy configuration detected: using fallback loading method")
+ logger.warning("Legacy configuration detected: using fallback loading method")
config_manager.config_path = Path(config_path)
config_manager._load_config()
- print("Web server configuration loaded successfully.")
+ logger.info("Web server configuration loaded successfully.")
else:
- print(f"WARNING: config.json not found at {config_path}. Using default settings.")
+ logger.warning(f"config.json not found at {config_path}. Using default settings.")
# Correctly point to the 'webui' directory for templates and static files
app = Flask(
__name__,
@@ -365,78 +362,78 @@ def _make_context_key(username, filename):
# --- Initialize Core Application Components ---
# Each client is initialized independently so one failure doesn't take down everything.
# Previously, a single exception set ALL clients to None, breaking the entire app.
-print("Initializing SoulSync services for Web UI...")
+logger.info("Initializing SoulSync services for Web UI...")
spotify_client = plex_client = jellyfin_client = navidrome_client = soulsync_library_client = soulseek_client = tidal_client = matching_engine = sync_service = web_scan_manager = None
try:
spotify_client = SpotifyClient()
- print(" Spotify client initialized")
+ logger.info(" Spotify client initialized")
except Exception as e:
- print(f" Spotify client failed to initialize: {e}")
+ logger.error(f" Spotify client failed to initialize: {e}")
try:
plex_client = PlexClient()
- print(" Plex client initialized")
+ logger.info(" Plex client initialized")
except Exception as e:
- print(f" Plex client failed to initialize: {e}")
+ logger.error(f" Plex client failed to initialize: {e}")
try:
jellyfin_client = JellyfinClient()
- print(" Jellyfin client initialized")
+ logger.info(" Jellyfin client initialized")
except Exception as e:
- print(f" Jellyfin client failed to initialize: {e}")
+ logger.error(f" Jellyfin client failed to initialize: {e}")
try:
navidrome_client = NavidromeClient()
- print(" Navidrome client initialized")
+ logger.info(" Navidrome client initialized")
except Exception as e:
- print(f" Navidrome client failed to initialize: {e}")
+ logger.error(f" Navidrome client failed to initialize: {e}")
try:
from core.soulsync_client import SoulSyncClient
soulsync_library_client = SoulSyncClient()
- print(" SoulSync library client initialized")
+ logger.info(" SoulSync library client initialized")
except Exception as e:
- print(f" SoulSync library client failed to initialize: {e}")
+ logger.error(f" SoulSync library client failed to initialize: {e}")
try:
soulseek_client = DownloadOrchestrator()
- print(" Download orchestrator initialized")
+ logger.info(" Download orchestrator initialized")
except Exception as e:
- print(f" Download orchestrator failed to initialize: {e}")
+ logger.error(f" Download orchestrator failed to initialize: {e}")
try:
tidal_client = TidalClient()
- print(" Tidal client initialized")
+ logger.info(" Tidal client initialized")
except Exception as e:
- print(f" Tidal client failed to initialize: {e}")
+ logger.error(f" Tidal client failed to initialize: {e}")
try:
matching_engine = MusicMatchingEngine()
- print(" Matching engine initialized")
+ logger.info(" Matching engine initialized")
except Exception as e:
- print(f" Matching engine failed to initialize: {e}")
+ logger.error(f" Matching engine failed to initialize: {e}")
try:
sync_service = PlaylistSyncService(spotify_client, plex_client, soulseek_client, jellyfin_client, navidrome_client)
- print(" Playlist sync service initialized")
+ logger.info(" Playlist sync service initialized")
except Exception as e:
- print(f" Playlist sync service failed to initialize: {e}")
+ logger.error(f" Playlist sync service failed to initialize: {e}")
# Inject shutdown check callback into YouTube and Tidal clients (avoids circular imports)
if soulseek_client:
if hasattr(soulseek_client, 'youtube'):
soulseek_client.youtube.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
- print(" Configured YouTube client shutdown callback")
+ logger.info(" Configured YouTube client shutdown callback")
if hasattr(soulseek_client, 'tidal'):
soulseek_client.tidal.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
- print(" Configured Tidal download client shutdown callback")
+ logger.info(" Configured Tidal download client shutdown callback")
if hasattr(soulseek_client, 'qobuz'):
soulseek_client.qobuz.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
- print(" Configured Qobuz client shutdown callback")
+ logger.info(" Configured Qobuz client shutdown callback")
if hasattr(soulseek_client, 'hifi'):
soulseek_client.hifi.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
- print(" Configured HiFi client shutdown callback")
+ logger.info(" Configured HiFi client shutdown callback")
# Initialize web scan manager for automatic post-download scanning
try:
@@ -447,18 +444,18 @@ try:
'soulsync_library_client': soulsync_library_client,
}
web_scan_manager = WebScanManager(media_clients, delay_seconds=60)
- print(" Web scan manager initialized")
+ logger.info(" Web scan manager initialized")
except Exception as e:
- print(f" Web scan manager failed to initialize: {e}")
+ logger.error(f" Web scan manager failed to initialize: {e}")
-print("Core service initialization complete.")
+logger.info("Core service initialization complete.")
# --- Automation Engine ---
try:
automation_engine = AutomationEngine(get_database())
- print("Automation engine initialized.")
+ logger.info("Automation engine initialized.")
except Exception as e:
- print(f"Automation engine failed to initialize: {e}")
+ logger.error(f"Automation engine failed to initialize: {e}")
automation_engine = None
def _register_automation_handlers():
@@ -825,7 +822,7 @@ def _register_automation_handlers():
if old_ids != new_ids:
added_count = len(new_ids - old_ids)
removed_count = len(old_ids - new_ids)
- print(f"[AUTOMATION] Playlist changed: '{pl.get('name', '')}' ā {added_count} added, {removed_count} removed (old={len(old_ids)}, new={len(new_ids)})")
+ logger.info(f"[AUTOMATION] Playlist changed: '{pl.get('name', '')}' ā {added_count} added, {removed_count} removed (old={len(old_ids)}, new={len(new_ids)})")
_update_automation_progress(auto_id,
log_line=f'"{pl.get("name", "")}" ā {added_count} added, {removed_count} removed', log_type='success')
try:
@@ -841,7 +838,7 @@ def _register_automation_handlers():
except Exception:
pass
else:
- print(f"[AUTOMATION] No changes: '{pl.get('name', '')}' (tracks={len(old_ids)})")
+ logger.warning(f"[AUTOMATION] No changes: '{pl.get('name', '')}' (tracks={len(old_ids)})")
_update_automation_progress(auto_id,
log_line=f'No changes: "{pl.get("name", "")}"', log_type='skip')
except Exception as e:
@@ -1119,7 +1116,7 @@ def _register_automation_handlers():
# but we pass None so it doesn't conflict with our pipeline progress
_run_playlist_discovery_worker(pls, automation_id=None)
except Exception as e:
- print(f"[Pipeline] Discovery error: {e}")
+ logger.error(f"[Pipeline] Discovery error: {e}")
finally:
disc_done.set()
@@ -2014,7 +2011,7 @@ def _register_automation_handlers():
})
web_scan_manager.add_scan_completion_callback(_on_library_scan_completed)
- print("Automation action handlers registered")
+ logger.info("Automation action handlers registered")
def _emit_track_downloaded(context):
@@ -2350,7 +2347,7 @@ def _record_soulsync_library_entry(context, spotify_artist, album_info):
pass
conn.commit()
- print(f"[SoulSync Library] Added: {artist_name} / {album_name} / {track_name}")
+ logger.info(f"[SoulSync Library] Added: {artist_name} / {album_name} / {track_name}")
except Exception as e:
logger.debug(f"[SoulSync Library] Non-critical error: {e}")
@@ -2372,9 +2369,9 @@ try:
'hydrabase_client': None, # updated after Hydrabase init
'hydrabase_worker': None, # updated after Hydrabase init
}
- print("Public REST API v1 registered at /api/v1")
+ logger.info("Public REST API v1 registered at /api/v1")
except Exception as e:
- print(f"Public REST API v1 failed to register: {e}")
+ logger.error(f"Public REST API v1 failed to register: {e}")
# --- Global Streaming State Management ---
# Thread-safe state tracking for streaming functionality
@@ -2653,14 +2650,14 @@ def get_cached_transfer_data():
'averageSpeed': download.speed,
}
except Exception as e:
- print(f"Could not fetch streaming source downloads: {e}")
+ logger.error(f"Could not fetch streaming source downloads: {e}")
# Update cache
transfer_data_cache['data'] = live_transfers_lookup
transfer_data_cache['last_update'] = current_time
except Exception as e:
- print(f"Could not fetch live transfers (cached): {e}")
+ logger.error(f"Could not fetch live transfers (cached): {e}")
# Return empty dict on error, but don't update cache timestamp
# This way we'll retry on the next request
return {}
@@ -2719,14 +2716,14 @@ def get_cached_beatport_data(section_type, data_key, genre_slug=None):
# Check if cache is still valid
age = current_time - cache_entry['timestamp']
if age < cache_entry['ttl'] and cache_entry['data'] is not None:
- print(f"Cache HIT for {section_type}/{data_key} (age: {age:.1f}s)")
+ logger.debug(f"Cache HIT for {section_type}/{data_key} (age: {age:.1f}s)")
return cache_entry['data']
else:
- print(f"ā° Cache MISS for {section_type}/{data_key} (age: {age:.1f}s, ttl: {cache_entry['ttl']}s)")
+ logger.debug(f"ā° Cache MISS for {section_type}/{data_key} (age: {age:.1f}s, ttl: {cache_entry['ttl']}s)")
return None
except Exception as e:
- print(f"Cache lookup error for {section_type}/{data_key}: {e}")
+ logger.error(f"Cache lookup error for {section_type}/{data_key}: {e}")
return None
def set_cached_beatport_data(section_type, data_key, data, genre_slug=None):
@@ -2747,7 +2744,7 @@ def set_cached_beatport_data(section_type, data_key, data, genre_slug=None):
if data_key in beatport_data_cache['homepage']:
beatport_data_cache['homepage'][data_key]['data'] = data
beatport_data_cache['homepage'][data_key]['timestamp'] = current_time
- print(f"Cached {section_type}/{data_key} (ttl: {beatport_data_cache['homepage'][data_key]['ttl']}s)")
+ logger.info(f"Cached {section_type}/{data_key} (ttl: {beatport_data_cache['homepage'][data_key]['ttl']}s)")
elif section_type == 'genre' and genre_slug:
# Initialize genre cache if not exists
if genre_slug not in beatport_data_cache['genre']:
@@ -2761,10 +2758,10 @@ def set_cached_beatport_data(section_type, data_key, data, genre_slug=None):
beatport_data_cache['genre'][genre_slug][data_key]['data'] = data
beatport_data_cache['genre'][genre_slug][data_key]['timestamp'] = current_time
- print(f"Cached {section_type}/{genre_slug}/{data_key}")
+ logger.info(f"Cached {section_type}/{genre_slug}/{data_key}")
except Exception as e:
- print(f"Cache storage error for {section_type}/{data_key}: {e}")
+ logger.error(f"Cache storage error for {section_type}/{data_key}: {e}")
def add_cache_headers(response, cache_duration=300):
"""
@@ -2855,7 +2852,7 @@ class WebUIDownloadMonitor:
self.monitoring = True
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.monitor_thread.start()
- print(f"Started download monitor for batch {batch_id}")
+ logger.info(f"Started download monitor for batch {batch_id}")
def stop_monitoring(self, batch_id):
"""Stop monitoring a specific batch"""
@@ -2863,7 +2860,7 @@ class WebUIDownloadMonitor:
self.monitored_batches.discard(batch_id)
if not self.monitored_batches:
self.monitoring = False
- print(f"Stopped download monitor (no active batches)")
+ logger.debug(f"Stopped download monitor (no active batches)")
def shutdown(self):
"""Stop the monitor loop and clear active batch tracking."""
@@ -2871,7 +2868,7 @@ class WebUIDownloadMonitor:
self.monitoring = False
self.monitored_batches.clear()
self.monitor_thread = None
- print("Download monitor shutdown requested")
+ logger.info("Download monitor shutdown requested")
def _monitor_loop(self):
"""Main monitoring loop - checks downloads every 1 second for responsive web UX"""
@@ -2885,12 +2882,12 @@ class WebUIDownloadMonitor:
except Exception as e:
# If we get shutdown errors, stop monitoring gracefully
if "interpreter shutdown" in str(e) or "cannot schedule new futures" in str(e):
- print(f"Monitor detected shutdown, stopping gracefully")
+ logger.info(f"Monitor detected shutdown, stopping gracefully")
self.monitoring = False
break
- print(f"Download monitor error: {e}")
+ logger.error(f"Download monitor error: {e}")
- print(f"Download monitor loop ended")
+ logger.info(f"Download monitor loop ended")
def _check_all_downloads(self):
"""Check all active downloads for timeouts and failures"""
@@ -2948,7 +2945,7 @@ class WebUIDownloadMonitor:
transferred = live_info.get('bytesTransferred', 0)
if expected_size > 0 and transferred < expected_size:
if not task.get('_incomplete_warned'):
- print(f"Monitor: {task_id} state={state} but bytes incomplete ({transferred}/{expected_size}) ā waiting")
+ logger.debug(f"Monitor: {task_id} state={state} but bytes incomplete ({transferred}/{expected_size}) ā waiting")
task['_incomplete_warned'] = True
continue
if has_completion and not has_error and task['status'] == 'downloading':
@@ -2960,7 +2957,7 @@ class WebUIDownloadMonitor:
# left tasks stuck in 'downloading' forever.
task['status'] = 'post_processing'
task['status_change_time'] = current_time
- print(f"Monitor detected completed download for {task_id} ({state}) - submitting post-processing")
+ logger.info(f"Monitor detected completed download for {task_id} ({state}) - submitting post-processing")
# Collect for handling outside the lock to prevent deadlock.
# _on_download_completed acquires tasks_lock which is non-reentrant.
completed_tasks.append((batch_id, task_id))
@@ -2974,21 +2971,21 @@ class WebUIDownloadMonitor:
try:
if op[0] == 'cancel_download':
_, download_id, username = op
- print(f"[Deferred] Cancelling download: {download_id} from {username}")
+ logger.debug(f"[Deferred] Cancelling download: {download_id} from {username}")
run_async(soulseek_client.cancel_download(download_id, username, remove=True))
- print(f"[Deferred] Successfully cancelled download {download_id}")
+ logger.debug(f"[Deferred] Successfully cancelled download {download_id}")
elif op[0] == 'cleanup_orphan':
_, context_key = op
with matched_context_lock:
matched_downloads_context.pop(context_key, None)
- print(f"[Deferred] Cleaned up orphaned download context: {context_key}")
+ logger.debug(f"[Deferred] Cleaned up orphaned download context: {context_key}")
elif op[0] == 'restart_worker':
_, task_id, batch_id = op
- print(f"[Deferred] Restarting worker for task {task_id}")
+ logger.debug(f"[Deferred] Restarting worker for task {task_id}")
missing_download_executor.submit(_download_track_worker, task_id, batch_id)
- print(f"[Deferred] Successfully restarted worker for task {task_id}")
+ logger.debug(f"[Deferred] Successfully restarted worker for task {task_id}")
except Exception as e:
- print(f"[Deferred] Error executing deferred operation {op[0]}: {e}")
+ logger.error(f"[Deferred] Error executing deferred operation {op[0]}: {e}")
# Handle completed downloads outside the lock to prevent deadlock
# (_on_download_completed acquires tasks_lock internally)
@@ -2996,19 +2993,19 @@ class WebUIDownloadMonitor:
try:
# Submit post-processing worker (file move, tagging, AcoustID verification)
# This makes batch downloads fully independent of browser polling.
- print(f"[Monitor] Submitting post-processing worker for task {task_id}")
+ logger.info(f"[Monitor] Submitting post-processing worker for task {task_id}")
missing_download_executor.submit(_run_post_processing_worker, task_id, batch_id)
# Chain to next download in the batch queue
_on_download_completed(batch_id, task_id, success=True)
except Exception as e:
- print(f"[Monitor] Error handling completed task {task_id}: {e}")
+ logger.error(f"[Monitor] Error handling completed task {task_id}: {e}")
# Handle exhausted retry tasks outside the lock to prevent deadlock
for batch_id, task_id in exhausted_tasks:
try:
- print(f"[Monitor] Calling completion callback for exhausted task {task_id}")
+ logger.info(f"[Monitor] Calling completion callback for exhausted task {task_id}")
_on_download_completed(batch_id, task_id, success=False)
except Exception as e:
- print(f"[Monitor] Error handling exhausted task {task_id}: {e}")
+ logger.error(f"[Monitor] Error handling exhausted task {task_id}: {e}")
# ENHANCED: Add worker count validation to detect ghost workers
self._validate_worker_counts()
@@ -3067,7 +3064,7 @@ class WebUIDownloadMonitor:
'averageSpeed': download.speed,
}
except Exception as yt_error:
- print(f"Monitor: Could not fetch streaming source downloads: {yt_error}")
+ logger.error(f"Monitor: Could not fetch streaming source downloads: {yt_error}")
return live_transfers
except Exception as e:
@@ -3075,11 +3072,11 @@ class WebUIDownloadMonitor:
if ("interpreter shutdown" in str(e) or
"cannot schedule new futures" in str(e) or
"Event loop is closed" in str(e)):
- print(f"Monitor detected shutdown, stopping immediately")
+ logger.info(f"Monitor detected shutdown, stopping immediately")
self.monitoring = False
return {}
else:
- print(f"Monitor: Could not fetch live transfers: {e}")
+ logger.error(f"Monitor: Could not fetch live transfers: {e}")
return {}
def _should_retry_task(self, task_id, task, live_transfers_lookup, current_time, deferred_ops):
@@ -3109,7 +3106,7 @@ class WebUIDownloadMonitor:
last_retry = task.get('last_retry_time', 0)
if retry_count < 3 and (current_time - last_retry) > 30:
- print(f"Task not in live transfers for >90s - retry {retry_count + 1}/3")
+ logger.warning(f"Task not in live transfers for >90s - retry {retry_count + 1}/3")
task['stuck_retry_count'] = retry_count + 1
task['last_retry_time'] = current_time
@@ -3125,7 +3122,7 @@ class WebUIDownloadMonitor:
source_key = f"{task_username}_{task_filename}"
used_sources.add(source_key)
task['used_sources'] = used_sources
- print(f"Marked missing-transfer source as used: {source_key}")
+ logger.warning(f"Marked missing-transfer source as used: {source_key}")
# Defer orphan cleanup
if task_username and task_filename:
@@ -3140,7 +3137,7 @@ class WebUIDownloadMonitor:
task.pop('queued_start_time', None)
task.pop('downloading_start_time', None)
task['status_change_time'] = current_time
- print(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for missing-transfer retry")
+ logger.warning(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for missing-transfer retry")
batch_id = task.get('batch_id')
if task_id and batch_id:
@@ -3152,7 +3149,7 @@ class WebUIDownloadMonitor:
track_label = task.get('track_info', {}).get('name', 'Unknown')
tried_sources = task.get('used_sources', set())
sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else ''
- print(f"Task failed after 3 retry attempts (not in live transfers)")
+ logger.error(f"Task failed after 3 retry attempts (not in live transfers)")
task['status'] = 'failed'
task['error_message'] = f'Download disappeared from transfer list 3 times for "{track_label}"{sources_str} ā source may be unavailable'
@@ -3172,7 +3169,7 @@ class WebUIDownloadMonitor:
# Don't retry too frequently (wait at least 5 seconds between error retries)
if retry_count < 3 and (current_time - last_retry) > 5: # Max 3 error retry attempts
- print(f"Task errored (state: {state_str}) - immediate retry {retry_count + 1}/3")
+ logger.error(f"Task errored (state: {state_str}) - immediate retry {retry_count + 1}/3")
task['error_retry_count'] = retry_count + 1
task['last_error_retry_time'] = current_time
@@ -3192,7 +3189,7 @@ class WebUIDownloadMonitor:
source_key = f"{username}_{filename}"
used_sources.add(source_key)
task['used_sources'] = used_sources
- print(f"Marked errored source as used: {source_key}")
+ logger.error(f"Marked errored source as used: {source_key}")
# Defer orphan cleanup to outside the lock (needs matched_context_lock)
if username and filename:
@@ -3210,7 +3207,7 @@ class WebUIDownloadMonitor:
task.pop('queued_start_time', None)
task.pop('downloading_start_time', None)
task['status_change_time'] = current_time
- print(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for error retry")
+ logger.error(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for error retry")
# Defer worker restart to outside the lock
batch_id = task.get('batch_id')
@@ -3225,7 +3222,7 @@ class WebUIDownloadMonitor:
track_label = task.get('track_info', {}).get('name', 'Unknown')
tried_sources = task.get('used_sources', set())
sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else ''
- print(f"Task failed after 3 error retry attempts")
+ logger.error(f"Task failed after 3 error retry attempts")
task['status'] = 'failed'
# Tidal-specific error: check if this was a quality issue.
# task['username'] is popped on error-retry (line ~2866) so we can't rely on it;
@@ -3250,7 +3247,7 @@ class WebUIDownloadMonitor:
# CRITICAL: Notify batch manager so track is added to permanently_failed_tracks
batch_id = task.get('batch_id')
if batch_id:
- print(f"[Retry Exhausted] Notifying batch manager of permanent failure for task {task_id}")
+ logger.error(f"[Retry Exhausted] Notifying batch manager of permanent failure for task {task_id}")
return True # Signal that we need to call completion outside the lock
return False
@@ -3275,7 +3272,7 @@ class WebUIDownloadMonitor:
# Don't retry too frequently (wait at least 30 seconds between retries)
if retry_count < 3 and (current_time - last_retry) > 30: # Max 3 retry attempts
- print(f"Task stuck in queue for {queue_time:.1f}s - immediate retry {retry_count + 1}/3")
+ logger.warning(f"Task stuck in queue for {queue_time:.1f}s - immediate retry {retry_count + 1}/3")
task['stuck_retry_count'] = retry_count + 1
task['last_retry_time'] = current_time
@@ -3296,7 +3293,7 @@ class WebUIDownloadMonitor:
source_key = f"{username}_{filename}"
used_sources.add(source_key)
task['used_sources'] = used_sources
- print(f"Marked timeout source as used: {source_key}")
+ logger.error(f"Marked timeout source as used: {source_key}")
# Defer orphan cleanup to outside the lock (needs matched_context_lock)
if username and filename:
@@ -3314,7 +3311,7 @@ class WebUIDownloadMonitor:
task.pop('queued_start_time', None)
task.pop('downloading_start_time', None)
task['status_change_time'] = current_time
- print(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for timeout retry")
+ logger.error(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for timeout retry")
# Defer worker restart to outside the lock
batch_id = task.get('batch_id')
@@ -3329,7 +3326,7 @@ class WebUIDownloadMonitor:
track_label = task.get('track_info', {}).get('name', 'Unknown')
tried_sources = task.get('used_sources', set())
sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else ''
- print(f"Task failed after 3 retry attempts (queue timeout)")
+ logger.error(f"Task failed after 3 retry attempts (queue timeout)")
task['status'] = 'failed'
task['error_message'] = f'Download stayed queued too long 3 times for "{track_label}"{sources_str} ā peers may be offline or have full queues'
# Clear timers to prevent further retry loops
@@ -3339,7 +3336,7 @@ class WebUIDownloadMonitor:
# CRITICAL: Notify batch manager so track is added to permanently_failed_tracks
batch_id = task.get('batch_id')
if batch_id:
- print(f"[Retry Exhausted] Notifying batch manager of permanent failure for task {task_id}")
+ logger.error(f"[Retry Exhausted] Notifying batch manager of permanent failure for task {task_id}")
return True # Signal that we need to call completion outside the lock
return False
@@ -3363,7 +3360,7 @@ class WebUIDownloadMonitor:
# Don't retry too frequently (wait at least 30 seconds between retries)
if retry_count < 3 and (current_time - last_retry) > 30: # Max 3 retry attempts
- print(f"Task stuck at 0% for {download_time:.1f}s - immediate retry {retry_count + 1}/3")
+ logger.warning(f"Task stuck at 0% for {download_time:.1f}s - immediate retry {retry_count + 1}/3")
task['stuck_retry_count'] = retry_count + 1
task['last_retry_time'] = current_time
@@ -3384,7 +3381,7 @@ class WebUIDownloadMonitor:
source_key = f"{username}_{filename}"
used_sources.add(source_key)
task['used_sources'] = used_sources
- print(f"Marked 0% progress source as used: {source_key}")
+ logger.info(f"Marked 0% progress source as used: {source_key}")
# Defer orphan cleanup to outside the lock (needs matched_context_lock)
if username and filename:
@@ -3402,7 +3399,7 @@ class WebUIDownloadMonitor:
task.pop('queued_start_time', None)
task.pop('downloading_start_time', None)
task['status_change_time'] = current_time
- print(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for 0% retry")
+ logger.warning(f"Task {task.get('track_info', {}).get('name', 'Unknown')} reset for 0% retry")
# Defer worker restart to outside the lock
batch_id = task.get('batch_id')
@@ -3416,7 +3413,7 @@ class WebUIDownloadMonitor:
track_label = task.get('track_info', {}).get('name', 'Unknown')
tried_sources = task.get('used_sources', set())
sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else ''
- print(f"Task failed after 3 retry attempts (0% progress timeout)")
+ logger.error(f"Task failed after 3 retry attempts (0% progress timeout)")
task['status'] = 'failed'
task['error_message'] = f'Download stuck at 0% three times for "{track_label}"{sources_str} ā peers may have connection issues'
# Clear timers to prevent further retry loops
@@ -3426,7 +3423,7 @@ class WebUIDownloadMonitor:
# CRITICAL: Notify batch manager so track is added to permanently_failed_tracks
batch_id = task.get('batch_id')
if batch_id:
- print(f"[Retry Exhausted] Notifying batch manager of permanent failure for task {task_id}")
+ logger.error(f"[Retry Exhausted] Notifying batch manager of permanent failure for task {task_id}")
return True # Signal that we need to call completion outside the lock
return False
else:
@@ -3453,7 +3450,7 @@ class WebUIDownloadMonitor:
last_retry = task.get('last_retry_time', 0)
if retry_count < 3 and (current_time - last_retry) > 30:
- print(f"Task stuck in unknown state '{state_str}' with 0 progress for {download_time:.1f}s - retry {retry_count + 1}/3")
+ logger.warning(f"Task stuck in unknown state '{state_str}' with 0 progress for {download_time:.1f}s - retry {retry_count + 1}/3")
task['stuck_retry_count'] = retry_count + 1
task['last_retry_time'] = current_time
@@ -3470,7 +3467,7 @@ class WebUIDownloadMonitor:
source_key = f"{username}_{filename}"
used_sources.add(source_key)
task['used_sources'] = used_sources
- print(f"Marked unknown-state source as used: {source_key}")
+ logger.info(f"Marked unknown-state source as used: {source_key}")
if username and filename:
old_context_key = _make_context_key(username, filename)
@@ -3493,7 +3490,7 @@ class WebUIDownloadMonitor:
track_label = task.get('track_info', {}).get('name', 'Unknown')
tried_sources = task.get('used_sources', set())
sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else ''
- print(f"Task failed after 3 retry attempts (unknown state '{state_str}')")
+ logger.error(f"Task failed after 3 retry attempts (unknown state '{state_str}')")
task['status'] = 'failed'
task['error_message'] = f'Download stuck in "{state_str}" state 3 times for "{track_label}"{sources_str}'
task.pop('queued_start_time', None)
@@ -3547,16 +3544,16 @@ class WebUIDownloadMonitor:
# Check for discrepancies
if reported_active != actually_active or orphaned_tasks:
- print(f"[Worker Validation] Batch {batch_id}: reported={reported_active}, actual={actually_active}, orphaned={len(orphaned_tasks)}")
+ logger.warning(f"[Worker Validation] Batch {batch_id}: reported={reported_active}, actual={actually_active}, orphaned={len(orphaned_tasks)}")
if orphaned_tasks:
- print(f"[Worker Validation] Found {len(orphaned_tasks)} orphaned tasks to cleanup")
+ logger.warning(f"[Worker Validation] Found {len(orphaned_tasks)} orphaned tasks to cleanup")
# Fix the active count if it's wrong
if reported_active != actually_active:
old_count = batch['active_count']
batch['active_count'] = actually_active
- print(f"[Worker Validation] Fixed active count: {old_count} ā {actually_active}")
+ logger.info(f"[Worker Validation] Fixed active count: {old_count} ā {actually_active}")
# Defer starting workers to outside the lock
if actually_active < max_concurrent and queue_index < len(queue):
@@ -3565,13 +3562,13 @@ class WebUIDownloadMonitor:
# Start replacement workers outside the lock
for batch_id in batches_needing_workers:
try:
- print(f"[Worker Validation] Starting replacement workers for {batch_id}")
+ logger.info(f"[Worker Validation] Starting replacement workers for {batch_id}")
_start_next_batch_of_downloads(batch_id)
except Exception as e:
- print(f"[Worker Validation] Error starting workers for {batch_id}: {e}")
+ logger.error(f"[Worker Validation] Error starting workers for {batch_id}: {e}")
except Exception as validation_error:
- print(f"Error in worker count validation: {validation_error}")
+ logger.error(f"Error in worker count validation: {validation_error}")
# Global download monitor instance
download_monitor = WebUIDownloadMonitor()
@@ -3611,7 +3608,7 @@ def validate_and_heal_batch_states():
# Check if batch has been complete for >5 minutes
time_since_completion = current_time - completion_time
if time_since_completion > 300: # 5 minutes
- print(f"[Auto-Cleanup] Removing stale completed batch {batch_id} (completed {time_since_completion:.0f}s ago)")
+ logger.warning(f"[Auto-Cleanup] Removing stale completed batch {batch_id} (completed {time_since_completion:.0f}s ago)")
batches_to_cleanup.append(batch_id)
continue # Skip other healing logic for this batch
@@ -3635,7 +3632,7 @@ def validate_and_heal_batch_states():
# Check for inconsistencies
if active_count != actually_active:
- print(f"[Batch Healing] {batch_id}: fixing active count {active_count} ā {actually_active}")
+ logger.info(f"[Batch Healing] {batch_id}: fixing active count {active_count} ā {actually_active}")
batch_data['active_count'] = actually_active
healed_batches.append(batch_id)
@@ -3647,7 +3644,7 @@ def validate_and_heal_batch_states():
# Clean up orphaned tasks that are blocking progress
if orphaned_tasks and phase == 'downloading':
- print(f"[Batch Healing] Found {len(orphaned_tasks)} orphaned tasks in active batch {batch_id}")
+ logger.warning(f"[Batch Healing] Found {len(orphaned_tasks)} orphaned tasks in active batch {batch_id}")
batches_needing_completion_check.append(batch_id)
# Cleanup stale batches inside the lock (safe - just dict mutations)
@@ -3660,31 +3657,31 @@ def validate_and_heal_batch_states():
del download_tasks[task_id]
if batches_to_cleanup:
- print(f"[Auto-Cleanup] Removed {len(batches_to_cleanup)} stale completed batches")
+ logger.warning(f"[Auto-Cleanup] Removed {len(batches_to_cleanup)} stale completed batches")
if healed_batches:
- print(f"[Batch Healing] Healed {len(healed_batches)} batches: {healed_batches}")
+ logger.info(f"[Batch Healing] Healed {len(healed_batches)} batches: {healed_batches}")
# ---- All work below runs WITHOUT tasks_lock held ----
# Start replacement workers for healed batches
for batch_id in batches_needing_workers:
try:
- print(f"[Batch Healing] Starting replacement workers for {batch_id}")
+ logger.info(f"[Batch Healing] Starting replacement workers for {batch_id}")
_start_next_batch_of_downloads(batch_id)
except Exception as e:
- print(f"[Batch Healing] Error starting workers for {batch_id}: {e}")
+ logger.error(f"[Batch Healing] Error starting workers for {batch_id}: {e}")
# Trigger completion checks for batches with orphaned tasks
for batch_id in batches_needing_completion_check:
try:
- print(f"[Batch Healing] Triggering completion check for batch with orphaned tasks")
+ logger.warning(f"[Batch Healing] Triggering completion check for batch with orphaned tasks")
_check_batch_completion_v2(batch_id)
except Exception as e:
- print(f"[Batch Healing] Error checking completion for {batch_id}: {e}")
+ logger.error(f"[Batch Healing] Error checking completion for {batch_id}: {e}")
except Exception as healing_error:
- print(f"[Batch Healing] Error during validation: {healing_error}")
+ logger.error(f"[Batch Healing] Error during validation: {healing_error}")
# Start periodic batch healing (every 30 seconds)
import threading
@@ -3719,7 +3716,7 @@ def start_batch_healing_timer():
return
validate_and_heal_batch_states()
except Exception as e:
- print(f"[Batch Healing Timer] Error: {e}")
+ logger.error(f"[Batch Healing Timer] {e}")
finally:
# Schedule next healing cycle
_schedule_batch_healing_timer(30.0)
@@ -3735,7 +3732,7 @@ import sys
def cleanup_monitor():
"""Clean up background monitor on shutdown"""
if download_monitor.monitoring:
- print("Flask shutdown detected, stopping download monitor...")
+ logger.info("Flask shutdown detected, stopping download monitor...")
download_monitor.shutdown()
# Give the thread a moment to exit cleanly
time.sleep(0.5)
@@ -3746,13 +3743,13 @@ def cleanup_monitor():
if acquired:
try:
batch_locks.clear()
- print("Cleaned up batch locks")
+ logger.info("Cleaned up batch locks")
finally:
tasks_lock.release()
else:
- print("Skipped batch lock cleanup - tasks_lock busy")
+ logger.warning("Skipped batch lock cleanup - tasks_lock busy")
except Exception as e:
- print(f"Error cleaning up batch locks: {e}")
+ logger.error(f"Error cleaning up batch locks: {e}")
# Global shutdown flag
IS_SHUTTING_DOWN = False
@@ -3762,10 +3759,10 @@ def _shutdown_executor(executor, name):
if executor is None:
return
try:
- print(f"Shutting down {name}...")
+ logger.info(f"Shutting down {name}...")
executor.shutdown(wait=False, cancel_futures=True)
except Exception as e:
- print(f"Error shutting down {name}: {e}")
+ logger.error(f"Error shutting down {name}: {e}")
def _stop_component(component, name, method_names=("stop", "shutdown")):
"""Call a best-effort stop method on a component if it has one."""
@@ -3775,10 +3772,10 @@ def _stop_component(component, name, method_names=("stop", "shutdown")):
method = getattr(component, method_name, None)
if callable(method):
try:
- print(f"Stopping {name}...")
+ logger.info(f"Stopping {name}...")
method()
except Exception as e:
- print(f"Error stopping {name}: {e}")
+ logger.error(f"Error stopping {name}: {e}")
return
def _stop_components_parallel(components):
@@ -3818,9 +3815,9 @@ def _shutdown_runtime_components():
try:
from core.api_call_tracker import api_call_tracker
api_call_tracker.save()
- print("API call history saved")
+ logger.info("API call history saved")
except Exception as e:
- print(f"Error saving API call history: {e}")
+ logger.error(f"Error saving API call history: {e}")
# Stop the active DB update worker before tearing down the executor it runs on.
# This lets an in-flight update observe should_stop and exit cleanly.
@@ -3871,7 +3868,7 @@ def _shutdown_runtime_components():
def signal_handler(signum, frame):
"""Handle SIGINT (Ctrl+C) and SIGTERM"""
- print(f"Signal {signum} received, cleaning up...")
+ logger.info(f"Signal {signum} received, cleaning up...")
_shutdown_runtime_components()
sys.exit(0)
@@ -3907,20 +3904,20 @@ def _handle_failed_download(batch_id, task_id, task, task_status):
if task['retry_count'] > 2: # Max 3 attempts total (matches GUI)
# All retries exhausted, mark as permanently failed
- print(f"Task {task_id} failed after 3 retry attempts")
+ logger.error(f"Task {task_id} failed after 3 retry attempts")
task_status['status'] = 'failed'
task['status'] = 'failed'
return
# Show retrying status while we process retry
task_status['status'] = 'pending' # Will show as pending until retry kicks in
- print(f"Triggering retry {task['retry_count']}/3 for failed task {task_id}")
+ logger.error(f"Triggering retry {task['retry_count']}/3 for failed task {task_id}")
# Trigger retry with next candidate (matches GUI retry_parallel_download_with_fallback)
missing_download_executor.submit(download_monitor._retry_task_with_fallback, batch_id, task_id, task)
except Exception as e:
- print(f"Error handling failed download {task_id}: {e}")
+ logger.error(f"Error handling failed download {task_id}: {e}")
task_status['status'] = 'failed'
task['status'] = 'failed'
@@ -3989,7 +3986,7 @@ def _prepare_stream_task(track_data):
last_progress_sent = 0.0
try:
- print(f"Starting stream preparation for: {track_data.get('filename')}")
+ logger.info(f"Starting stream preparation for: {track_data.get('filename')}")
# Update state to loading
with stream_lock:
@@ -4016,9 +4013,9 @@ def _prepare_stream_task(track_data):
os.remove(existing_file)
elif os.path.isdir(existing_file):
shutil.rmtree(existing_file)
- print(f"Cleared old stream file: {existing_file}")
+ logger.info(f"Cleared old stream file: {existing_file}")
except Exception as e:
- print(f"Could not remove existing stream file: {e}")
+ logger.error(f"Could not remove existing stream file: {e}")
# Start the download using the same mechanism as regular downloads
loop = asyncio.new_event_loop()
@@ -4039,7 +4036,7 @@ def _prepare_stream_task(track_data):
})
return
- print(f"Download initiated for streaming")
+ logger.info(f"Download initiated for streaming")
# Enhanced monitoring with queue timeout detection (matching GUI)
max_wait_time = 60 # Increased timeout
@@ -4065,7 +4062,7 @@ def _prepare_stream_task(track_data):
download_state = download_status.get('state', '').lower()
original_state = download_status.get('state', '')
- print(f"API Download - State: {original_state}, Progress: {api_progress:.1f}%")
+ logger.info(f"API Download - State: {original_state}, Progress: {api_progress:.1f}%")
# Track queue state timing (matching GUI logic)
is_queued = ('queued' in download_state or 'initializing' in download_state)
@@ -4079,13 +4076,13 @@ def _prepare_stream_task(track_data):
# Handle queue state timing
if is_queued and queue_start_time is None:
queue_start_time = time.time()
- print(f"Download entered queue state: {original_state}")
+ logger.info(f"Download entered queue state: {original_state}")
with stream_lock:
stream_state["status"] = "queued"
elif is_downloading and not actively_downloading:
actively_downloading = True
queue_start_time = None # Reset queue timer
- print(f"Download started actively downloading: {original_state}")
+ logger.info(f"Download started actively downloading: {original_state}")
with stream_lock:
stream_state["status"] = "loading"
@@ -4093,7 +4090,7 @@ def _prepare_stream_task(track_data):
if is_queued and queue_start_time:
queue_elapsed = time.time() - queue_start_time
if queue_elapsed > queue_timeout:
- print(f"ā° Queue timeout after {queue_elapsed:.1f}s - download stuck in queue")
+ logger.error(f"ā° Queue timeout after {queue_elapsed:.1f}s - download stuck in queue")
with stream_lock:
stream_state.update({
"status": "error",
@@ -4109,7 +4106,7 @@ def _prepare_stream_task(track_data):
# Check if download is complete
if is_completed:
- print(f"Download completed via API status: {original_state}")
+ logger.info(f"Download completed via API status: {original_state}")
# Wait for file to stabilise on disk before moving
found_file = _find_downloaded_file(download_path, track_data)
@@ -4134,12 +4131,12 @@ def _prepare_stream_task(track_data):
for attempt in range(retry_attempts):
if found_file:
break
- print(f"File not found yet, attempt {attempt + 1}/{retry_attempts}")
+ logger.warning(f"File not found yet, attempt {attempt + 1}/{retry_attempts}")
time.sleep(1)
found_file = _find_downloaded_file(download_path, track_data)
if found_file:
- print(f"Found downloaded file: {found_file}")
+ logger.debug(f"Found downloaded file: {found_file}")
# Move file to Stream folder
original_filename = extract_filename(found_file)
@@ -4147,7 +4144,7 @@ def _prepare_stream_task(track_data):
try:
shutil.move(found_file, stream_path)
- print(f"Moved file to stream folder: {stream_path}")
+ logger.debug(f"Moved file to stream folder: {stream_path}")
# Clean up empty directories (matching GUI)
_cleanup_empty_directories(download_path, found_file)
@@ -4169,15 +4166,15 @@ def _prepare_stream_task(track_data):
download_id, track_data.get('username'), remove=True)
)
if success:
- print(f"Cleaned up download {download_id} from API")
+ logger.debug(f"Cleaned up download {download_id} from API")
except Exception as e:
- print(f"Error cleaning up download: {e}")
+ logger.error(f"Error cleaning up download: {e}")
- print(f"Stream file ready for playback: {stream_path}")
+ logger.info(f"Stream file ready for playback: {stream_path}")
return # Success!
except Exception as e:
- print(f"Error moving file to stream folder: {e}")
+ logger.error(f"Error moving file to stream folder: {e}")
with stream_lock:
stream_state.update({
"status": "error",
@@ -4185,7 +4182,7 @@ def _prepare_stream_task(track_data):
})
return
else:
- print("Could not find downloaded file after completion")
+ logger.error("Could not find downloaded file after completion")
with stream_lock:
stream_state.update({
"status": "error",
@@ -4194,17 +4191,17 @@ def _prepare_stream_task(track_data):
return
else:
# No transfer found in API - may still be initializing
- print(f"No transfer found in API yet... (elapsed: {wait_count * poll_interval}s)")
+ logger.debug(f"No transfer found in API yet... (elapsed: {wait_count * poll_interval}s)")
except Exception as e:
- print(f"Error checking download progress: {e}")
+ logger.error(f"Error checking download progress: {e}")
# Continue to next iteration if API call fails
# Wait before next poll
time.sleep(poll_interval)
# If we get here, download timed out
- print(f"Download timed out after {max_wait_time}s")
+ logger.warning(f"Download timed out after {max_wait_time}s")
with stream_lock:
stream_state.update({
"status": "error",
@@ -4212,7 +4209,7 @@ def _prepare_stream_task(track_data):
})
except asyncio.CancelledError:
- print("Stream task cancelled")
+ logger.warning("Stream task cancelled")
with stream_lock:
stream_state.update({
"status": "stopped",
@@ -4227,10 +4224,10 @@ def _prepare_stream_task(track_data):
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
loop.close()
except Exception as e:
- print(f"Error cleaning up streaming event loop: {e}")
+ logger.error(f"Error cleaning up streaming event loop: {e}")
except Exception as e:
- print(f"Stream preparation failed: {e}")
+ logger.error(f"Stream preparation failed: {e}")
with stream_lock:
stream_state.update({
"status": "error",
@@ -4267,7 +4264,7 @@ def _find_streaming_download_in_all_downloads(all_downloads, track_data):
return None
except Exception as e:
- print(f"Error finding streaming download: {e}")
+ logger.error(f"Error finding streaming download: {e}")
return None
def _find_downloaded_file(download_path, track_data):
@@ -4293,15 +4290,15 @@ def _find_downloaded_file(download_path, track_data):
safe_title = re.sub(r'[<>:"/\\|?*]', '_', title)
target_filename_youtube = safe_title # Extension-less for flexible matching
source_name = 'HiFi' if is_hifi else ('Qobuz' if is_qobuz else 'Tidal')
- print(f"[{source_name} Stream] Looking for file starting with: {target_filename_youtube}")
+ logger.debug(f"[{source_name} Stream] Looking for file starting with: {target_filename_youtube}")
else:
# yt-dlp will create "Title.mp3" from "Title"
target_filename_youtube = f"{title}.mp3"
- print(f"[YouTube Stream] Looking for file: {target_filename_youtube}")
+ logger.debug(f"[YouTube Stream] Looking for file: {target_filename_youtube}")
elif is_streaming_source:
# Fallback: if streaming source but no encoded format, use as-is
target_filename_youtube = target_filename
- print(f"[Stream] Using direct filename: {target_filename_youtube}")
+ logger.debug(f"[Stream] Using direct filename: {target_filename_youtube}")
try:
# Walk through the downloads directory to find the file
@@ -4335,7 +4332,7 @@ def _find_downloaded_file(download_path, track_data):
similarity = SequenceMatcher(None, compare_file, compare_target).ratio()
source_label = 'HiFi' if is_hifi else ('Qobuz' if is_qobuz else ('Tidal' if is_tidal else 'YouTube'))
- print(f"[{source_label} Stream] Comparing: '{file}' vs '{target_filename_youtube}' = {similarity:.2f}")
+ logger.debug(f"[{source_label} Stream] Comparing: '{file}' vs '{target_filename_youtube}' = {similarity:.2f}")
# Keep track of best match
if similarity > best_similarity:
@@ -4344,28 +4341,28 @@ def _find_downloaded_file(download_path, track_data):
# If we have a very good match (95%+), use it immediately
if similarity >= 0.95:
- print(f"Found excellent match for streaming file: {file_path}")
+ logger.debug(f"Found excellent match for streaming file: {file_path}")
return file_path
else:
# For Soulseek, exact match
if file == target_filename:
- print(f"Found streaming file: {file_path}")
+ logger.debug(f"Found streaming file: {file_path}")
return file_path
# For YouTube/Tidal, if we found a good enough match (80%+), use it
if is_streaming_source and best_match and best_similarity >= 0.80:
source_label = 'Qobuz' if is_qobuz else ('Tidal' if is_tidal else 'YouTube')
- print(f"Found good match ({best_similarity:.2f}) for {source_label} streaming file: {best_match}")
+ logger.debug(f"Found good match ({best_similarity:.2f}) for {source_label} streaming file: {best_match}")
return best_match
- print(f"Could not find downloaded file: {target_filename}")
+ logger.error(f"Could not find downloaded file: {target_filename}")
if is_streaming_source:
- print(f" Looking for: {target_filename_youtube}")
- print(f" Best similarity: {best_similarity:.2f}")
+ logger.debug(f" Looking for: {target_filename_youtube}")
+ logger.debug(f" Best similarity: {best_similarity:.2f}")
return None
except Exception as e:
- print(f"Error searching for downloaded file: {e}")
+ logger.error(f"Error searching for downloaded file: {e}")
return None
# --- Refactored Logic from GUI Threads ---
@@ -4656,7 +4653,7 @@ def run_service_test(service, test_config):
if original_config:
for key, value in original_config.items():
config_manager.set(f"{service}.{key}", value)
- print(f"Restored original config for '{service}' after test.")
+ logger.debug(f"Restored original config for '{service}' after test.")
def run_detection(server_type):
@@ -4664,7 +4661,7 @@ def run_detection(server_type):
Performs comprehensive network detection for a given server type (plex, jellyfin, slskd).
This implements the same scanning logic as the GUI's detection threads.
"""
- print(f"Running comprehensive detection for {server_type}...")
+ logger.info(f"Running comprehensive detection for {server_type}...")
def get_network_info():
"""Get comprehensive network information with subnet detection"""
@@ -4813,36 +4810,36 @@ def run_detection(server_type):
return None
# Priority 1: Test localhost first
- print(f"Testing localhost for {server_type}...")
+ logger.debug(f"Testing localhost for {server_type}...")
localhost_result = test_func("localhost")
if localhost_result:
- print(f"Found {server_type} at localhost!")
+ logger.info(f"Found {server_type} at localhost!")
return localhost_result
# Priority 1.5: In Docker, try Docker host IP
import os
if os.path.exists('/.dockerenv'):
- print(f"Docker detected, testing Docker host for {server_type}...")
+ logger.info(f"Docker detected, testing Docker host for {server_type}...")
try:
# Try host.docker.internal (Windows/Mac)
host_result = test_func("host.docker.internal")
if host_result:
- print(f"Found {server_type} at Docker host!")
+ logger.info(f"Found {server_type} at Docker host!")
return host_result.replace("host.docker.internal", "localhost") # Convert back to localhost for config
# Try Docker bridge gateway (Linux)
gateway_result = test_func("172.17.0.1")
if gateway_result:
- print(f"Found {server_type} at Docker gateway!")
+ logger.info(f"Found {server_type} at Docker gateway!")
return gateway_result.replace("172.17.0.1", "localhost") # Convert back to localhost for config
except Exception as e:
- print(f"Docker host detection failed: {e}")
+ logger.error(f"Docker host detection failed: {e}")
# Priority 2: Test local IP
- print(f"Testing local IP {local_ip} for {server_type}...")
+ logger.debug(f"Testing local IP {local_ip} for {server_type}...")
local_result = test_func(local_ip)
if local_result:
- print(f"Found {server_type} at {local_ip}!")
+ logger.info(f"Found {server_type} at {local_ip}!")
return local_result
# Priority 3: Test common IPs (router gateway, etc.)
@@ -4852,12 +4849,12 @@ def run_detection(server_type):
local_ip.rsplit('.', 1)[0] + '.100', # Common static IP
]
- print(f"Testing common IPs for {server_type}...")
+ logger.debug(f"Testing common IPs for {server_type}...")
for ip in common_ips:
- print(f" Checking {ip}...")
+ logger.info(f" Checking {ip}...")
result = test_func(ip)
if result:
- print(f"Found {server_type} at {ip}!")
+ logger.info(f"Found {server_type} at {ip}!")
return result
# Priority 4: Scan the network range (limited to reasonable size)
@@ -4867,7 +4864,7 @@ def run_detection(server_type):
step = max(1, len(network_hosts) // 50)
network_hosts = network_hosts[::step]
- print(f"Scanning network range for {server_type} ({len(network_hosts)} hosts)...")
+ logger.debug(f"Scanning network range for {server_type} ({len(network_hosts)} hosts)...")
# Use ThreadPoolExecutor for concurrent scanning (limited for web context)
with ThreadPoolExecutor(max_workers=5) as executor:
@@ -4881,23 +4878,23 @@ def run_detection(server_type):
try:
result = future.result()
if result:
- print(f"Found {server_type} at {ip}!")
+ logger.info(f"Found {server_type} at {ip}!")
# Cancel all pending futures before returning
for f in future_to_ip:
if not f.done():
f.cancel()
return result
except Exception as e:
- print(f"Error testing {ip}: {e}")
+ logger.error(f"Error testing {ip}: {e}")
continue
except Exception as e:
- print(f"Error in concurrent scanning: {e}")
+ logger.error(f"Error in concurrent scanning: {e}")
- print(f"No {server_type} server found on network")
+ logger.warning(f"No {server_type} server found on network")
return None
except Exception as e:
- print(f"Error during {server_type} detection: {e}")
+ logger.error(f"Error during {server_type} detection: {e}")
return None
# --- Web UI Routes ---
@@ -5309,9 +5306,9 @@ def _regenerate_batch_m3u(batch, tracks):
m3u_path = os.path.join(m3u_folder, f'{safe_fn}.m3u')
with open(m3u_path, 'w', encoding='utf-8') as f:
f.write(m3u_content)
- print(f"[M3U] Regenerated M3U on batch complete: {m3u_path} ({found}/{len(tracks)} resolved)")
+ logger.info(f"[M3U] Regenerated M3U on batch complete: {m3u_path} ({found}/{len(tracks)} resolved)")
except Exception as e:
- print(f"[M3U] Error in _regenerate_batch_m3u: {e}")
+ logger.error(f"[M3U] Error in _regenerate_batch_m3u: {e}")
@app.route('/api/save-playlist-m3u', methods=['POST'])
@@ -5573,7 +5570,7 @@ def _build_system_stats():
if isinstance(speed, (int, float)) and speed > 0:
total_download_speed += float(speed)
except Exception as e:
- print(f"Warning: Could not fetch download speeds: {e}")
+ logger.error(f"Could not fetch download speeds: {e}")
# Convert bytes/sec to KB/s and format
if total_download_speed > 0:
@@ -5856,10 +5853,10 @@ def get_debug_info():
# Log lines
log_map = {
- 'app': os.path.join('logs', 'app.log'),
- 'acoustid': os.path.join('logs', 'acoustid.log'),
- 'post_processing': os.path.join('logs', 'post_processing.log'),
- 'source_reuse': os.path.join('logs', 'source_reuse.log'),
+ 'app': Path(_log_path),
+ 'acoustid': _log_dir / 'acoustid.log',
+ 'post_processing': _log_dir / 'post_processing.log',
+ 'source_reuse': _log_dir / 'source_reuse.log',
}
log_path = log_map.get(log_source, log_map['app'])
info['log_source'] = log_source
@@ -5989,9 +5986,9 @@ def add_activity_item(icon: str, title: str, subtitle: str, time_ago: str = "Now
except Exception:
pass
- print(f"Activity: {icon} {title} - {subtitle}")
+ logger.info(f"Activity: {icon} {title} - {subtitle}")
except Exception as e:
- print(f"Error adding activity item: {e}")
+ logger.error(f"Error adding activity item: {e}")
# --- Internal API Key Management (browser-only, no auth) ---
@app.route('/api/v1/api-keys-internal', methods=['GET'])
@@ -6059,7 +6056,7 @@ def handle_settings():
for key, value in new_settings[service].items():
config_manager.set(f'{service}.{key}', value)
- print("Settings saved successfully via Web UI.")
+ logger.info("Settings saved successfully via Web UI.")
# Add activity for settings save
changed_services = list(new_settings.keys())
@@ -6097,7 +6094,7 @@ def handle_settings():
tidal_enrichment_worker.client = tidal_client
# Invalidate status cache so next poll reflects new settings (e.g. fallback source change)
_status_cache_timestamps['spotify'] = 0
- print("Service clients re-initialized with new settings.")
+ logger.info("Service clients re-initialized with new settings.")
return jsonify({"success": True, "message": "Settings saved successfully."})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@@ -6122,7 +6119,7 @@ def handle_dev_mode():
data = request.get_json()
if data.get('password') == 'hydratest':
dev_mode_enabled = True
- print("Dev mode activated")
+ logger.info("Dev mode activated")
return jsonify({"success": True, "enabled": True})
return jsonify({"success": False, "error": "Invalid password"}), 401
return jsonify({"enabled": dev_mode_enabled})
@@ -6247,10 +6244,10 @@ def hydrabase_connect():
config_manager.set('hydrabase.url', url)
config_manager.set('hydrabase.api_key', api_key)
config_manager.set('hydrabase.auto_connect', True)
- print(f"[Hydrabase] Connected to {url}")
+ logger.info(f"[Hydrabase] Connected to {url}")
return jsonify({"success": True, "message": "Connected"})
except Exception as e:
- print(f"[Hydrabase] Connection failed: {e}")
+ logger.error(f"[Hydrabase] Connection failed: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/hydrabase/disconnect', methods=['POST'])
@@ -6268,7 +6265,7 @@ def hydrabase_disconnect():
# Only disable dev mode if not using Hydrabase as a regular fallback source
if _get_metadata_fallback_source() != 'hydrabase':
dev_mode_enabled = False
- print("[Hydrabase] Disconnected")
+ logger.info("[Hydrabase] Disconnected")
return jsonify({"success": True})
@app.route('/api/hydrabase/status')
@@ -6326,10 +6323,10 @@ def hydrabase_send():
result = json.loads(response)
except json.JSONDecodeError:
result = response
- print(f"[Hydrabase] Sent payload ā got response")
+ logger.info(f"[Hydrabase] Sent payload ā got response")
return jsonify({"success": True, "data": result})
except Exception as e:
- print(f"[Hydrabase] Send failed: {e}")
+ logger.error(f"[Hydrabase] Send failed: {e}")
with _hydrabase_lock:
try:
_hydrabase_ws.close()
@@ -6398,10 +6395,10 @@ def get_log_tail():
level_filter = request.args.get('level', '').upper() # DEBUG, INFO, WARNING, ERROR or empty
log_map = {
- 'app': os.path.join('logs', 'app.log'),
- 'post_processing': os.path.join('logs', 'post_processing.log'),
- 'acoustid': os.path.join('logs', 'acoustid.log'),
- 'source_reuse': os.path.join('logs', 'source_reuse.log'),
+ 'app': Path(_log_path),
+ 'acoustid': _log_dir / 'acoustid.log',
+ 'post_processing': _log_dir / 'post_processing.log',
+ 'source_reuse': _log_dir / 'source_reuse.log',
}
log_path = log_map.get(log_source, log_map['app'])
@@ -6413,7 +6410,7 @@ def get_log_tail():
if ' - INFO - ' in line: return 'INFO'
if ' - WARNING - ' in line: return 'WARNING'
if ' - ERROR - ' in line or ' - CRITICAL - ' in line: return 'ERROR'
- # Heuristic for print() output and non-logger lines
+ # Heuristic for plain-text output and non-logger lines
ll = line.lower()
if 'error' in ll or 'traceback' in ll or 'exception' in ll or 'failed' in ll: return 'ERROR'
if 'warning' in ll or 'warn' in ll: return 'WARNING'
@@ -7154,7 +7151,7 @@ def test_connection_endpoint():
if not service:
return jsonify({"success": False, "error": "No service specified."}), 400
- print(f"Received test connection request for: {service}")
+ logger.info(f"Received test connection request for: {service}")
# Get the current settings from the main config manager to test with
test_config = config_manager.get(service, {})
@@ -7175,18 +7172,18 @@ def test_connection_endpoint():
_status_cache['spotify']['connected'] = True
_status_cache['spotify']['source'] = _get_metadata_fallback_source()
_status_cache_timestamps['spotify'] = current_time
- print("Updated Spotify status cache after successful test")
+ logger.info("Updated Spotify status cache after successful test")
elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']:
_status_cache['media_server']['connected'] = True
_status_cache['media_server']['type'] = service
_status_cache_timestamps['media_server'] = current_time
- print(f"Updated {service} status cache after successful test")
+ logger.info(f"Updated {service} status cache after successful test")
elif service == 'soulseek':
_status_cache['soulseek']['connected'] = True
_status_cache_timestamps['soulseek'] = current_time
- print("Updated Soulseek status cache after successful test")
+ logger.info("Updated Soulseek status cache after successful test")
elif service == 'listenbrainz':
- print("ListenBrainz test successful")
+ logger.info("ListenBrainz test successful")
# Add activity for connection test
if success:
@@ -7204,7 +7201,7 @@ def test_dashboard_connection_endpoint():
if not service:
return jsonify({"success": False, "error": "No service specified."}), 400
- print(f"Received dashboard test connection request for: {service}")
+ logger.info(f"Received dashboard test connection request for: {service}")
# Get the current settings from the main config manager to test with
test_config = config_manager.get(service, {})
@@ -7225,16 +7222,16 @@ def test_dashboard_connection_endpoint():
_status_cache['spotify']['connected'] = True
_status_cache['spotify']['source'] = _get_metadata_fallback_source()
_status_cache_timestamps['spotify'] = current_time
- print("Updated Spotify status cache after successful dashboard test")
+ logger.info("Updated Spotify status cache after successful dashboard test")
elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']:
_status_cache['media_server']['connected'] = True
_status_cache['media_server']['type'] = service
_status_cache_timestamps['media_server'] = current_time
- print(f"Updated {service} status cache after successful dashboard test")
+ logger.info(f"Updated {service} status cache after successful dashboard test")
elif service == 'soulseek':
_status_cache['soulseek']['connected'] = True
_status_cache_timestamps['soulseek'] = current_time
- print("Updated Soulseek status cache after successful dashboard test")
+ logger.info("Updated Soulseek status cache after successful dashboard test")
# Add activity for dashboard connection test (different from settings test)
if success:
@@ -7248,7 +7245,7 @@ def test_dashboard_connection_endpoint():
def detect_media_server_endpoint():
data = request.get_json()
server_type = data.get('server_type')
- print(f"Received auto-detect request for: {server_type}")
+ logger.info(f"Received auto-detect request for: {server_type}")
# Add activity for auto-detect start
add_activity_item("", "Auto-Detect Started", f"Searching for {server_type} server", "Now")
@@ -7559,7 +7556,7 @@ def apply_quality_preset(preset_name):
@app.route('/api/detect-soulseek', methods=['POST'])
def detect_soulseek_endpoint():
- print("Received auto-detect request for slskd")
+ logger.info("Received auto-detect request for slskd")
# Add activity for soulseek auto-detect start
add_activity_item("", "Auto-Detect Started", "Searching for slskd server", "Now")
@@ -7600,10 +7597,10 @@ def auth_spotify():
state=f'profile_{profile_id_int}'
)
auth_url = auth_manager.get_authorize_url()
- print(f"Per-profile Spotify auth initiated for profile {profile_id_int}")
+ logger.info(f"Per-profile Spotify auth initiated for profile {profile_id_int}")
return redirect(auth_url)
except (ValueError, Exception) as e:
- print(f"Per-profile Spotify auth failed, falling back to global: {e}")
+ logger.error(f"Per-profile Spotify auth failed, falling back to global: {e}")
# Global auth (admin or fallback)
temp_spotify_client = SpotifyClient()
@@ -7611,7 +7608,7 @@ def auth_spotify():
# Get the authorization URL
auth_url = temp_spotify_client.sp.auth_manager.get_authorize_url()
configured_uri = config_manager.get_spotify_config().get('redirect_uri', 'http://127.0.0.1:8888/callback')
- print(f"Spotify auth initiated ā redirect_uri: {configured_uri}")
+ logger.info(f"Spotify auth initiated ā redirect_uri: {configured_uri}")
add_activity_item("", "Spotify Auth Started", "Please complete OAuth in browser", "Now")
# Detect if accessing remotely
@@ -7728,7 +7725,7 @@ def auth_spotify():
else:
return "Spotify Authentication Failed
Could not initialize Spotify client. Check your credentials.
", 400
except Exception as e:
- print(f"Error starting Spotify auth: {e}")
+ logger.error(f"Error starting Spotify auth: {e}")
return f"Spotify Authentication Error
{str(e)}
", 500
@app.route('/auth/tidal')
@@ -7736,7 +7733,7 @@ def auth_tidal():
"""
Initiates Tidal OAuth authentication flow
"""
- print("TIDAL AUTH ROUTE CALLED ")
+ logger.info("TIDAL AUTH ROUTE CALLED ")
try:
# Create a fresh tidal client to get OAuth URL
from core.tidal_client import TidalClient
@@ -7759,20 +7756,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}")
+ logger.info(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}")
+ logger.info(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]}...")
+ logger.info(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', '')
@@ -7792,8 +7789,8 @@ 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']}")
+ logger.info(f"Generated Tidal OAuth URL: {auth_url}")
+ logger.info(f"Redirect URI in URL: {params['redirect_uri']}")
add_activity_item("", "Tidal Auth Started", "Please complete OAuth in browser", "Now")
@@ -7865,9 +7862,9 @@ def auth_tidal():
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}")
+ logger.error(f"Error starting Tidal auth: {e}")
import traceback
- print(f"Full traceback: {traceback.format_exc()}")
+ logger.error(f"Full traceback: {traceback.format_exc()}")
return f"Tidal Authentication Error
{str(e)}
", 500
@@ -7884,19 +7881,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}")
+ logger.error(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)}")
+ logger.info(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")
+ logger.info(f"Spotify callback received on port 8008 with authorization code")
# Check for per-profile state parameter
state = request.args.get('state', '')
@@ -7904,7 +7901,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}")
+ logger.info(f"Per-profile callback detected for profile {profile_id_from_state}")
except ValueError:
pass
@@ -7940,7 +7937,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}")
+ logger.info(f"Using redirect_uri for token exchange: {configured_uri}")
auth_manager = SpotifyOAuth(
client_id=config['client_id'],
@@ -7975,7 +7972,7 @@ def spotify_callback():
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}")
+ logger.error(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
@@ -8078,7 +8075,7 @@ def tidal_callback():
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}")
+ logger.error(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()
@@ -8088,7 +8085,7 @@ def tidal_callback():
else:
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}")
+ logger.error(f"Error during Tidal token exchange: {e}")
return f"An Error Occurred
An unexpected error occurred during the authentication process: {e}
", 500
@@ -8209,14 +8206,9 @@ def get_beatport_hero_tracks():
# SMART FILTERING - Remove duplicates and invalid tracks
valid_tracks = []
seen_urls = set()
-
- logger.info(f"Processing {len(tracks)} raw tracks from scraper (SMART FILTERING)...")
+ filtered_reasons = collections.Counter()
for i, track in enumerate(tracks):
- logger.info(f" Track {i+1}: {track.get('title', 'NO_TITLE')} - {track.get('artist', 'NO_ARTIST')}")
- logger.info(f" URL: {track.get('url', 'NO_URL')}")
- logger.info(f" Image: {'YES' if track.get('image_url') else 'NO'}")
-
# Extract and clean basic data
title = track.get('title', '').strip()
artist = track.get('artist', '').strip()
@@ -8261,7 +8253,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)}")
+ filtered_reasons.update(skip_reasons)
continue
# Mark URL as seen for deduplication
@@ -8304,7 +8296,16 @@ def get_beatport_hero_tracks():
break
valid_tracks.append(track_data)
- logger.info(f" Track {i+1} added: {title} - {artist}")
+
+ sample_titles = [f"{t['title']} - {t['artist']}" for t in valid_tracks[:3]]
+ logger.debug(
+ "Beatport smart filter summary: raw=%s valid=%s filtered=%s reasons=%s sample=%s",
+ len(tracks),
+ len(valid_tracks),
+ len(tracks) - len(valid_tracks),
+ dict(filtered_reasons),
+ sample_titles,
+ )
logger.info(f"Retrieved {len(valid_tracks)} valid unique Beatport tracks (SMART FILTERING)")
@@ -8490,17 +8491,17 @@ 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.debug(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.debug(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.debug(f"FOUND FEATURED CHARTS: '{h2.get_text(strip=True)}'")
break
if not featured_container:
@@ -8515,7 +8516,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.debug(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 = {}
@@ -8583,7 +8584,7 @@ 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.debug(f"Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}")
logger.info(f"Successfully extracted {len(charts)} featured charts")
@@ -8639,12 +8640,12 @@ 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.debug(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.debug(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:
@@ -8819,7 +8820,7 @@ def search_music():
return jsonify({"results": all_results})
except Exception as e:
- print(f"Search error: {e}")
+ logger.error(f"Search error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/enhanced-search', methods=['POST'])
@@ -9503,16 +9504,16 @@ def download_music_video():
track_title = best.name
if hasattr(best, 'release_date') and best.release_date:
year = str(best.release_date)[:4]
- print(f"[Music Video] Matched to: {artist_name} - {track_title} (confidence: {best_score:.2f})")
+ logger.info(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}")
+ logger.warning(f"[Music Video] No metadata match, using parsed: {artist_name} - {track_title}")
except Exception as e:
- print(f"[Music Video] Metadata lookup failed: {e}")
+ logger.error(f"[Music Video] Metadata lookup failed: {e}")
if ' - ' in raw_title:
parts = raw_title.split(' - ', 1)
artist_name = parts[0].strip()
@@ -9558,17 +9559,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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"[Music Video] {e}")
# Run in background thread
import threading
@@ -9761,11 +9762,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}")
+ logger.info(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}")
+ logger.info(f"Found exact match in {location_name}: {file_path}")
return file_path, 1.0
exact_matches.append(file_path)
continue
@@ -9777,10 +9778,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}")
+ logger.info(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}")
+ logger.info(f"Found dedup-suffix match in {location_name}: {file_path}")
return file_path, 1.0
exact_matches.append(file_path)
continue
@@ -9796,7 +9797,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]}")
+ logger.info(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
@@ -9808,7 +9809,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}")
+ logger.info(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
@@ -9830,7 +9831,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}")
+ logger.info(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
@@ -9841,7 +9842,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}")
+ logger.info(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
@@ -9910,7 +9911,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")
+ logger.warning(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:
@@ -9921,10 +9922,10 @@ def get_download_status():
if found_path:
try:
os.remove(found_path)
- print(f"Deleted orphaned download: {os.path.basename(found_path)}")
+ logger.warning(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}")
+ logger.error(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
@@ -9948,11 +9949,11 @@ 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}")
+ logger.info(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" Available keys: {available_keys}...")
+ logger.warning(f"[Context Lookup] No context found for key: {context_key}")
+ logger.info(f" Available keys: {available_keys}...")
_stale_transfer_keys.add(context_key)
if context:
@@ -9965,10 +9966,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")
+ logger.info(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}")
+ logger.info(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
@@ -9977,7 +9978,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)}")
+ logger.warning(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
@@ -9989,7 +9990,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})")
+ logger.warning(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
@@ -9998,19 +9999,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.")
+ logger.error(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)")
+ logger.warning(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}")
+ logger.info(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')
@@ -10027,16 +10028,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}")
+ logger.info(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}")
+ logger.info(f"Keeping context for verification worker: {context_key}")
except Exception as e:
- print(f"Error starting post-processing thread for {context_key}: {e}")
+ logger.error(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")
+ logger.warning(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)
@@ -10082,14 +10083,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")
+ logger.info(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}")
+ logger.info(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}")
+ logger.info(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')
@@ -10103,9 +10104,9 @@ def get_download_status():
thread.daemon = True
thread.start()
_processed_download_ids.add(_ctx_key)
- print(f"[{_label}] Marked as processed: {_ctx_key}")
+ logger.info(f"[{_label}] Marked as processed: {_ctx_key}")
except Exception as e:
- print(f"[{_label}] Error starting post-processing thread for {_ctx_key}: {e}")
+ logger.error(f"[{_label}] Error starting post-processing thread for {_ctx_key}: {e}")
processing_thread = threading.Thread(target=process_streaming_download)
processing_thread.daemon = True
@@ -10116,7 +10117,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}")
+ logger.error(f"Could not fetch YouTube/Tidal downloads for status: {streaming_error}")
traceback.print_exc()
# Enrich transfers with metadata from download context (artist, album, artwork)
@@ -10140,7 +10141,7 @@ def get_download_status():
return jsonify({"transfers": all_transfers})
except Exception as e:
- print(f"Error fetching download status: {e}")
+ logger.error(f"Error fetching download status: {e}")
return jsonify({"error": str(e)}), 500
@@ -10170,7 +10171,7 @@ def cancel_download():
else:
return jsonify({"success": False, "error": "Failed to cancel download via slskd."}), 500
except Exception as e:
- print(f"Error cancelling download: {e}")
+ logger.error(f"Error cancelling download: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/downloads/cancel-all', methods=['POST'])
@@ -10192,7 +10193,7 @@ def cancel_all_downloads():
return jsonify({"success": True, "message": "All downloads cancelled and cleared."})
except Exception as e:
- print(f"Error cancelling all downloads: {e}")
+ logger.error(f"Error cancelling all downloads: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/downloads/clear-finished', methods=['POST'])
@@ -10210,7 +10211,7 @@ def clear_finished_downloads():
else:
return jsonify({"success": False, "error": "Backend failed to clear downloads."}), 500
except Exception as e:
- print(f"Error clearing finished downloads: {e}")
+ logger.error(f"Error clearing finished downloads: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/downloads/task//candidates', methods=['GET'])
@@ -10257,7 +10258,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}")
+ logger.error(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'])
@@ -10365,11 +10366,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}")
+ logger.info(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}")
+ logger.error(f"[Manual Download] {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -10396,12 +10397,12 @@ def clear_quarantine():
shutil.rmtree(entry_path)
removed_files += 1
except Exception as e:
- print(f"[Quarantine] Failed to remove {entry}: {e}")
+ logger.error(f"[Quarantine] Failed to remove {entry}: {e}")
- print(f"[Quarantine] Cleared {removed_files} item(s) from quarantine folder")
+ logger.info(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}")
+ logger.error(f"[Quarantine] Error clearing quarantine: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/scan/request', methods=['POST'])
@@ -10598,7 +10599,7 @@ def clear_all_searches():
else:
return jsonify({"success": False, "error": "Backend failed to clear searches."}), 500
except Exception as e:
- print(f"Error clearing searches: {e}")
+ logger.error(f"Error clearing searches: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/searches/maintain', methods=['POST'])
@@ -10620,7 +10621,7 @@ def maintain_search_history():
else:
return jsonify({"success": False, "error": "Backend failed to maintain search history."}), 500
except Exception as e:
- print(f"Error maintaining search history: {e}")
+ logger.error(f"Error maintaining search history: {e}")
return jsonify({"success": False, "error": str(e)}), 500
def fix_artist_image_url(thumb_url):
@@ -10641,13 +10642,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}")
+ logger.debug(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]}...")
+ logger.info(f"Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...")
if plex_base_url and plex_token:
# Extract the path from URL
@@ -10662,14 +10663,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}")
+ logger.info(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'}...")
+ logger.info(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
@@ -10688,7 +10689,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}")
+ logger.info(f"Fixed URL: {fixed_url}")
return fixed_url
elif active_server == 'navidrome':
@@ -10696,7 +10697,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}")
+ logger.info(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
@@ -10721,16 +10722,16 @@ 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}")
+ logger.info(f"Fixed URL: {fixed_url}")
return fixed_url
- print(f"No configuration found for {active_server} or unsupported server type")
+ logger.warning(f"No configuration found for {active_server} or unsupported server type")
# Return original URL if no fixing needed/possible
return thumb_url
except Exception as e:
- print(f"Error fixing image URL '{thumb_url}': {e}")
+ logger.error(f"Error fixing image URL '{thumb_url}': {e}")
return thumb_url
@app.route('/api/library/history')
@@ -10797,7 +10798,7 @@ def get_library_artists():
})
except Exception as e:
- print(f"Error fetching library artists: {e}")
+ logger.error(f"Error fetching library artists: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -10826,7 +10827,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}")
+ logger.info(f"Getting artist detail for ID: {artist_id}")
# Get database instance
database = get_database()
@@ -10835,7 +10836,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}")
+ logger.error(f"Database returned error: {db_result}")
return jsonify({
"success": False,
"error": db_result.get('error', 'Artist not found')
@@ -10844,18 +10845,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")
+ logger.info(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')}'")
+ logger.info(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']}'")
+ logger.info(f"Artist image after fix: '{artist_info['image_url']}'")
else:
- print(f"No artist image URL found for {artist_info['name']}")
+ logger.warning(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}")
+ logger.info(f"Final artist data being sent: {artist_info}")
# Fix image URLs for all albums
for album in owned_releases['albums']:
@@ -10897,7 +10898,7 @@ def get_artist_detail(artist_id):
)
if artist_detail_discography['success']:
- print(
+ logger.debug(
"Source-priority discography found - "
f"Albums: {len(artist_detail_discography['albums'])}, "
f"EPs: {len(artist_detail_discography['eps'])}, "
@@ -10905,10 +10906,10 @@ def get_artist_detail(artist_id):
)
merged_discography = artist_detail_discography
else:
- print(f"Source-priority discography not found: {artist_detail_discography.get('error', 'Unknown error')}")
+ logger.debug(f"Source-priority discography not found: {artist_detail_discography.get('error', 'Unknown error')}")
merged_discography = owned_releases
except Exception as detail_error:
- print(f"Error fetching source-priority discography: {detail_error}")
+ logger.error(f"Error fetching source-priority discography: {detail_error}")
merged_discography = owned_releases
spotify_artist_data = None
@@ -10969,7 +10970,7 @@ def get_artist_detail(artist_id):
return jsonify(response_data)
except Exception as e:
- print(f"Error in get_artist_detail: {e}")
+ logger.error(f"Error in get_artist_detail: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -11029,7 +11030,7 @@ def get_similar_artists_stream(artist_name):
"""
def generate():
try:
- print(f"Streaming similar artists for: {artist_name}")
+ logger.info(f"Streaming similar artists for: {artist_name}")
# Import required libraries
from bs4 import BeautifulSoup
@@ -11038,7 +11039,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}")
+ logger.debug(f"Fetching MusicMap: {musicmap_url}")
# Set headers to mimic a browser
headers = {
@@ -11073,7 +11074,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")
+ logger.debug(f"Found {len(similar_artist_names)} similar artists from MusicMap")
# Determine metadata source
use_hydrabase = _is_hydrabase_active()
@@ -11092,9 +11093,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}")
+ logger.info(f"Searched artist ID: {searched_artist_id}")
except Exception as e:
- print(f"Could not get searched artist ID: {e}")
+ logger.error(f"Could not get searched artist ID: {e}")
# Match each artist one by one and stream results
max_artists = 20
@@ -11103,7 +11104,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}")
+ logger.info(f"Matching: {artist_name_to_match}")
# Search for the artist via active metadata source
if use_hydrabase:
@@ -11116,12 +11117,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}")
+ logger.info(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}")
+ logger.warning(f"Skipping duplicate artist: {spotify_artist.name}")
continue
seen_artist_ids.add(spotify_artist.id)
@@ -11138,24 +11139,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}")
+ logger.info(f"Matched and streamed: {spotify_artist.name}")
else:
- print(f"No Spotify match found for: {artist_name_to_match}")
+ logger.warning(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}")
+ logger.error(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")
+ logger.info(f"Streaming complete: {matched_count} artists matched")
except requests.exceptions.RequestException as e:
- print(f"Error fetching MusicMap: {e}")
+ logger.debug(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}")
+ logger.error(f"Error streaming similar artists: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'error': str(e)})}\n\n"
@@ -11174,7 +11175,7 @@ def get_similar_artists(artist_name):
JSON with similar artists matched to Spotify data
"""
try:
- print(f"Getting similar artists for: {artist_name}")
+ logger.info(f"Getting similar artists for: {artist_name}")
# Import required libraries
from bs4 import BeautifulSoup
@@ -11183,7 +11184,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}")
+ logger.debug(f"Fetching MusicMap: {musicmap_url}")
# Set headers to mimic a browser
headers = {
@@ -11220,7 +11221,7 @@ def get_similar_artists(artist_name):
similar_artist_names.append(artist_text)
- print(f"Found {len(similar_artist_names)} similar artists from MusicMap")
+ logger.debug(f"Found {len(similar_artist_names)} similar artists from MusicMap")
# Determine metadata source
use_hydrabase = _is_hydrabase_active()
@@ -11241,9 +11242,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}")
+ logger.info(f"Searched artist ID: {searched_artist_id}")
except Exception as e:
- print(f"Could not get searched artist ID: {e}")
+ logger.error(f"Could not get searched artist ID: {e}")
# Match each artist (limit to first 20 for performance)
matched_artists = []
@@ -11252,7 +11253,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}")
+ logger.info(f"Matching: {artist_name_to_match}")
# Search for the artist via active metadata source
if use_hydrabase:
@@ -11265,12 +11266,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}")
+ logger.info(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}")
+ logger.warning(f"Skipping duplicate artist: {spotify_artist.name}")
continue
seen_artist_ids.add(spotify_artist.id)
@@ -11283,15 +11284,15 @@ def get_similar_artists(artist_name):
'popularity': spotify_artist.popularity if hasattr(spotify_artist, 'popularity') else 0
})
- print(f"Matched: {spotify_artist.name}")
+ logger.info(f"Matched: {spotify_artist.name}")
else:
- print(f"No Spotify match found for: {artist_name_to_match}")
+ logger.warning(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}")
+ logger.error(f"Error matching {artist_name_to_match}: {match_error}")
continue
- print(f"Successfully matched {len(matched_artists)} artists to Spotify")
+ logger.info(f"Successfully matched {len(matched_artists)} artists to Spotify")
return jsonify({
"success": True,
@@ -11302,14 +11303,14 @@ def get_similar_artists(artist_name):
})
except requests.exceptions.RequestException as e:
- print(f"Error fetching MusicMap: {e}")
+ logger.debug(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}")
+ logger.error(f"Error getting similar artists: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -11371,7 +11372,7 @@ def get_artist_image(artist_id):
image_url = fallback._get_artist_image_from_albums(artist_id)
return jsonify({"success": True, "image_url": image_url})
except Exception as e:
- print(f"Error fetching artist image: {e}")
+ logger.error(f"Error fetching artist image: {e}")
return jsonify({"success": False, "image_url": None, "error": str(e)})
@app.route('/api/artist//discography', methods=['GET'])
@@ -11692,7 +11693,7 @@ def download_discography(artist_id):
total_added += added
total_skipped += skipped
- print(f"[Discography] {album_name}: {added} added, {skipped} skipped")
+ logger.warning(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)
@@ -11701,13 +11702,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")
+ logger.warning(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}")
+ logger.error(f"Error in download discography: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/artist//completion', methods=['POST'])
@@ -11728,7 +11729,7 @@ def check_artist_discography_completion(artist_id):
)
return jsonify(result)
except Exception as e:
- print(f"Error checking discography completion: {e}")
+ logger.error(f"Error checking discography completion: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -11752,7 +11753,7 @@ def check_artist_discography_completion_stream(artist_id):
def generate_completion_stream():
try:
- print(f"Starting streaming completion check for artist: {artist_name}")
+ logger.info(f"Starting streaming completion check for artist: {artist_name}")
for event in iter_artist_discography_completion_events(
discography,
artist_name=artist_name,
@@ -11763,7 +11764,7 @@ def check_artist_discography_completion_stream(artist_id):
# Small delay to make the streaming effect visible
time.sleep(0.1) # 100ms delay between items
except Exception as e:
- print(f"Error in streaming completion check: {e}")
+ logger.error(f"Error in streaming completion check: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
@@ -11863,7 +11864,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}")
+ logger.error(f"Error in library completion stream: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
@@ -11994,7 +11995,7 @@ def library_check_tracks():
return jsonify({"success": True, "owned_tracks": owned_map})
except Exception as e:
- print(f"Error checking track ownership: {e}")
+ logger.error(f"Error checking track ownership: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -12203,7 +12204,7 @@ def enhance_artist_quality(artist_id):
'external_urls': {},
}
except Exception as e:
- print(f"[Enhance] Spotify lookup failed for {spotify_tid}: {e}")
+ logger.error(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
@@ -12277,7 +12278,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}")
+ logger.error(f"[Enhance] Search match failed for {title}: {e}")
# Fallback source when Spotify unavailable or no match found
if not matched_track_data:
@@ -12346,9 +12347,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})")
+ logger.warning(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}")
+ logger.error(f"[Enhance] Fallback source failed for {title}: {e}")
if not matched_track_data:
failed_count += 1
@@ -12375,7 +12376,7 @@ def enhance_artist_quality(artist_id):
if success:
enhanced_count += 1
- print(f"[Enhance] Queued for upgrade: {artist_name} - {title} ({tier_name})")
+ logger.info(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'})
@@ -12387,7 +12388,7 @@ def enhance_artist_quality(artist_id):
'failed_tracks': failed_tracks
})
except Exception as e:
- print(f"[Enhance] Error: {e}")
+ logger.error(f"[Enhance] {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -13594,9 +13595,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}")
+ logger.info(f"[Reorganize] Moved {sidecar} to {dest_dir}")
except Exception as sc_err:
- print(f"[Reorganize] Failed to move {sidecar}: {sc_err}")
+ logger.error(f"[Reorganize] Failed to move {sidecar}: {sc_err}")
# Clean up empty directories left behind (after sidecars moved)
for src_dir in moved_dirs:
@@ -14109,7 +14110,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)}")
+ logger.info(f"Library play request: {os.path.basename(file_path)}")
# Set stream state to ready with the library file path directly
with stream_lock:
@@ -14129,7 +14130,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}")
+ logger.error(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')}
@@ -14203,7 +14204,7 @@ def library_enrich_entity():
})
except Exception as e:
- print(f"Error enriching entity: {e}")
+ logger.error(f"Error enriching entity: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -14360,7 +14361,7 @@ def library_search_service():
return jsonify({"success": True, "results": results})
except Exception as e:
- print(f"Error searching service: {e}")
+ logger.error(f"Error searching service: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -14684,7 +14685,7 @@ def library_manual_match():
})
except Exception as e:
- print(f"Error manual matching: {e}")
+ logger.error(f"Error manual matching: {e}")
@app.route('/api/library/clear-match', methods=['PUT'])
def library_clear_match():
@@ -14742,7 +14743,7 @@ def library_clear_match():
})
except Exception as e:
- print(f"Error clearing match: {e}")
+ logger.error(f"Error clearing match: {e}")
return jsonify({"success": False, "error": str(e)}), 500
import traceback
@@ -14826,7 +14827,7 @@ def library_delete_track(track_id):
result["file_error"] = file_error
return jsonify(result)
except Exception as e:
- print(f"Error deleting track {track_id}: {e}")
+ logger.error(f"Error deleting track {track_id}: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -15468,24 +15469,24 @@ def sync_artist_library(artist_id):
# Fetch the artist object from the server
server_artist = None
- print(f"[Artist Sync] Fetching artist {db_artist_id} from {server_source}...")
+ logger.info(f"[Artist Sync] Fetching artist {db_artist_id} from {server_source}...")
if server_source == 'plex' and hasattr(media_client, 'server'):
try:
server_artist = media_client.server.fetchItem(int(db_artist_id))
- print(f"[Artist Sync] Plex returned: {getattr(server_artist, 'title', 'None')}")
+ logger.info(f"[Artist Sync] Plex returned: {getattr(server_artist, 'title', 'None')}")
except Exception as e:
- print(f"[Artist Sync] Plex fetchItem failed: {e}")
+ logger.error(f"[Artist Sync] Plex fetchItem failed: {e}")
elif hasattr(media_client, 'get_artist_by_id'):
try:
server_artist = media_client.get_artist_by_id(str(db_artist_id))
- print(f"[Artist Sync] Server returned: {getattr(server_artist, 'title', None) or server_artist}")
+ logger.info(f"[Artist Sync] Server returned: {getattr(server_artist, 'title', None) or server_artist}")
except Exception as e:
- print(f"[Artist Sync] get_artist_by_id failed: {e}")
+ logger.error(f"[Artist Sync] get_artist_by_id failed: {e}")
else:
- print(f"[Artist Sync] No get_artist_by_id method on {type(media_client).__name__}")
+ logger.warning(f"[Artist Sync] No get_artist_by_id method on {type(media_client).__name__}")
if not server_artist:
- print(f"[Artist Sync] Could not fetch artist from server ā skipping pull phase")
+ logger.error(f"[Artist Sync] Could not fetch artist from server ā skipping pull phase")
if server_artist:
# Check for name change
@@ -15495,7 +15496,7 @@ def sync_artist_library(artist_id):
"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}'")
+ logger.info(f"[Artist Sync] Name updated: '{artist_name}' ā '{new_name}'")
artist_name = new_name
name_updated = True
@@ -15503,10 +15504,10 @@ def sync_artist_library(artist_id):
success, details, new_albums, new_tracks = worker._process_artist_with_content(
server_artist, skip_existing_tracks=True
)
- print(f"[Artist Sync] Server pull for {artist_name}: {details}")
+ logger.info(f"[Artist Sync] Server pull for {artist_name}: {details}")
except Exception as e:
- print(f"[Artist Sync] Server pull failed for {artist_name}: {e}")
+ logger.error(f"[Artist Sync] Server pull failed for {artist_name}: {e}")
# āā Phase 2: Remove stale entries (files no longer on disk) āā
stale_removed = 0
@@ -15546,7 +15547,7 @@ def sync_artist_library(artist_id):
conn.commit()
- print(f"[Artist Sync] {artist_name}: +{new_albums} albums, +{new_tracks} tracks, "
+ logger.warning(f"[Artist Sync] {artist_name}: +{new_albums} albums, +{new_tracks} tracks, "
f"-{stale_removed} stale, -{empty_albums_removed} empty albums")
return jsonify({
@@ -15560,7 +15561,7 @@ def sync_artist_library(artist_id):
})
except Exception as e:
- print(f"Error syncing artist {artist_id}: {e}")
+ logger.error(f"Error syncing artist {artist_id}: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -15636,7 +15637,7 @@ def library_delete_album(album_id):
"files_failed": files_failed
})
except Exception as e:
- print(f"Error deleting album {album_id}: {e}")
+ logger.error(f"Error deleting album {album_id}: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -15661,7 +15662,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}")
+ logger.error(f"Error batch deleting tracks: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -15692,7 +15693,7 @@ def library_radio():
return jsonify(result)
except Exception as e:
- print(f"Error getting radio tracks: {e}")
+ logger.error(f"Error getting radio tracks: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -15709,7 +15710,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')}")
+ logger.info(f"Web UI Stream request for: {data.get('filename')}")
try:
# Stop any existing streaming task
@@ -15733,7 +15734,7 @@ def stream_start():
return jsonify({"success": True, "message": "Streaming started"})
except Exception as e:
- print(f"Error starting stream: {e}")
+ logger.error(f"Error starting stream: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/stream/status')
@@ -15749,7 +15750,7 @@ def stream_status():
"error_message": stream_state["error_message"]
})
except Exception as e:
- print(f"Error getting stream status: {e}")
+ logger.error(f"Error getting stream status: {e}")
return jsonify({
"status": "error",
"progress": 0,
@@ -15770,7 +15771,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)}")
+ logger.info(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()
@@ -15852,7 +15853,7 @@ def stream_audio():
return response
except Exception as e:
- print(f"Error serving audio file: {e}")
+ logger.error(f"Error serving audio file: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/stream/stop', methods=['POST'])
@@ -15878,9 +15879,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}")
+ logger.info(f"Removed stream file: {filename}")
else:
- print(f"Library playback stopped - skipping file deletion")
+ logger.info(f"Library playback stopped - skipping file deletion")
# Reset stream state
with stream_lock:
@@ -15896,7 +15897,7 @@ def stream_stop():
return jsonify({"success": True, "message": "Stream stopped"})
except Exception as e:
- print(f"Error stopping stream: {e}")
+ logger.error(f"Error stopping stream: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# --- Matched Downloads API Endpoints ---
@@ -15910,22 +15911,22 @@ 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', '')}")
+ logger.info(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")
+ logger.info(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)")
import re
clean_album_title = re.sub(r'^\(\d{4}\)\s*', '', album_title).strip()
- print(f" clean_album_title: '{clean_album_title}'")
+ logger.info(f" clean_album_title: '{clean_album_title}'")
# 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")
+ logger.info(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
@@ -15945,7 +15946,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}")
+ logger.error(f"Error fetching artist '{artist_name}': {e}")
return artist_name, None
# Use limited concurrency to respect rate limits
@@ -15996,7 +15997,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}'")
+ logger.info(f"Single track mode - searching for artist: '{search_artist}'")
# Search for artists directly
artist_matches = spotify_client.search_artists(search_artist, limit=10)
@@ -16024,7 +16025,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}")
+ logger.error(f"Error generating artist suggestions: {e}")
return []
def _generate_album_suggestions(selected_artist, search_result):
@@ -16036,22 +16037,22 @@ def _generate_album_suggestions(selected_artist, search_result):
return []
try:
- print(f"Generating album suggestions for artist: {selected_artist['name']}")
+ logger.info(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")
+ logger.warning("No album name found in search result")
return []
# Clean target album name
import re
clean_target = re.sub(r'^\(\d{4}\)\s*', '', target_album_name).strip()
- print(f" target_album: '{clean_target}'")
+ logger.info(f" target_album: '{clean_target}'")
# 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")
+ logger.info(f"Found {len(artist_albums)} albums for artist")
album_matches = []
for album in artist_albums:
@@ -16078,7 +16079,7 @@ def _generate_album_suggestions(selected_artist, search_result):
return album_matches[:4]
except Exception as e:
- print(f"Error generating album suggestions: {e}")
+ logger.error(f"Error generating album suggestions: {e}")
return []
@app.route('/api/match/suggestions', methods=['POST'])
@@ -16102,7 +16103,7 @@ def get_match_suggestions():
return jsonify({"suggestions": suggestions})
except Exception as e:
- print(f"Error in match suggestions: {e}")
+ logger.error(f"Error in match suggestions: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/match/search', methods=['POST'])
@@ -16200,7 +16201,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}")
+ logger.error(f"Error in match search: {e}")
return jsonify({"error": str(e)}), 500
@@ -16250,10 +16251,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']}': "
+ logger.info(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}")
+ logger.error(f"[Preflight] MB release preflight failed: {pf_err}")
# Process matched tracks with full Spotify metadata
for matched_item in enhanced_tracks:
@@ -16299,10 +16300,14 @@ 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(
+ "Queued matched track: title=%r track_number=%s",
+ spotify_track['name'],
+ spotify_track['track_number'],
+ )
started_count += 1
else:
- logger.error(f"Failed to queue track: {filename}")
+ logger.error("Failed to queue track: filename=%s", filename)
except Exception as e:
logger.error(f"Error processing matched track: {e}")
@@ -16352,18 +16357,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.")
+ logger.info(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.")
+ logger.warning("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.")
+ logger.error("Could not fetch official tracklist from Spotify. Metadata may be inaccurate.")
# --- END OF NEW LOGIC ---
# Compute total_discs for multi-disc album subfolder support
@@ -16387,10 +16392,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']}': "
+ logger.info(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}")
+ logger.error(f"[Preflight] MB release preflight failed: {pf_err}")
started_count = 0
for track_data in tracks_to_download:
@@ -16411,7 +16416,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')}'")
+ logger.info(f"[Content Filter] Skipping explicit track: '{corrected_meta.get('title')}'")
continue
# Create a clean context object using the CORRECTED metadata
@@ -16441,13 +16446,17 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
"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')}')")
+ logger.info(
+ "Queued track: filename=%s matched_title=%r",
+ filename,
+ corrected_meta.get('title'),
+ )
started_count += 1
else:
- print(f" - Failed to queue track: {filename}")
+ logger.error("Failed to queue track: filename=%s", filename)
except Exception as e:
- print(f"Error processing track in album batch: {track_data.get('filename')}. Error: {e}")
+ logger.error(f"Error processing track in album batch: {track_data.get('filename')}: {e}")
continue
return started_count
@@ -16652,7 +16661,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']}'")
+ logger.info(f"Parsed Filename '{base_name}': Artist='{metadata['artist']}', Title='{metadata['title']}', Album='{metadata['album']}', Track#='{metadata['track_number']}'")
return metadata
@@ -16822,7 +16831,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']}'")
+ logger.info(f"Found track in album context: '{track_data['name']}'")
return {
'is_album': True,
'album_name': spotify_album.name,
@@ -16832,7 +16841,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}")
+ logger.error(f"Error in _search_track_in_album_context: {e}")
return None
@@ -16846,35 +16855,44 @@ 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" - 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')}'")
- print(f" - Artist: '{artist.get('name', 'Unknown')}'")
- print(f" - Context has clean data: {context.get('has_clean_spotify_data', False)}")
- print(f" - Is album download: {context.get('is_album_download', False)}")
+ logger.info(
+ "[Album Detection] start: track=%r clean_spotify_title=%r clean_spotify_album=%r "
+ "filename_album=%r artist=%r clean_data=%s album_download=%s",
+ original_search.get('title', 'Unknown'),
+ original_search.get('spotify_clean_title', 'None'),
+ original_search.get('spotify_clean_album', 'None'),
+ original_search.get('album', 'None'),
+ artist.get('name', 'Unknown'),
+ context.get('has_clean_spotify_data', False),
+ context.get('is_album_download', False),
+ )
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')}'")
+ logger.info(
+ "[Album Detection] track=%r artist=%r has_album_attr=%s album=%r",
+ original_search.get('title', 'Unknown'),
+ artist_name,
+ bool(original_search.get('album')),
+ original_search.get('album'),
+ )
# --- THIS IS THE CRITICAL FIX ---
# 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.")
-
# We exclusively use the track number and title that were matched
# *before* the download started. We do not try to re-parse the filename.
track_number = original_search.get('track_number', 1)
clean_track_name = original_search.get('title', 'Unknown Track')
- print(f" -> Using pre-matched Track #{track_number} and Title '{clean_track_name}'")
+ logger.info(
+ "[Album Detection] using matched context: track_number=%s title=%r album=%r",
+ track_number,
+ clean_track_name,
+ spotify_album_context['name'],
+ )
return {
'is_album': True,
@@ -16901,7 +16919,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}'")
+ logger.info(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')
@@ -16910,10 +16928,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")
+ logger.info(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")
+ logger.error(f"PRIORITY 1 FAILED: Track not found using {album_source} album name")
finally:
# Restore original album value
if original_album is not None:
@@ -16922,13 +16940,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)...")
+ logger.info(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}'")
+ logger.info(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}"
@@ -16967,25 +16985,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})")
+ logger.info(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}")
+ logger.info(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")
+ logger.info(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}'")
+ logger.info(f"Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})")
+ logger.info(f"Clean track name from Spotify: '{clean_track_name}'")
# Enhanced album detection using detailed API data (GUI PARITY)
is_album = (
@@ -17003,7 +17021,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})")
+ logger.info(f"Album classification: {is_album} (type={album_type}, tracks={total_tracks})")
return {
'is_album': is_album,
@@ -17016,7 +17034,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")
+ logger.warning("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)
@@ -17034,7 +17052,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
}
except Exception as e:
- print(f"Error in _detect_album_info_web: {e}")
+ logger.error(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')
@@ -17052,13 +17070,13 @@ def _cleanup_empty_directories(download_path, moved_file_path):
while current_dir != download_path and current_dir.startswith(download_path):
is_empty = not any(not f.startswith('.') for f in os.listdir(current_dir))
if is_empty:
- print(f"Removing empty directory: {current_dir}")
+ logger.warning(f"Removing empty directory: {current_dir}")
os.rmdir(current_dir)
current_dir = os.path.dirname(current_dir)
else:
break
except Exception as e:
- print(f"Warning: An error occurred during directory cleanup: {e}")
+ logger.error(f"An error occurred during directory cleanup: {e}")
def _sweep_empty_download_directories():
@@ -17100,10 +17118,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")
+ logger.warning(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}")
+ logger.error(f"[Folder Cleanup] Error sweeping empty directories: {e}")
return 0
@@ -17163,7 +17181,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}'")
+ logger.info(f"Detected deluxe edition: '{album_name}' contains '{indicator}'")
return True
return False
@@ -17186,7 +17204,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}'")
+ logger.info(f"Album correction applied: '{base_album}' -> '{correction}'")
return correction
# Handle punctuation variations
@@ -17197,7 +17215,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}'")
+ logger.info(f"Album variant normalization: '{base_album}' -> '{normalized}'")
return normalized
def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: str = None) -> str:
@@ -17230,10 +17248,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}'")
+ logger.info(f"Using cached album name for '{album_key}': '{cached_name}'")
return cached_name
- print(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
+ logger.info(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
# Check if this track indicates a deluxe edition
is_deluxe_track = False
@@ -17247,7 +17265,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!")
+ logger.info(f"UPGRADE: Album '{base_album}' upgraded from standard to deluxe!")
album_editions[album_key] = "deluxe"
current_edition = "deluxe"
@@ -17262,12 +17280,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})")
+ logger.info(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}")
+ logger.error(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:
@@ -17277,7 +17295,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}')")
+ logger.info(f"Album Title Cleaning: '{original}' (artist: '{artist_name}')")
# Remove "Album - " prefix
cleaned = re.sub(r'^Album\s*-\s*', '', cleaned, flags=re.IGNORECASE)
@@ -17316,7 +17334,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}'")
+ logger.info(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:
@@ -17335,10 +17353,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})")
+ logger.error(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}'")
+ logger.info(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)
@@ -17346,21 +17364,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}")
+ logger.info(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}")
+ logger.warning(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)")
+ logger.info(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}")
+ logger.error(f"Could not get tracks for album: {album.name}")
continue
# Check if our track is in this album
@@ -17379,7 +17397,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})")
+ logger.info(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'
@@ -17390,7 +17408,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})")
+ logger.info(f"Album context classification: is_album={ctx_is_album} (type={ctx_album_type}, tracks={ctx_total_tracks})")
return {
'is_album': ctx_is_album,
@@ -17402,13 +17420,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}'")
+ logger.warning(f"Track '{clean_track}' not found in album '{album.name}'")
- print(f"Track '{clean_track}' not found in any matching albums")
+ logger.warning(f"Track '{clean_track}' not found in any matching albums")
return None
except Exception as e:
- print(f"Error in album-aware search: {e}")
+ logger.error(f"Error in album-aware search: {e}")
return None
def _clean_track_title_web(track_title: str, artist_name: str) -> str:
@@ -17418,7 +17436,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}')")
+ logger.info(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."
@@ -17447,7 +17465,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}'")
+ logger.info(f"Track Title Result: '{original}' -> '{cleaned}'")
return cleaned if cleaned else original
@@ -17483,7 +17501,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}')")
+ logger.info(f"Removed artist from start: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')")
title = cleaned_title
artist_removed = True
else:
@@ -17493,7 +17511,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}')")
+ logger.info(f"Removed artist from end: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')")
title = cleaned_title
artist_removed = True
@@ -17555,7 +17573,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}'")
+ logger.info(f"Skipping artist removal - collaboration detected: '{title}'")
# Remove "prod. Producer" patterns
title = re.sub(r'\s+prod\.?\s+\S+', '', title, flags=re.IGNORECASE)
@@ -17583,7 +17601,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}'")
+ logger.info(f"YouTube title cleaned: '{original_title}' ā '{title}'")
return title
@@ -17648,7 +17666,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}'")
+ logger.info(f"YouTube artist cleaned: '{original_artist}' ā '{artist_string}'")
return artist_string
@@ -17675,14 +17693,14 @@ def parse_youtube_playlist(url):
playlist_info = ydl.extract_info(url, download=False)
if not playlist_info:
- print("Could not extract playlist information")
+ logger.error("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")
+ logger.info(f"Found YouTube playlist: '{playlist_name}' with {len(entries)} entries")
for entry in entries:
if not entry:
@@ -17722,11 +17740,11 @@ def parse_youtube_playlist(url):
'image_url': playlist_info.get('thumbnail', '') or '',
}
- print(f"Successfully parsed YouTube playlist: {len(tracks)} tracks extracted")
+ logger.info(f"Successfully parsed YouTube playlist: {len(tracks)} tracks extracted")
return playlist_data
except Exception as e:
- print(f"Error parsing YouTube playlist: {e}")
+ logger.error(f"Error parsing YouTube playlist: {e}")
return None
@@ -17806,7 +17824,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}")
+ logger.info(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)
@@ -17978,9 +17996,9 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext):
total_discs = max((t.get('disc_number', 1) for t in _atd['items']), default=1)
if total_discs > 1:
spotify_album['total_discs'] = total_discs
- print(f"[Multi-Disc] Resolved {total_discs} discs for single-track download of '{spotify_album.get('name')}'")
+ logger.info(f"[Multi-Disc] Resolved {total_discs} discs for single-track download of '{spotify_album.get('name')}'")
except Exception as _disc_err:
- print(f"[Multi-Disc] Could not resolve total_discs: {_disc_err}")
+ logger.warning(f"[Multi-Disc] Could not resolve total_discs: {_disc_err}")
# Check if user controls disc structure via $disc in their template
album_template = config_manager.get('file_organization.templates.album_path', '')
@@ -18205,13 +18223,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}")
+ logger.error(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)}")
+ logger.info(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:
@@ -18219,7 +18237,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")
+ logger.warning("[Downsample] ffmpeg not found ā skipping hi-res conversion")
return None
temp_path = final_path + '.tmp.flac'
@@ -18234,27 +18252,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]}")
+ logger.error(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")
+ logger.warning(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")
+ logger.info(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)}")
+ logger.info(f"[Downsample] Converted to 16-bit/44.1kHz: {os.path.basename(final_path)}")
# Update QUALITY tag in the new file
new_quality = 'FLAC 16bit'
@@ -18263,7 +18281,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}")
+ logger.error(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', '')
@@ -18275,7 +18293,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}")
+ logger.info(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
@@ -18284,16 +18302,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}")
+ logger.error(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)}")
+ logger.info(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}")
+ logger.error(f"[Downsample] Conversion error: {e}")
if os.path.exists(temp_path):
try:
os.remove(temp_path)
@@ -18335,7 +18353,7 @@ def _create_lossy_copy(final_path):
}
if codec not in codec_map:
- print(f"[Lossy Copy] Unknown codec '{codec}' ā skipping conversion")
+ logger.info(f"[Lossy Copy] Unknown codec '{codec}' ā skipping conversion")
return None
ffmpeg_codec, out_ext, quality_label, extra_args = codec_map[codec]
@@ -18355,11 +18373,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")
+ logger.warning(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)}")
+ logger.info(f"[Lossy Copy] Converting to {quality_label}: {os.path.basename(final_path)}")
cmd = [
ffmpeg_bin, '-i', final_path,
'-codec:a', ffmpeg_codec,
@@ -18370,7 +18388,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)}")
+ logger.info(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:
@@ -18387,7 +18405,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}")
+ logger.error(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
@@ -18417,7 +18435,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)")
+ logger.warning(f"[Lossy Copy] Using cover.jpg as art source (FLAC had no embedded art)")
except Exception:
pass
@@ -18442,15 +18460,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")
+ logger.info(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")
+ logger.info(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}")
+ logger.error(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):
@@ -18466,7 +18484,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)}")
+ logger.info(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
@@ -18474,16 +18492,16 @@ 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)}")
+ logger.info(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}")
+ logger.error(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)}")
+ logger.error(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)}")
+ logger.warning(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}")
+ logger.error(f"[Blasphemy Mode] Error during original deletion, keeping original: {del_err}")
else:
# ffmpeg always prints its version banner to stderr (~300 chars).
# Strip it so the actual error is visible, and show more than 200 chars.
@@ -18498,17 +18516,17 @@ def _create_lossy_copy(final_path):
elif line.strip() == '':
past_banner = True
error_msg = '\n'.join(error_lines).strip() if error_lines else stderr[-500:]
- print(f"[Lossy Copy] ffmpeg failed (exit code {result.returncode}): {error_msg[:500]}")
+ logger.error(f"[Lossy Copy] ffmpeg failed (exit code {result.returncode}): {error_msg[:500]}")
# Clean up empty/broken output file
if os.path.isfile(out_path) and os.path.getsize(out_path) == 0:
os.remove(out_path)
- print(f"[Lossy Copy] Removed empty output file: {os.path.basename(out_path)}")
+ logger.warning(f"[Lossy Copy] Removed empty output file: {os.path.basename(out_path)}")
except subprocess.TimeoutExpired:
- print(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")
+ logger.info(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}")
if os.path.isfile(out_path) and os.path.getsize(out_path) == 0:
os.remove(out_path)
except Exception as e:
- print(f"[Lossy Copy] Conversion error: {e}")
+ logger.error(f"[Lossy Copy] Conversion error: {e}")
return None
def _get_album_type_display(raw_type, track_count) -> str:
@@ -18766,10 +18784,10 @@ def _wipe_source_tags(file_path: str) -> bool:
else:
audio.save()
if tag_count > 0:
- print(f"[Tag Wipe] Stripped {tag_count} source tags from: {os.path.basename(file_path)}")
+ logger.info(f"[Tag Wipe] Stripped {tag_count} source tags from: {os.path.basename(file_path)}")
return True
except Exception as e:
- print(f"[Tag Wipe] Failed (non-fatal): {e}")
+ logger.error(f"[Tag Wipe] Failed (non-fatal): {e}")
return False
@@ -18791,11 +18809,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])}")
+ logger.info(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}")
+ logger.error(f"Could not strip APEv2 tags (non-fatal): {e}")
return summary
def _verify_metadata_written(file_path: str) -> bool:
@@ -18803,7 +18821,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}")
+ logger.info(f"[VERIFY] Tags are None after save: {file_path}")
return False
title_found = False
artist_found = False
@@ -18813,7 +18831,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!")
+ logger.info(f"[VERIFY] APEv2 tags still present after processing!")
return False
except APENoHeaderError:
pass
@@ -18824,12 +18842,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}")
+ logger.warning(f"[VERIFY] Missing metadata - title:{title_found} artist:{artist_found}")
return False
- print(f"[VERIFY] Metadata verified OK")
+ logger.info(f"[VERIFY] Metadata verified OK")
return True
except Exception as e:
- print(f"[VERIFY] Verification error (non-fatal): {e}")
+ logger.error(f"[VERIFY] Verification error (non-fatal): {e}")
return False
def _is_ogg_opus(audio_file):
@@ -18847,7 +18865,7 @@ 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.")
+ logger.warning("Metadata enhancement disabled in config.")
return True
# Normalize None album_info to empty dict to prevent AttributeError on .get() calls
@@ -18857,14 +18875,14 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
# 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)}")
+ logger.info(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}")
+ logger.error(f"Could not load audio file with Mutagen: {file_path}")
return False
# āā Wipe ALL existing tags and save immediately āā
@@ -18879,7 +18897,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: "
+ logger.info(f"Clearing {len(audio_file.tags)} existing tags: "
f"{', '.join(str(k) for k in tag_keys)}")
audio_file.tags.clear()
else:
@@ -18895,7 +18913,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.")
+ logger.error("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):
@@ -19011,18 +19029,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.")
+ logger.info("Metadata enhanced successfully.")
else:
- print("Metadata saved but verification found issues (see above).")
+ logger.info("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()}")
+ logger.error(f"Error enhancing metadata for {file_path}: {e}")
+ logger.error(f"[Metadata Debug] Exception type: {type(e).__name__}")
+ logger.info(f"[Metadata Debug] File exists: {os.path.exists(file_path)}")
+ logger.warning(f"[Metadata Debug] Artist: {artist.get('name', 'MISSING') if artist else 'None'}")
+ logger.warning(f"[Metadata Debug] Album info: {album_info.get('album_name', 'MISSING') if album_info else 'None'}")
+ logger.error(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:
@@ -19071,14 +19089,14 @@ def _generate_lrc_file(file_path: str, context: dict, artist: dict, album_info:
)
if success:
- print(f"LRC file generated for: {track_name}")
+ logger.info(f"LRC file generated for: {track_name}")
else:
- print(f"No lyrics found for: {track_name}")
+ logger.warning(f"No lyrics found for: {track_name}")
return success
except Exception as e:
- print(f"Error generating LRC file for {file_path}: {e}")
+ logger.error(f"Error generating LRC file for {file_path}: {e}")
return False
def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> dict:
@@ -19092,15 +19110,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']}'")
+ logger.info(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']}'")
+ logger.info(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']}'")
+ logger.warning(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:
@@ -19112,29 +19130,12 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
all_artists.append(a)
else:
all_artists.append(str(a))
-
- # Configurable artist separator (default: comma-space)
- _artist_sep = config_manager.get('metadata_enhancement.tags.artist_separator', ', ') or ', '
- _feat_in_title = config_manager.get('metadata_enhancement.tags.feat_in_title', False)
-
- # Featured artist in title mode: keep only primary artist, append rest to title
- if _feat_in_title and len(all_artists) > 1:
- metadata['artist'] = all_artists[0]
- _feat_str = ', '.join(all_artists[1:])
- _title = metadata.get('title', '')
- if _title and not re.search(r'\b(feat\.?|ft\.?|featuring)\b', _title, re.IGNORECASE):
- metadata['title'] = f"{_title} (feat. {_feat_str})"
- else:
- metadata['artist'] = _artist_sep.join(all_artists)
-
- # Store raw artist list for multi-value tag writing
- metadata['_artists_list'] = all_artists
- print(f"Metadata: Using all artists: '{metadata['artist']}'")
+ metadata['artist'] = ', '.join(all_artists)
+ logger.info(f"Metadata: Using all artists: '{metadata['artist']}'")
else:
# Fallback to single artist
metadata['artist'] = artist.get('name', '')
- metadata['_artists_list'] = [metadata['artist']] if metadata['artist'] else []
- print(f"Metadata: Using primary artist: '{metadata['artist']}'")
+ logger.info(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)
@@ -19194,18 +19195,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']}")
+ logger.info(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")
+ logger.info(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}")
+ logger.info(f"[SAFEGUARD] Using track_number: {corrected_track_number}")
else:
metadata['album'] = metadata['title'] # For true singles, album is the title
metadata['track_number'] = 1
@@ -19266,7 +19267,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')}")
+ logger.info(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
@@ -19313,7 +19314,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)")
+ logger.info(f"Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
else:
image_data = None # Too small, likely an error page
except Exception:
@@ -19323,14 +19324,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.")
+ logger.warning("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.")
+ logger.error("Failed to download album art data.")
return
# MP3 (ID3)
@@ -19353,9 +19354,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.")
+ logger.info("Album art successfully embedded.")
except Exception as e:
- print(f"Error embedding album art: {e}")
+ logger.error(f"Error embedding album art: {e}")
def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
"""
@@ -19559,13 +19560,13 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
""", (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")
+ logger.info(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}")
+ logger.error(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
@@ -19637,10 +19638,10 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
break
except (ValueError, TypeError):
pass
- print(f"MusicBrainz release details: type={primary_type or '?'}, "
+ logger.info(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}")
+ logger.error(f"MusicBrainz release detail lookup failed (non-fatal): {e}")
# āā 2b. Deezer lookup for BPM, ISRC fallback, and source IDs āā
deezer_bpm = None
@@ -19659,7 +19660,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(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)
@@ -19671,9 +19672,9 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
if dz_isrc:
deezer_isrc = dz_isrc
else:
- print("Deezer worker not available, skipping Deezer lookup")
+ logger.info("Deezer worker not available, skipping Deezer lookup")
except Exception as e:
- print(f"Deezer lookup failed (non-fatal): {e}")
+ logger.error(f"Deezer lookup failed (non-fatal): {e}")
# āā 2c. AudioDB lookup for mood, style, genre, and source ID āā
audiodb_mood = None
@@ -19691,13 +19692,13 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(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}")
+ logger.warning(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,
@@ -19708,14 +19709,14 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.warning(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")
+ logger.info("AudioDB worker not available, skipping AudioDB lookup")
except Exception as e:
- print(f"AudioDB lookup failed (non-fatal): {e}")
+ logger.error(f"AudioDB lookup failed (non-fatal): {e}")
# āā 2d. Tidal lookup for ISRC fallback, copyright, and source IDs āā
tidal_isrc = None
@@ -19730,7 +19731,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(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'])
@@ -19747,7 +19748,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
if td_copyright:
tidal_copyright = td_copyright
except Exception as e:
- print(f"Tidal lookup failed (non-fatal): {e}")
+ logger.error(f"Tidal lookup failed (non-fatal): {e}")
# āā 2e. Qobuz lookup for ISRC fallback, copyright, label, and source IDs āā
qobuz_isrc = None
@@ -19770,7 +19771,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(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')
@@ -19789,7 +19790,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.error(f"Qobuz lookup failed (non-fatal): {e}")
# āā 2f. Last.fm lookup for tags (genre merge) and URL āā
lastfm_tags = []
@@ -19812,9 +19813,9 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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")
+ logger.info(f"Last.fm track info found: {len(lastfm_tags)} tags")
except Exception as e:
- print(f"Last.fm lookup failed (non-fatal): {e}")
+ logger.error(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
@@ -19828,7 +19829,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
try:
import core.genius_client as _genius_module
if time.time() < _genius_module._rate_limit_until:
- print("Genius rate-limited, skipping (non-blocking)")
+ logger.info("Genius rate-limited, skipping (non-blocking)")
else:
g_client = genius_worker.client if genius_worker else None
if g_client:
@@ -19837,12 +19838,12 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
g_id = g_result.get('id')
if g_id:
id_tags['GENIUS_TRACK_ID'] = str(g_id)
- print(f"Genius song matched: {g_id}")
+ logger.info(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}")
+ logger.error(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
@@ -19930,7 +19931,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
written.append(key)
if written:
- print(f"Embedded IDs: {', '.join(written)}")
+ logger.info(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:
@@ -19940,7 +19941,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
audio_file['date'] = [release_year]
elif isinstance(audio_file, MP4):
audio_file['\xa9day'] = [release_year]
- print(f"Date tag: {release_year}")
+ logger.info(f"Date tag: {release_year}")
# āā 3b. Write BPM tag (from Deezer) āā
if _tag_enabled('deezer.tags.bpm') and deezer_bpm and deezer_bpm > 0:
@@ -19951,7 +19952,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
audio_file['BPM'] = [str(bpm_int)]
elif isinstance(audio_file, MP4):
audio_file['tmpo'] = [bpm_int]
- print(f"BPM: {bpm_int}")
+ logger.info(f"BPM: {bpm_int}")
# āā 3c. Write mood tag (from AudioDB) āā
if _tag_enabled('audiodb.tags.mood') and audiodb_mood:
@@ -19961,7 +19962,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(f"Mood: {audiodb_mood}")
# āā 3d. Write style tag (from AudioDB) āā
if _tag_enabled('audiodb.tags.style') and audiodb_style:
@@ -19971,7 +19972,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(f"Style: {audiodb_style}")
# āā 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag āā
if _tag_enabled('metadata_enhancement.tags.genre_merge'):
@@ -20000,7 +20001,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
audio_file['GENRE'] = [genre_string]
elif isinstance(audio_file, MP4):
audio_file['\xa9gen'] = [genre_string]
- print(f"Genres merged: {genre_string}")
+ logger.info(f"Genres merged: {genre_string}")
# āā 5. Write ISRC if available (per-source fallback chain) āā
_isrc_candidates = []
@@ -20020,7 +20021,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(f"ISRC ({source}): {final_isrc}")
# āā 6. Write copyright tag (Tidal ā Qobuz fallback) āā
_copyright_candidates = []
@@ -20036,7 +20037,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
audio_file['COPYRIGHT'] = [final_copyright]
elif isinstance(audio_file, MP4):
audio_file['cprt'] = [final_copyright]
- print(f"Ā©ļø Copyright ({source}): {final_copyright[:60]}")
+ logger.info(f"Ā©ļø Copyright ({source}): {final_copyright[:60]}")
# āā 7. Write label/publisher tag (from Qobuz) āā
if _tag_enabled('qobuz.tags.label') and qobuz_label:
@@ -20046,7 +20047,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.info(f"Label (Qobuz): {qobuz_label}")
# āā 8. Write Last.fm and Genius URLs as custom tags āā
if _tag_enabled('lastfm.tags.url') and lastfm_url:
@@ -20066,7 +20067,7 @@ def _embed_source_ids(audio_file, metadata: dict, context: dict = None):
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}")
+ logger.error(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.
@@ -20091,7 +20092,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...")
+ logger.info(f"Existing cover.jpg is {existing_size // 1024}KB ā attempting CAA upgrade...")
except Exception:
return
else:
@@ -20110,7 +20111,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)")
+ logger.info(f"Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
else:
image_data = None
except Exception:
@@ -20118,7 +20119,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")
+ logger.error(f"CAA upgrade failed ā keeping existing cover.jpg")
return
# Fallback to Spotify/iTunes/Deezer URL
@@ -20134,7 +20135,7 @@ 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")
+ logger.info(f"Using cover art URL from spotify_album context")
# Upgrade to highest available resolution before fetching
if art_url and 'i.scdn.co' in art_url:
from core.spotify_client import _upgrade_spotify_image_url
@@ -20143,7 +20144,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
import re as _re
art_url = _re.sub(r'\d+x\d+bb', '3000x3000bb', art_url)
if not art_url:
- print("No cover art URL available for download.")
+ logger.warning("No cover art URL available for download.")
return
with urllib.request.urlopen(art_url, timeout=10) as response:
image_data = response.read()
@@ -20154,9 +20155,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}")
+ logger.info(f"Cover art downloaded to: {cover_path}")
except Exception as e:
- print(f"Error downloading cover.jpg: {e}")
+ logger.error(f"Error downloading cover.jpg: {e}")
@@ -20177,7 +20178,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}")
+ logger.error(f"Error fetching Spotify album tracks: {e}")
return []
def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -> dict:
@@ -20193,7 +20194,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']}'")
+ logger.info(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'],
@@ -20217,7 +20218,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']}'")
+ logger.info(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'),
@@ -20227,7 +20228,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.")
+ logger.error(f"Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.")
return slsk_track_meta # Fallback to original
@@ -20250,13 +20251,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")
+ logger.info("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']}")
+ logger.info(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', [])
@@ -20361,13 +20362,13 @@ def _pp_lookup_musicbrainz(pp, _names_match):
if _release_recording.get('id'):
pp['recording_mbid'] = _release_recording['id']
id_tags['MUSICBRAINZ_RECORDING_ID'] = _release_recording['id']
- print(f"MusicBrainz recording from release tracklist: {_release_recording['id']}")
+ logger.info(f"MusicBrainz recording from release tracklist: {_release_recording['id']}")
break
break
except (ValueError, TypeError):
pass
except Exception as e:
- print(f"MusicBrainz lookup failed (non-fatal): {e}")
+ logger.error(f"MusicBrainz lookup failed (non-fatal): {e}")
def _pp_lookup_deezer(pp, _names_match):
@@ -20381,7 +20382,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")
+ logger.info("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 \
@@ -20391,7 +20392,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}")
+ logger.info(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')
@@ -20407,7 +20408,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}")
+ logger.error(f"Deezer lookup failed (non-fatal): {e}")
def _pp_lookup_audiodb(pp, _names_match):
@@ -20421,7 +20422,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")
+ logger.info("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 \
@@ -20429,22 +20430,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}")
+ logger.info(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}")
+ logger.warning(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}")
+ logger.warning(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}")
+ logger.error(f"AudioDB lookup failed (non-fatal): {e}")
def _pp_lookup_tidal(pp, _names_match):
@@ -20463,7 +20464,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}")
+ logger.info(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'])
@@ -20487,7 +20488,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}")
+ logger.error(f"Tidal lookup failed (non-fatal): {e}")
def _pp_lookup_qobuz(pp, _names_match):
@@ -20513,7 +20514,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}")
+ logger.info(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')
@@ -20543,7 +20544,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}")
+ logger.error(f"Qobuz lookup failed (non-fatal): {e}")
def _pp_lookup_lastfm(pp, _names_match):
@@ -20569,9 +20570,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")
+ logger.info(f"Last.fm track info found: {len(pp['lastfm_tags'])} tags")
except Exception as e:
- print(f"Last.fm lookup failed (non-fatal): {e}")
+ logger.error(f"Last.fm lookup failed (non-fatal): {e}")
def _pp_lookup_genius(pp, _names_match):
@@ -20585,7 +20586,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)")
+ logger.info("Genius rate-limited, skipping (non-blocking)")
return
g_client = genius_worker.client if genius_worker else None
if not g_client:
@@ -20595,12 +20596,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}")
+ logger.info(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}")
+ logger.error(f"Genius lookup failed (non-fatal): {e}")
def _post_process_matched_download_with_verification(context_key, context, file_path, task_id, batch_id):
@@ -20804,16 +20805,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}")
+ logger.info(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}")
+ logger.warning(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}")
+ logger.info(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:
@@ -21050,9 +21051,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)}")
+ logger.info(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)}")
+ logger.error(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 ---
@@ -21073,10 +21074,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)")
+ logger.info(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)")
+ logger.info(f"File may still be writing after stability checks: {_basename} ({_prev_size} bytes)")
# --- END FILE STABILITY CHECK ---
# --- ACOUSTID VERIFICATION ---
@@ -21120,26 +21121,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}'")
+ logger.info(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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"Quarantine failed, deleting wrong file: {file_path}")
try:
os.remove(file_path)
except Exception as del_error:
@@ -21169,14 +21170,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")
+ logger.warning(f"AcoustID verification skipped: missing track/artist info")
context['_acoustid_result'] = 'skip'
else:
- print(f"ā¹ļø AcoustID verification not available: {available_reason}")
+ logger.info(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}")
+ logger.error(f"AcoustID verification error (continuing normally): {verify_error}")
context['_acoustid_result'] = 'error'
# --- END ACOUSTID VERIFICATION ---
@@ -21249,11 +21250,11 @@ def _post_process_matched_download(context_key, context, file_path):
return
# --- END SIMPLE DOWNLOAD HANDLING ---
- print(f"Starting robust post-processing for: {context_key}")
+ logger.info(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.")
+ logger.error(f"Post-processing failed: Missing spotify_artist context.")
return
# āā UNKNOWN ARTIST GUARD āā
@@ -21262,7 +21263,7 @@ def _post_process_matched_download(context_key, context, file_path):
_junk_artist_names = {'', 'unknown', 'unknown artist', 'various artists', 'none', 'null'}
_artist_name = (spotify_artist.get('name', '') if isinstance(spotify_artist, dict) else '').strip()
if _artist_name.lower() in _junk_artist_names:
- print(f"[Unknown Artist Guard] Artist name is '{_artist_name}' ā attempting to resolve")
+ logger.info(f"[Unknown Artist Guard] Artist name is '{_artist_name}' ā attempting to resolve")
_resolved = False
track_info_guard = context.get("track_info", {}) or {}
original_search_guard = context.get("original_search_result", {}) or {}
@@ -21274,7 +21275,7 @@ def _post_process_matched_download(context_key, context, file_path):
_name = _first.get('name', '') if isinstance(_first, dict) else str(_first)
if _name and _name.strip().lower() not in _junk_artist_names:
spotify_artist['name'] = _name.strip()
- print(f"[Unknown Artist Guard] Resolved from track_info.artists: '{_name}'")
+ logger.info(f"[Unknown Artist Guard] Resolved from track_info.artists: '{_name}'")
_resolved = True
# Try 2: Pull from original_search_result
@@ -21282,7 +21283,7 @@ def _post_process_matched_download(context_key, context, file_path):
_os_artist = original_search_guard.get('artist') or original_search_guard.get('artist_name') or ''
if isinstance(_os_artist, str) and _os_artist.strip().lower() not in _junk_artist_names:
spotify_artist['name'] = _os_artist.strip()
- print(f"[Unknown Artist Guard] Resolved from original_search_result: '{_os_artist}'")
+ logger.info(f"[Unknown Artist Guard] Resolved from original_search_result: '{_os_artist}'")
_resolved = True
# Try 3: Re-fetch from metadata source using track ID
@@ -21300,13 +21301,13 @@ def _post_process_matched_download(context_key, context, file_path):
_d_name = _d_first.get('name', '') if isinstance(_d_first, dict) else str(_d_first)
if _d_name and _d_name.strip().lower() not in _junk_artist_names:
spotify_artist['name'] = _d_name.strip()
- print(f"[Unknown Artist Guard] Resolved from metadata API: '{_d_name}'")
+ logger.info(f"[Unknown Artist Guard] Resolved from metadata API: '{_d_name}'")
_resolved = True
except Exception as _guard_err:
- print(f"[Unknown Artist Guard] Metadata re-fetch failed: {_guard_err}")
+ logger.error(f"[Unknown Artist Guard] Metadata re-fetch failed: {_guard_err}")
if not _resolved:
- print(f"[Unknown Artist Guard] Could not resolve artist ā proceeding with '{_artist_name}'")
+ logger.error(f"[Unknown Artist Guard] Could not resolve artist ā proceeding with '{_artist_name}'")
context['spotify_artist'] = spotify_artist
# āā END UNKNOWN ARTIST GUARD āā
@@ -21315,28 +21316,28 @@ def _post_process_matched_download(context_key, context, file_path):
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}")
+ logger.debug(f"[Debug] Post-processing - track_info type: {type(track_info)}, is None: {track_info is None}, is empty: {not track_info}")
+ logger.debug(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())}")
+ logger.debug(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}")
+ logger.info(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}'")
+ logger.info(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)}")
+ logger.info(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:
@@ -21345,7 +21346,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']}")
+ logger.info(f"Audio quality detected: {context['_audio_quality']}")
# FLAC bit depth filter
if _check_flac_bit_depth(file_path, context, context_key):
@@ -21353,7 +21354,7 @@ 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')})")
+ logger.warning(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
@@ -21361,7 +21362,7 @@ def _post_process_matched_download(context_key, context, file_path):
_wipe_source_tags(file_path)
# Move file to playlist folder
- print(f"Moving '{os.path.basename(file_path)}' to '{final_path}'")
+ logger.info(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)
@@ -21394,13 +21395,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}")
+ logger.info(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}")
+ logger.error(f"[Playlist Folder] Error checking wishlist removal: {wishlist_error}")
_emit_track_downloaded(context)
_record_library_history_download(context)
@@ -21415,7 +21416,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")
+ logger.info(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
@@ -21425,7 +21426,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")
+ logger.info("Album context with clean Spotify data found - using direct album info")
original_search = context.get("original_search_result", {})
spotify_album = context.get("spotify_album", {})
@@ -21433,11 +21434,12 @@ def _post_process_matched_download(context_key, context, file_path):
clean_track_name = original_search.get('spotify_clean_title', 'Unknown Track')
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" 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')}")
+ logger.debug(
+ "Path 1 - Clean Spotify data path: keys=%s has_track_number=%s track_number=%s",
+ list(original_search.keys()),
+ 'track_number' in original_search,
+ original_search.get('track_number', 'NOT_FOUND'),
+ )
album_info = {
'is_album': True,
@@ -21450,20 +21452,21 @@ 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}'")
+ logger.info(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")
+ logger.warning("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" 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')}")
- print(f" spotify_album name: {spotify_album.get('name', 'NOT_FOUND')}")
+ logger.debug(
+ "Path 2 - Enhanced fallback album context path: keys=%s has_track_number=%s track_number=%s spotify_album=%s",
+ list(original_search.keys()),
+ 'track_number' in original_search,
+ original_search.get('track_number', 'NOT_FOUND'),
+ spotify_album.get('name', 'NOT_FOUND'),
+ )
# ENHANCEMENT: Use spotify_clean_album if available for consistency
album_name = (original_search.get('spotify_clean_album') or
@@ -21480,10 +21483,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}'")
+ logger.info(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")
+ logger.info("Single track download - attempting album detection")
album_info = _detect_album_info_web(context, spotify_artist)
# --- Album grouping resolution ---
@@ -21491,8 +21494,11 @@ 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"\nSMART ALBUM GROUPING for track: '{album_info.get('clean_track_name', 'Unknown')}'")
- print(f" Original album: '{album_info.get('album_name', 'None')}'")
+ logger.info(
+ "SMART ALBUM GROUPING for track=%r original_album=%r",
+ album_info.get('clean_track_name', 'Unknown'),
+ album_info.get('album_name', 'None'),
+ )
# Get original album name from context if available
original_album = None
@@ -21503,17 +21509,18 @@ def _post_process_matched_download(context_key, context, file_path):
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")
+ logger.info("Album grouping complete: final_album=%r", consistent_album_name)
elif album_info and album_info['is_album'] and is_album_download:
- 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")
+ logger.info(
+ "EXPLICIT ALBUM DOWNLOAD - preserving Spotify album name=%r; skipping smart grouping",
+ album_info.get('album_name', 'None'),
+ )
# 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']}")
+ logger.info(f"Audio quality detected: {context['_audio_quality']}")
# FLAC bit depth filter
if _check_flac_bit_depth(file_path, context, context_key):
@@ -21530,46 +21537,50 @@ 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}'")
+ logger.info(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}'")
+ logger.info(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}'")
+ logger.warning(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" 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}")
+ logger.debug(
+ "Final track_number processing: source=%s album_info_track_number=%s track_number=%s",
+ album_info.get('source', 'unknown'),
+ album_info.get('track_number', 'NOT_FOUND'),
+ 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)}")
track_number = _extract_track_number_from_filename(file_path)
- print(f" -> Extracted track number: {track_number}")
+ logger.info(
+ "Track number was None; extracted from filename=%r -> %s",
+ os.path.basename(file_path),
+ 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")
+ logger.error(f"Invalid track number ({track_number}), defaulting to 1")
track_number = 1
- print(f"[DEBUG] FINAL track_number used for filename: {track_number}")
+ logger.debug(f"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")
+ logger.info(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}'")
+ logger.info(f"Album path: '{final_path}'")
else:
# Single track structure: Transfer/ARTIST/ARTIST - SINGLE/SINGLE.ext
# --- GUI PARITY: Use multiple sources for clean track name ---
@@ -21579,15 +21590,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}'")
+ logger.info(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}'")
+ logger.info(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}'")
+ logger.warning(f"Using original title as fallback: '{clean_track_name}'")
# Ensure clean name is in album_info for path builder
if album_info:
@@ -21595,7 +21606,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}'")
+ logger.info(f"Single path: '{final_path}'")
# Store the actual computed path so verification uses this exact path
# instead of recomputing independently (which can produce mismatches)
@@ -21603,11 +21614,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')})")
+ logger.warning(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')}")
+ logger.warning(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)")
+ logger.info(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
@@ -21625,12 +21636,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}'")
+ logger.info(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)}")
+ logger.info(f"[Protection] Destination exists and source already gone - file already transferred: {os.path.basename(final_path)}")
return
try:
from mutagen import File as MutagenFile
@@ -21645,50 +21656,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)}")
+ logger.info(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}")
+ logger.error(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)}")
+ logger.info(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}")
+ logger.error(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)}")
+ logger.info(f"[Protection] Existing file already has metadata enhancement - skipping overwrite: {os.path.basename(final_path)}")
+ logger.info(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}")
+ logger.error(f"[Protection] Could not remove redundant file (already gone): {file_path}")
except Exception as e:
- print(f"[Protection] Error removing redundant file: {e}")
+ logger.error(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)}")
+ logger.info(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}")
+ logger.error(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)}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"[Protection] Failed to remove existing file for overwrite: {e}")
# --- PRE-MOVE SOURCE CHECK ---
# Right before moving, verify the source file still exists.
@@ -21696,7 +21707,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)}")
+ logger.info(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)
@@ -21724,13 +21735,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)}")
+ logger.debug(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)}")
+ logger.warning(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)
@@ -21743,13 +21754,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)}")
+ logger.info(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}")
+ logger.error(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)}")
+ logger.info(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)
@@ -21782,7 +21793,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)}")
+ logger.info(f"Post-processing complete for: {context.get('_final_processed_path', final_path)}")
_emit_track_downloaded(context)
_record_library_history_download(context)
@@ -21795,7 +21806,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}")
+ logger.error(f"[Post-Process] Retag data capture failed (non-fatal): {retag_err}")
# REPAIR: Register album folder for repair scanning when batch completes
try:
@@ -21806,7 +21817,7 @@ 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}")
+ logger.error(f"[Post-Process] Repair folder registration failed: {repair_err}")
# ALBUM CONSISTENCY: Register completed file for post-batch MB tag reconciliation
try:
@@ -21824,19 +21835,19 @@ def _post_process_matched_download(context_key, context, file_path):
if batch_id_for_consistency in download_batches:
download_batches[batch_id_for_consistency].setdefault('_consistency_files', []).append(_file_info)
except Exception as cons_err:
- print(f"[Post-Process] Album consistency registration failed: {cons_err}")
+ logger.error(f"[Post-Process] Album consistency registration failed: {cons_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}")
+ logger.error(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}")
+ logger.info(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
@@ -21848,7 +21859,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")
+ logger.info(f"[Post-Process] Marked task {task_id} as completed")
_on_download_completed(batch_id, task_id, success=True)
@@ -21856,7 +21867,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"\nCRITICAL ERROR in post-processing for {context_key}: {e}")
+ logger.error(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
@@ -21867,15 +21878,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")
+ logger.warning(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")
+ logger.warning(f"Re-added {context_key} to context for retry")
else:
- print(f"Source file gone, not retrying: {context_key}")
+ logger.warning(f"Source file gone, not retrying: {context_key}")
finally:
file_lock.release()
# Clean up the lock entry to prevent unbounded memory growth
@@ -21973,7 +21984,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}'")
+ logger.info(f"[Retag] Recorded track for retag: '{title}' in '{album_name}'")
# Cap retag groups at 100, remove oldest
db.trim_retag_groups(100)
@@ -22072,7 +22083,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')}'")
+ logger.warning(f"[Retag] No match found for track: '{existing_track.get('title')}'")
matched_pairs.append((existing_track, None))
with retag_lock:
@@ -22094,7 +22105,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}")
+ logger.warning(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)
@@ -22136,9 +22147,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}'")
+ logger.info(f"[Retag] Re-tagged: '{track_title}'")
except Exception as meta_err:
- print(f"[Retag] Metadata write failed for '{track_title}': {meta_err}")
+ logger.error(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]
@@ -22146,7 +22157,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}'")
+ logger.info(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)
@@ -22158,9 +22169,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")
+ logger.info(f"[Retag] Moved {lyrics_ext} file alongside audio")
except Exception as lrc_err:
- print(f"[Retag] Failed to move {lyrics_ext} file: {lrc_err}")
+ logger.error(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)
@@ -22174,7 +22185,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")
+ logger.warning(f"[Retag] Removed orphaned cover.jpg from old directory")
except Exception:
pass
@@ -22186,15 +22197,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")
+ logger.warning(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}")
+ logger.error(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}")
+ logger.error(f"[Retag] Cover art download failed: {cover_err}")
with retag_lock:
retag_state['processed'] += 1
@@ -22225,12 +22236,12 @@ def _execute_retag(group_id, album_id):
"progress": 100,
"current_track": ""
})
- print(f"[Retag] Retag operation complete for group {group_id}")
+ logger.info(f"[Retag] Retag operation complete for group {group_id}")
except Exception as e:
import traceback
- print(f"[Retag] Error during retag: {e}")
- print(traceback.format_exc())
+ logger.error(f"[Retag] Error during retag: {e}")
+ logger.error(traceback.format_exc())
with retag_lock:
retag_state.update({
"status": "error",
@@ -22255,17 +22266,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}")
+ logger.info(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}")
+ logger.info(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}")
+ logger.info(f"[Wishlist] Found wishlist_id in context: {wishlist_id}")
# Get the Spotify track ID from the wishlist entry (search all profiles)
database = get_database()
@@ -22276,7 +22287,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}")
+ logger.info(f"[Wishlist] Found Spotify ID from wishlist entry: {spotify_track_id}")
break
# Method 4: Try to construct ID from track metadata for fuzzy matching
@@ -22285,7 +22296,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}'")
+ logger.warning(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:
@@ -22309,22 +22320,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}")
+ logger.info(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}")
+ logger.info(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}")
+ logger.info(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}")
+ logger.warning(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")
+ logger.warning(f"ā¹ļø [Wishlist] No Spotify track ID found for wishlist removal check")
except Exception as e:
- print(f"[Wishlist] Error in wishlist removal check: {e}")
+ logger.error(f"[Wishlist] Error in wishlist removal check: {e}")
import traceback
traceback.print_exc()
@@ -22342,13 +22353,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})")
+ logger.info(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}")
+ logger.info(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
@@ -22362,7 +22373,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}'")
+ logger.warning(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()
@@ -22388,14 +22399,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}")
+ logger.info(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}'")
+ logger.warning(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}")
+ logger.error(f"[Analysis] Error checking wishlist removal by metadata: {e}")
import traceback
traceback.print_exc()
return False
@@ -22413,7 +22424,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...")
+ logger.info("[Auto Cleanup] Starting automatic wishlist cleanup after database update...")
# Get all wishlist tracks (across all profiles - cleanup is global)
database = get_database()
@@ -22422,10 +22433,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")
+ logger.warning("[Auto Cleanup] No tracks in wishlist to clean up")
return
- print(f"[Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
+ logger.info(f"[Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
removed_count = 0
@@ -22460,11 +22471,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})")
+ logger.info(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}")
+ logger.error(f"[Auto Cleanup] Error checking database for track '{track_name}': {db_error}")
continue
# If found in database, remove from wishlist
@@ -22473,14 +22484,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})")
+ logger.info(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}")
+ logger.error(f"[Auto Cleanup] Error removing track from wishlist: {remove_error}")
- print(f"[Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist")
+ logger.info(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}")
+ logger.error(f"[Auto Cleanup] Error in automatic wishlist cleanup: {e}")
import traceback
traceback.print_exc()
@@ -23977,7 +23988,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")
+ logger.info("Simple background monitor started")
while not globals().get('IS_SHUTTING_DOWN', False):
try:
@@ -23998,15 +24009,15 @@ def _simple_monitor_task():
if current_time - data['first_attempt'] > 60
]
for key in stale_keys:
- print(f"Cleaning up stale retry attempt: {key}")
+ logger.warning(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}")
+ logger.error(f"Simple monitor error: {e}")
time.sleep(10)
- print("Simple background monitor stopped")
+ logger.info("Simple background monitor stopped")
def start_simple_background_monitor():
"""Starts the simple background monitor thread."""
@@ -24024,7 +24035,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)}")
+ logger.info(f"[Sanitize] Unexpected track data type: {type(track_data)}")
return track_data
# Create a copy to avoid modifying original data
@@ -24049,7 +24060,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)}")
+ logger.info(f"[Sanitize] Unexpected artists format: {type(raw_artists)}")
sanitized['artists'] = [str(raw_artists)] if raw_artists else []
return sanitized
@@ -24072,7 +24083,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")
+ logger.info(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
@@ -24084,7 +24095,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")
+ logger.info(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
@@ -24109,7 +24120,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")
+ logger.warning(f"[Stuck Detection] Wishlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering")
check_and_recover_stuck_flags()
return False
@@ -24132,7 +24143,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")
+ logger.warning(f"[Stuck Detection] Watchlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering")
check_and_recover_stuck_flags()
return False
@@ -24179,13 +24190,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...")
+ logger.info("[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.")
+ logger.info("[Auto-Wishlist] Already processing (verified with stuck detection), skipping.")
return
# Check conditions and set flag
@@ -24194,7 +24205,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.")
+ logger.info("[Auto-Wishlist] Already processing (race condition check), skipping.")
should_skip_already_running = True
else:
@@ -24202,7 +24213,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}")
+ logger.info(f"[Auto-Wishlist] Flag set at timestamp {wishlist_auto_processing_timestamp}")
if should_skip_already_running:
return
@@ -24216,17 +24227,17 @@ 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")
+ logger.info(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:
- print("ā¹ļø [Auto-Wishlist] No tracks in wishlist for auto-processing.")
+ logger.warning("ā¹ļø [Auto-Wishlist] No tracks in wishlist for auto-processing.")
with wishlist_timer_lock:
wishlist_auto_processing = False
wishlist_auto_processing_timestamp = 0
return
- print(f"[Auto-Wishlist] Found {count} tracks in wishlist, starting automatic processing...")
+ logger.info(f"[Auto-Wishlist] Found {count} tracks in wishlist, starting automatic processing...")
# Check if wishlist processing is already active (auto or manual)
playlist_id = "wishlist"
@@ -24236,7 +24247,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")
+ logger.info(f"Wishlist processing already active in another batch ({batch_playlist_id}), skipping automatic start")
with wishlist_timer_lock:
wishlist_auto_processing = False
return
@@ -24246,15 +24257,15 @@ def _process_wishlist_automatically(automation_id=None):
from database.music_database import MusicDatabase
db = MusicDatabase()
- print("[Auto-Wishlist] Cleaning duplicate tracks before processing...")
+ logger.warning("[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']}")
+ logger.warning(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...")
+ logger.debug("[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']))
@@ -24299,12 +24310,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}")
+ logger.info(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}")
+ logger.error(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")
+ logger.info(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:
@@ -24316,7 +24327,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.")
+ logger.warning("No tracks returned from wishlist service.")
with wishlist_timer_lock:
wishlist_auto_processing = False
wishlist_auto_processing_timestamp = 0
@@ -24340,8 +24351,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")
+ logger.warning(f"[Auto-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization")
+ logger.info(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:
@@ -24379,14 +24390,14 @@ 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")
+ logger.info(f"[Auto-Wishlist] Current cycle: {current_cycle}")
+ logger.info(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')
# If no tracks in this category, skip to next cycle immediately
if len(filtered_tracks) == 0:
- print(f"ā¹ļø [Auto-Wishlist] No {current_cycle} tracks in wishlist, toggling cycle and scheduling next run")
+ logger.warning(f"ā¹ļø [Auto-Wishlist] No {current_cycle} tracks in wishlist, toggling cycle and scheduling next run")
# Toggle cycle
next_cycle = 'singles' if current_cycle == 'albums' else 'albums'
@@ -24397,7 +24408,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}")
+ logger.info(f"[Auto-Wishlist] Cycle toggled: {current_cycle} ā {next_cycle}")
with wishlist_timer_lock:
wishlist_auto_processing = False
@@ -24440,7 +24451,7 @@ def _process_wishlist_automatically(automation_id=None):
'profile_id': 1
}
- print(f"Starting automatic wishlist batch {batch_id} with {len(wishlist_tracks)} tracks")
+ logger.info(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')
@@ -24450,7 +24461,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}")
+ logger.error(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')
@@ -24465,7 +24476,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}%)")
+ logger.info(f"[DB Progress] {current_item} - {processed}/{total} ({percentage:.1f}%)")
with db_update_lock:
db_update_state.update({
"current_item": current_item,
@@ -24478,7 +24489,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}")
+ logger.info(f"[DB Phase] {phase}")
with db_update_lock:
db_update_state["phase"] = phase
_update_automation_progress(_db_update_automation_id, phase=phase)
@@ -24580,11 +24591,11 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
# WISHLIST CLEANUP: Automatically clean up wishlist after database update
try:
- print("[DB Update] Database update completed, starting automatic wishlist cleanup...")
+ logger.info("[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}")
+ logger.error(f"[DB Update] Error starting automatic wishlist cleanup: {cleanup_error}")
def _db_update_error_callback(error_message):
global _db_update_automation_id
@@ -24618,7 +24629,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)}")
+ logger.warning(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)."""
@@ -24635,7 +24646,7 @@ def _resume_workers_after_scan():
w.resume()
resumed += 1
if resumed:
- print(f"Resumed {resumed} workers after database scan")
+ logger.info(f"Resumed {resumed} workers after database scan")
_workers_paused_by_scan = set()
def _run_soulsync_full_refresh():
@@ -24648,7 +24659,7 @@ def _run_soulsync_full_refresh():
_db_update_error_callback(f"Output folder not found: {transfer_path}")
return
- print(f"[SoulSync Full Refresh] Starting ā clearing soulsync data, re-scanning: {transfer_path}")
+ logger.info(f"[SoulSync Full Refresh] Starting ā clearing soulsync data, re-scanning: {transfer_path}")
_db_update_phase_callback('Clearing library...')
db = get_database()
@@ -24664,7 +24675,7 @@ def _run_soulsync_full_refresh():
audio_files.append(os.path.join(root, fname))
total = len(audio_files)
- print(f"[SoulSync Full Refresh] Found {total} audio files, rebuilding library...")
+ logger.info(f"[SoulSync Full Refresh] Found {total} audio files, rebuilding library...")
if total == 0:
_db_update_finished_callback(0, 0, 0, 0, 0)
return
@@ -24744,11 +24755,11 @@ def _run_soulsync_full_refresh():
successful += 1
except Exception as e:
failed += 1
- print(f"[SoulSync Full Refresh] Track insert error: {e}")
+ logger.error(f"[SoulSync Full Refresh] Track insert error: {e}")
conn.commit()
except Exception as e:
- print(f"[SoulSync Full Refresh] DB error: {e}")
+ logger.error(f"[SoulSync Full Refresh] DB error: {e}")
_db_update_error_callback(f"Database error: {e}")
return
@@ -24757,12 +24768,12 @@ def _run_soulsync_full_refresh():
summary = f"Full refresh complete: {successful} tracks from {album_count} albums by {artist_count} artists"
if failed > 0:
summary += f" ({failed} failed)"
- print(f"[SoulSync Full Refresh] {summary}")
+ logger.info(f"[SoulSync Full Refresh] {summary}")
add_activity_item("", "SoulSync Full Refresh", summary, "Now")
_db_update_finished_callback(artist_count, album_count, total, successful, failed)
except Exception as e:
- print(f"[SoulSync Full Refresh] Error: {e}")
+ logger.error(f"[SoulSync Full Refresh] {e}")
import traceback
traceback.print_exc()
_db_update_error_callback(f"Full refresh failed: {e}")
@@ -24785,7 +24796,7 @@ def _run_soulsync_deep_scan():
_db_update_error_callback(f"Output folder not found: {transfer_path}")
return
- print(f"[SoulSync Deep Scan] Starting ā Transfer: {transfer_path}")
+ logger.info(f"[SoulSync Deep Scan] Starting ā Transfer: {transfer_path}")
_db_update_phase_callback('scanning')
# Phase 1: Collect all audio files in Transfer
@@ -24796,7 +24807,7 @@ def _run_soulsync_deep_scan():
if os.path.splitext(filename)[1].lower() in audio_extensions:
transfer_files.add(os.path.join(root, filename))
- print(f"[SoulSync Deep Scan] Found {len(transfer_files)} audio files in Transfer")
+ logger.info(f"[SoulSync Deep Scan] Found {len(transfer_files)} audio files in Transfer")
# Phase 2: Get all soulsync file paths from DB
db = get_database()
@@ -24809,9 +24820,9 @@ def _run_soulsync_deep_scan():
if row['file_path']:
db_paths.add(row['file_path'])
except Exception as e:
- print(f"[SoulSync Deep Scan] Error reading DB paths: {e}")
+ logger.error(f"[SoulSync Deep Scan] Error reading DB paths: {e}")
- print(f"[SoulSync Deep Scan] {len(db_paths)} tracks in soulsync DB")
+ logger.info(f"[SoulSync Deep Scan] {len(db_paths)} tracks in soulsync DB")
# Phase 3: Find untracked files (in Transfer but not in DB)
untracked = transfer_files - db_paths
@@ -24833,7 +24844,7 @@ def _run_soulsync_deep_scan():
shutil.move(file_path, dest_path)
moved_count += 1
except Exception as e:
- print(f"[SoulSync Deep Scan] Could not move {os.path.basename(file_path)}: {e}")
+ logger.error(f"[SoulSync Deep Scan] Could not move {os.path.basename(file_path)}: {e}")
# Clean up empty directories in Transfer after moving files
for root, dirs, files in os.walk(transfer_path, topdown=False):
@@ -24879,9 +24890,9 @@ def _run_soulsync_deep_scan():
conn.commit()
if orphan_albums > 0 or orphan_artists > 0:
- print(f"[SoulSync Deep Scan] Cleaned up {orphan_albums} orphaned albums, {orphan_artists} orphaned artists")
+ logger.warning(f"[SoulSync Deep Scan] Cleaned up {orphan_albums} orphaned albums, {orphan_artists} orphaned artists")
except Exception as e:
- print(f"[SoulSync Deep Scan] Error cleaning stale records: {e}")
+ logger.error(f"[SoulSync Deep Scan] Error cleaning stale records: {e}")
summary = f"Deep scan complete: {len(transfer_files)} files scanned"
if moved_count > 0:
@@ -24891,12 +24902,12 @@ def _run_soulsync_deep_scan():
if moved_count == 0 and stale_count == 0:
summary += " ā library is clean"
- print(f"[SoulSync Deep Scan] {summary}")
+ logger.info(f"[SoulSync Deep Scan] {summary}")
add_activity_item("", "SoulSync Deep Scan", summary, "Now")
_db_update_finished_callback(0, 0, len(transfer_files), moved_count + stale_count, 0)
except Exception as e:
- print(f"[SoulSync Deep Scan] Error: {e}")
+ logger.error(f"[SoulSync Deep Scan] {e}")
import traceback
traceback.print_exc()
_db_update_error_callback(f"Deep scan failed: {e}")
@@ -24912,7 +24923,7 @@ def _run_db_update_task(full_refresh, server_type):
_run_soulsync_full_refresh()
else:
# Incremental: library updates at download/import time, nothing to do
- print("[SoulSync Standalone] Incremental scan skipped ā library updates at download time. Use Deep Scan or Full Refresh.")
+ logger.warning("[SoulSync Standalone] Incremental scan skipped ā library updates at download time. Use Deep Scan or Full Refresh.")
_db_update_finished_callback(0, 0, 0, 0, 0)
return
@@ -25016,7 +25027,7 @@ def get_database_stats():
stats = db.get_database_info_for_server()
return jsonify(stats)
except Exception as e:
- print(f"Error getting database stats: {e}")
+ logger.error(f"Error getting database stats: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wishlist/process', methods=['POST'])
@@ -25044,7 +25055,7 @@ def get_wishlist_count():
count = wishlist_service.get_wishlist_count(profile_id=get_current_profile_id())
return jsonify({"count": count})
except Exception as e:
- print(f"Error getting wishlist count: {e}")
+ logger.error(f"Error getting wishlist count: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wishlist/stats', methods=['GET'])
@@ -25113,7 +25124,7 @@ def get_wishlist_stats():
})
except Exception as e:
- print(f"Error getting wishlist stats: {e}")
+ logger.error(f"Error getting wishlist stats: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -25150,7 +25161,7 @@ def get_wishlist_cycle():
return jsonify({"cycle": cycle})
except Exception as e:
- print(f"Error getting wishlist cycle: {e}")
+ logger.error(f"Error getting wishlist cycle: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wishlist/cycle', methods=['POST'])
@@ -25180,11 +25191,11 @@ def set_wishlist_cycle():
""", (cycle,))
conn.commit()
- print(f"Wishlist cycle set to: {cycle}")
+ logger.info(f"Wishlist cycle set to: {cycle}")
return jsonify({"success": True, "cycle": cycle})
except Exception as e:
- print(f"Error setting wishlist cycle: {e}")
+ logger.error(f"Error setting wishlist cycle: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/discovery/lookback-period', methods=['GET'])
@@ -25219,7 +25230,7 @@ def get_discovery_lookback_period():
return jsonify({"period": period})
except Exception as e:
- print(f"Error getting discovery lookback period: {e}")
+ logger.error(f"Error getting discovery lookback period: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/discovery/lookback-period', methods=['POST'])
@@ -25267,11 +25278,11 @@ def set_discovery_lookback_period():
conn.commit()
- print(f"Discovery lookback period set to: {period}")
+ logger.info(f"Discovery lookback period set to: {period}")
return jsonify({"success": True, "period": period})
except Exception as e:
- print(f"Error setting discovery lookback period: {e}")
+ logger.error(f"Error setting discovery lookback period: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/discovery/hemisphere', methods=['GET'])
@@ -25347,9 +25358,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")
+ logger.warning(f"Cleaned {duplicates_removed} duplicate tracks from wishlist")
else:
- print(f"Skipping wishlist duplicate cleanup - download in progress")
+ logger.warning(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())
@@ -25372,7 +25383,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")
+ logger.warning(f"[API-Wishlist-Tracks] Found and removed {duplicates_found} duplicate tracks during sanitization")
# FILTER by category if specified
if category:
@@ -25401,7 +25412,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'})")
+ logger.info(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
@@ -25409,7 +25420,7 @@ def get_wishlist_tracks():
result_tracks = sanitized_tracks[:limit] if limit else sanitized_tracks
return jsonify({"tracks": result_tracks, "total": total_count})
except Exception as e:
- print(f"Error getting wishlist tracks: {e}")
+ logger.error(f"Error getting wishlist tracks: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wishlist/download_missing', methods=['POST'])
@@ -25441,16 +25452,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...")
+ logger.warning("[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")
+ logger.warning(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...")
+ logger.info("[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
@@ -25497,12 +25508,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}")
+ logger.info(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}")
+ logger.error(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")
+ logger.info(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)
@@ -25527,8 +25538,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")
+ logger.warning(f"[Manual-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization")
+ logger.info(f"[Manual-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service")
# FILTER BY TRACK IDs if specified (prioritized - prevents race conditions)
if track_ids:
@@ -25552,7 +25563,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)")
+ logger.info(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:
@@ -25603,7 +25614,7 @@ 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}")
+ logger.info(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):
@@ -25650,7 +25661,7 @@ def start_wishlist_missing_downloads():
})
except Exception as e:
- print(f"Error starting wishlist download process: {e}")
+ logger.error(f"Error starting wishlist download process: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/wishlist/clear', methods=['POST'])
@@ -25681,7 +25692,7 @@ def clear_wishlist():
wishlist_auto_processing_timestamp = 0
if cancelled_count > 0:
- print(f"[Wishlist Clear] Cancelled {cancelled_count} active wishlist downloads")
+ logger.warning(f"[Wishlist Clear] Cancelled {cancelled_count} active wishlist downloads")
add_activity_item("", "Wishlist Cleared", f"Wishlist cleared and {cancelled_count} downloads cancelled", "Now")
return jsonify({"success": True, "message": "Wishlist cleared successfully", "cancelled_downloads": cancelled_count})
@@ -25689,7 +25700,7 @@ def clear_wishlist():
return jsonify({"success": False, "error": "Failed to clear wishlist"}), 500
except Exception as e:
- print(f"Error clearing wishlist: {e}")
+ logger.error(f"Error clearing wishlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/wishlist/cleanup', methods=['POST'])
@@ -25703,7 +25714,7 @@ def cleanup_wishlist():
db = MusicDatabase()
active_server = config_manager.get_active_media_server()
- print("[Wishlist Cleanup] Starting wishlist cleanup process...")
+ logger.info("[Wishlist Cleanup] Starting wishlist cleanup process...")
# Get wishlist tracks for current profile
cleanup_profile_id = get_current_profile_id()
@@ -25711,7 +25722,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")
+ logger.info(f"[Wishlist Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
removed_count = 0
processed_count = 0
@@ -25727,7 +25738,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}'")
+ logger.info(f"[Wishlist Cleanup] Checking track {processed_count}/{len(wishlist_tracks)}: '{track_name}'")
# Check each artist
found_in_db = False
@@ -25750,11 +25761,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})")
+ logger.info(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}")
+ logger.error(f"[Wishlist Cleanup] Error checking database for track '{track_name}': {db_error}")
continue
# If found in database, remove from wishlist
@@ -25763,13 +25774,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})")
+ logger.info(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})")
+ logger.error(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}")
+ logger.error(f"[Wishlist Cleanup] Error removing track from wishlist: {remove_error}")
- print(f"[Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist")
+ logger.info(f"[Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist")
return jsonify({
"success": True,
@@ -25779,7 +25790,7 @@ def cleanup_wishlist():
})
except Exception as e:
- print(f"Error in wishlist cleanup: {e}")
+ logger.error(f"Error in wishlist cleanup: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -26002,20 +26013,20 @@ def add_album_track_to_wishlist():
)
if success:
- print(f"Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist")
+ logger.info(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")
+ logger.error(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}")
+ logger.error(f"Error adding track to wishlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -26058,7 +26069,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']}")
+ logger.info(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'])
@@ -26789,7 +26800,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}")
+ logger.info(f"[Quality Scanner] Starting scan with scope: {scope}")
# Get database instance
db = MusicDatabase()
@@ -26814,7 +26825,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}")
+ logger.info(f"[Quality Scanner] Minimum acceptable tier: {min_acceptable_tier}")
# Get tracks to scan based on scope
with quality_scanner_lock:
@@ -26828,12 +26839,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")
+ logger.warning(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")
+ logger.info(f"[Quality Scanner] Scanning {len(artist_names)} watchlist artists")
# Get all tracks for these artists by name
conn = db._get_connection()
@@ -26863,7 +26874,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")
+ logger.info(f"[Quality Scanner] Found {total_tracks} tracks to scan")
with quality_scanner_lock:
quality_scanner_state["total"] = total_tracks
@@ -26875,7 +26886,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")
+ logger.info(f"[Quality Scanner] Spotify not authenticated")
return
wishlist_service = get_wishlist_service()
@@ -26884,7 +26895,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}")
+ logger.info(f"[Quality Scanner] Stop requested, halting at track {idx}/{total_tracks}")
break
try:
@@ -26910,7 +26921,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})")
+ logger.info(f"[Quality Scanner] Low quality: {artist_name} - {title} ({tier_name}, {file_path})")
# Attempt to match to Spotify using matching_engine
matched = False
@@ -26925,7 +26936,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}")
+ logger.info(f"[Quality Scanner] Generated {len(search_queries)} search queries for {artist_name} - {title}")
# Find best match using confidence scoring
best_match = None
@@ -26969,31 +26980,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})")
+ logger.info(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})")
+ logger.info(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}")
+ logger.error(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")
+ logger.info(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}")
+ logger.debug(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})")
+ logger.info(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 = {
@@ -27033,14 +27044,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}")
+ logger.info(f"[Quality Scanner] Matched and added to wishlist: {artist_name} - {title}")
else:
- print(f"[Quality Scanner] Failed to add to wishlist: {artist_name} - {title}")
+ logger.error(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})")
+ logger.warning(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}")
+ logger.error(f"[Quality Scanner] Matching error for {artist_name} - {title}: {matching_error}")
# Store result
result_entry = {
@@ -27059,10 +27070,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}")
+ logger.warning(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}")
+ logger.error(f"[Quality Scanner] Error processing track: {track_error}")
continue
# Scan complete (don't overwrite if already stopped by user)
@@ -27073,7 +27084,7 @@ 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, "
+ logger.info(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
@@ -27091,7 +27102,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
pass
except Exception as e:
- print(f"[Quality Scanner] Critical error: {e}")
+ logger.error(f"[Quality Scanner] Critical error: {e}")
import traceback
traceback.print_exc()
@@ -27120,7 +27131,7 @@ def _run_duplicate_cleaner():
duplicate_cleaner_state["space_freed"] = 0
duplicate_cleaner_state["error_message"] = ""
- print(f"[Duplicate Cleaner] Starting duplicate scan...")
+ logger.warning(f"[Duplicate Cleaner] Starting duplicate scan...")
# Get Transfer folder path from config
transfer_folder = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
@@ -27129,13 +27140,13 @@ def _run_duplicate_cleaner():
duplicate_cleaner_state["status"] = "error"
duplicate_cleaner_state["phase"] = "Output folder not configured or does not exist"
duplicate_cleaner_state["error_message"] = "Please configure output folder in settings"
- print(f"[Duplicate Cleaner] Transfer folder not found: {transfer_folder}")
+ logger.warning(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}")
+ logger.warning(f"[Duplicate Cleaner] Deleted folder: {deleted_folder}")
# Phase 1: Count total files for progress tracking
with duplicate_cleaner_lock:
@@ -27148,7 +27159,7 @@ def _run_duplicate_cleaner():
dirs.remove('deleted')
total_files += len(files)
- print(f"[Duplicate Cleaner] Found {total_files} total files to scan")
+ logger.warning(f"[Duplicate Cleaner] Found {total_files} total files to scan")
with duplicate_cleaner_lock:
duplicate_cleaner_state["total_files"] = total_files
@@ -27215,7 +27226,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}")
+ logger.warning(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):
@@ -27227,7 +27238,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'])} "
+ logger.warning(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:]:
@@ -27246,7 +27257,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'])} "
+ logger.warning(f"[Duplicate Cleaner] Moved to deleted: {os.path.basename(duplicate_file['full_path'])} "
f"({duplicate_file['extension']}, {duplicate_file['size']} bytes)")
# Update stats
@@ -27256,7 +27267,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}")
+ logger.error(f"[Duplicate Cleaner] Error moving file {duplicate_file['full_path']}: {e}")
continue
# Scan complete
@@ -27266,7 +27277,7 @@ 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, "
+ logger.warning(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")
@@ -27285,7 +27296,7 @@ def _run_duplicate_cleaner():
pass
except Exception as e:
- print(f"[Duplicate Cleaner] Critical error: {e}")
+ logger.error(f"[Duplicate Cleaner] Critical error: {e}")
import traceback
traceback.print_exc()
@@ -27304,7 +27315,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}")
+ logger.info(f"[Quality Scanner API] Starting scan with scope: {scope}")
# Reset state
quality_scanner_state["status"] = "running"
@@ -27350,7 +27361,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...")
+ logger.warning(f"[Duplicate Cleaner API] Starting duplicate cleaner...")
# Reset state
duplicate_cleaner_state["status"] = "running"
@@ -27441,7 +27452,7 @@ def search_retag_albums():
})
return jsonify({"success": True, "albums": albums})
except Exception as e:
- print(f"[Retag] Album search error: {e}")
+ logger.error(f"[Retag] Album search error: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/retag/execute', methods=['POST'])
@@ -27598,15 +27609,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 "
+ logger.info(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")
+ logger.warning(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")
+ logger.warning(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)
@@ -27620,7 +27631,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")
+ logger.info(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)
@@ -27632,7 +27643,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")
+ logger.error(f"[Quality Filter] No candidates match quality profile - download will fail per user preferences")
return []
verified_candidates = []
@@ -27691,18 +27702,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}")
+ logger.warning(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")
+ logger.error(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")
+ logger.warning(f"[Worker Recovery] Batch {batch_id} not found - nothing to recover")
return True
batch = download_batches[batch_id]
@@ -27712,11 +27723,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}")
+ logger.warning(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")
+ logger.warning(f"[Worker Recovery] Attempting to start replacement worker")
# Release lock temporarily to avoid deadlock in _start_next_batch_of_downloads
tasks_lock.release()
try:
@@ -27727,14 +27738,14 @@ def _recover_worker_slot(batch_id, task_id):
return True
else:
- print(f"[Worker Recovery] Active count already 0 - no recovery needed")
+ logger.warning(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}")
+ logger.error(f"[Worker Recovery] FATAL ERROR in recovery: {recovery_error}")
return False
def _get_batch_lock(batch_id):
@@ -27753,7 +27764,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}")
+ logger.info(f"[Batch Manager] Server shutting down - skipping new tasks for batch {batch_id}")
return
with tasks_lock:
@@ -27766,7 +27777,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)}")
+ logger.info(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):
@@ -27776,7 +27787,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})")
+ logger.warning(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
@@ -27785,9 +27796,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'")
+ logger.info(f"[Batch Manager] Set task {task_id} status to 'searching'")
else:
- print(f"[Batch Lock] Task {task_id} not found in download_tasks - skipping")
+ logger.warning(f"[Batch Lock] Task {task_id} not found in download_tasks - skipping")
download_batches[batch_id]['queue_index'] += 1
queue_index += 1
continue
@@ -27801,26 +27812,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}")
+ logger.info(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")
+ logger.error(f"[Batch Lock] CRITICAL: Failed to submit task {task_id} to executor: {submit_error}")
+ logger.info(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")
+ logger.error(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}")
+ logger.info(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)"""
@@ -27926,11 +27937,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}")
+ logger.info(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")
+ logger.warning(f"[Wishlist Processing] Batch {batch_id} not found")
return {'tracks_added': 0, 'errors': 0}
batch = download_batches[batch_id]
@@ -27938,13 +27949,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)")
+ logger.error(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")
+ logger.info(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]
@@ -27954,12 +27965,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}")
+ logger.error(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")
+ logger.warning(f"[Wishlist Processing] Processing {len(cancelled_tracks)} cancelled tracks")
# Process cancelled tracks with safeguard to prevent infinite loops
processed_count = 0
@@ -27993,9 +28004,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")
+ logger.error(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")
+ logger.warning(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)
@@ -28021,14 +28032,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']}")
+ logger.error(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")
+ logger.error(f"[Wishlist Processing] Processing {failed_count} failed tracks for wishlist")
if permanently_failed_tracks:
try:
@@ -28056,10 +28067,10 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
sp_id = sp_track.get('id', '') if isinstance(sp_track, dict) else ''
if str(sp_id).startswith('wing_it_'):
wing_it_skipped += 1
- print(f"[Wishlist Processing] Skipping wing-it track: {track_name}")
+ logger.info(f"[Wishlist Processing] Skipping wing-it track: {track_name}")
continue
- print(f"[Wishlist Processing] Adding track {i+1}/{max_failed_tracks}: {track_name}")
+ logger.error(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,
@@ -28069,7 +28080,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")
+ logger.info(f"[Wishlist Processing] Added {track_name} to wishlist")
try:
if automation_engine:
automation_engine.emit('wishlist_item_added', {
@@ -28080,23 +28091,23 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
except Exception:
pass
else:
- print(f"[Wishlist Processing] Failed to add {track_name} to wishlist")
+ logger.error(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}")
+ logger.error(f"[Wishlist Processing] Exception adding track to wishlist: {e}")
if wing_it_skipped:
- print(f"[Wishlist Processing] Skipped {wing_it_skipped} wing-it fallback tracks")
- print(f"[Wishlist Processing] Added {wishlist_added_count}/{failed_count} failed tracks to wishlist (errors: {error_count})")
+ logger.warning(f"[Wishlist Processing] Skipped {wing_it_skipped} wing-it fallback tracks")
+ logger.error(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}")
+ logger.error(f"[Wishlist Processing] Critical error adding failed tracks to wishlist: {e}")
import traceback
traceback.print_exc()
else:
- print(f"ā¹ļø [Wishlist Processing] No failed tracks to add to wishlist")
+ logger.error(f"ā¹ļø [Wishlist Processing] No failed tracks to add to wishlist")
# Store completion summary in batch for API response (matching sync.py pattern)
completion_summary = {
@@ -28111,7 +28122,7 @@ 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}")
+ logger.info(f"[Wishlist Processing] Completed wishlist processing for batch {batch_id}")
# Auto-cleanup: Clear completed downloads from slskd
try:
@@ -28130,7 +28141,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
return completion_summary
except Exception as e:
- print(f"[Wishlist Processing] CRITICAL ERROR in wishlist processing: {e}")
+ logger.error(f"[Wishlist Processing] CRITICAL ERROR in wishlist processing: {e}")
import traceback
traceback.print_exc()
@@ -28148,7 +28159,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}")
+ logger.error(f"[Wishlist Processing] Failed to update batch after error: {lock_error}")
return {'tracks_added': 0, 'errors': 1, 'total_failed': 0}
@@ -28160,7 +28171,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}")
+ logger.info(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)
@@ -28168,7 +28179,7 @@ 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")
+ logger.error(f"[Auto-Wishlist] Background processing complete: {tracks_added} added to wishlist, {total_failed} failed")
# Add activity for wishlist processing
if tracks_added > 0:
@@ -28194,9 +28205,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}")
+ logger.info(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}")
+ logger.error(f"[Auto-Wishlist] Error toggling cycle: {cycle_error}")
# Mark auto-processing as complete and reset timestamp
with wishlist_timer_lock:
@@ -28216,7 +28227,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}")
+ logger.error(f"[Auto-Wishlist] Error in auto-completion processing: {e}")
import traceback
traceback.print_exc()
@@ -28231,7 +28242,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}")
+ logger.warning(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
@@ -28244,7 +28255,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")
+ logger.info(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'
@@ -28277,15 +28288,15 @@ 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']}")
+ logger.warning(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']}")
+ logger.info(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']}")
+ logger.error(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:
@@ -28303,7 +28314,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}")
+ logger.info(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')
@@ -28329,18 +28340,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}")
+ logger.error(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']}")
+ logger.error(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")
+ logger.info(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]
@@ -28363,7 +28374,7 @@ def _on_download_completed(batch_id, task_id, success=True):
if task_status == 'searching':
task_age = current_time - task.get('status_change_time', current_time)
if task_age > 600: # 10 minutes
- print(f"ā° [Stuck Detection] Task {task_id} stuck in searching for {task_age:.0f}s - forcing not_found")
+ logger.info(f"ā° [Stuck Detection] Task {task_id} stuck in searching for {task_age:.0f}s - forcing not_found")
task['status'] = 'not_found'
task['error_message'] = f'Search stuck for {int(task_age // 60)} minutes with no results ā timed out'
finished_count += 1
@@ -28372,7 +28383,7 @@ def _on_download_completed(batch_id, task_id, success=True):
elif task_status == 'post_processing':
task_age = current_time - task.get('status_change_time', current_time)
if task_age > 300: # 5 minutes (post-processing should be fast)
- print(f"ā° [Stuck Detection] Task {task_id} stuck in post_processing for {task_age:.0f}s - forcing completion")
+ logger.info(f"ā° [Stuck Detection] Task {task_id} stuck in post_processing for {task_age:.0f}s - forcing completion")
task['status'] = 'completed' # Assume it worked if file verification is taking too long
finished_count += 1
else:
@@ -28381,19 +28392,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")
+ logger.warning(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")
+ logger.error(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")
+ logger.warning(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")
+ logger.info(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:
@@ -28434,30 +28445,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")
+ logger.info(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")
+ logger.info(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")
+ logger.info(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")
+ logger.info(f"Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase")
- print(f"[Batch Manager] Batch {batch_id} complete - stopping monitor")
+ logger.info(f"[Batch Manager] Batch {batch_id} complete - stopping monitor")
download_monitor.stop_monitoring(batch_id)
# M3U REGENERATION: Regenerate M3U with real library paths now that
@@ -28481,7 +28492,7 @@ def _on_download_completed(batch_id, task_id, success=True):
if m3u_tracks:
_regenerate_batch_m3u(batch, m3u_tracks)
except Exception as m3u_err:
- print(f"[M3U] Error regenerating M3U on batch complete: {m3u_err}")
+ logger.error(f"[M3U] Error regenerating M3U on batch complete: {m3u_err}")
# REPAIR: Scan all album folders from this batch for track number issues
if repair_worker:
@@ -28512,12 +28523,12 @@ def _on_download_completed(batch_id, task_id, success=True):
file_lock_fn=_get_file_lock,
)
if _cons_result.get('success'):
- print(f"[Album Consistency] {_cons_result['tags_written']}/{_cons_result['total_files']} files "
+ logger.info(f"[Album Consistency] {_cons_result['tags_written']}/{_cons_result['total_files']} files "
f"harmonized to release {_cons_result.get('release_mbid', '')[:8]}...")
elif _cons_result.get('error'):
- print(f"[Album Consistency] Skipped: {_cons_result['error']}")
+ logger.error(f"[Album Consistency] Skipped: {_cons_result['error']}")
except Exception as cons_err:
- print(f"[Album Consistency] Failed (non-fatal): {cons_err}")
+ logger.error(f"[Album Consistency] Failed (non-fatal): {cons_err}")
# Mark that wishlist processing is starting (prevents premature cleanup)
batch['wishlist_processing_started'] = True
@@ -28530,12 +28541,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")
+ logger.warning(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}")
+ logger.info(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):
@@ -28570,13 +28581,13 @@ 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")
+ logger.warning(f"[Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing")
# Allow duplicate tracks across albums ā when enabled, only skip tracks already
# owned in THIS album, not tracks owned in other albums
allow_duplicates = config_manager.get('wishlist.allow_duplicate_tracks', True)
if allow_duplicates and batch_is_album:
- print(f"[Duplicates] Allow duplicate tracks enabled ā only checking ownership within target album")
+ logger.info(f"[Duplicates] Allow duplicate tracks enabled ā only checking ownership within target album")
# PREFLIGHT: Pre-populate MusicBrainz release cache for album downloads.
# This ensures ALL tracks in the album use the same release MBID during
@@ -28601,12 +28612,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}': "
+ logger.info(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")
+ logger.warning(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}")
+ logger.error(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
@@ -28627,11 +28638,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})")
+ logger.info(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")
+ logger.warning(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")
+ logger.error(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),
@@ -28643,7 +28654,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")
+ logger.warning(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
@@ -28710,7 +28721,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}")
+ logger.error(f"[Analysis] Error checking wishlist removal for found track: {wishlist_error}")
with tasks_lock:
if batch_id in download_batches:
@@ -28726,7 +28737,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")
+ logger.warning(f"[Content Filter] Filtered out {skipped} explicit track(s) from download queue")
with tasks_lock:
if batch_id in download_batches:
@@ -28734,7 +28745,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.")
+ logger.warning(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'))
@@ -28785,37 +28796,37 @@ 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)")
+ logger.warning(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)")
+ logger.warning(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)")
+ logger.warning(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)")
+ logger.warning(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")
+ logger.warning("[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
- print(f" transitioning batch {batch_id} to download phase with {len(missing_tracks)} tracks.")
+ logger.warning(f" transitioning batch {batch_id} to download phase with {len(missing_tracks)} tracks.")
# Read batch context (quick lock) before doing any network I/O
with tasks_lock:
@@ -28844,7 +28855,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}'")
+ logger.info(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
@@ -28883,7 +28894,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} ā "
+ logger.info(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)
@@ -28899,7 +28910,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")
+ logger.info(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:
@@ -28910,16 +28921,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)")
+ logger.info(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")
+ logger.warning(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")
+ logger.warning(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}")
+ logger.error(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:
@@ -28932,7 +28943,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}")
+ logger.info(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
@@ -28941,7 +28952,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')}'")
+ logger.info(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
@@ -29002,7 +29013,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', '?')}'")
+ logger.info(f"[Wishlist Album Grouping] Album '{_wl_album.get('name', album_id)}' ā artist: '{wishlist_album_artist_map[album_id].get('name', '?')}'")
@@ -29015,7 +29026,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')}")
+ logger.info(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':
@@ -29074,16 +29085,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']}'")
+ logger.info(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}")
+ logger.info(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')}")
+ logger.debug(f"[Debug] Task Creation - playlist folder mode NOT enabled for: {track_info.get('name')}")
download_tasks[task_id] = {
'status': 'pending', 'track_info': track_info,
@@ -29099,7 +29110,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}")
+ logger.error(f"Master worker for batch {batch_id} failed: {e}")
import traceback
traceback.print_exc()
@@ -29115,11 +29126,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)")
+ logger.error(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")
+ logger.error("[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
@@ -29131,21 +29142,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}")
+ logger.info(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")
+ logger.warning(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")
+ logger.warning(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")
+ logger.info(f"[Post-Processing] Task {task_id} already completed by stream processor, skipping verification")
return
# Extract file information for verification
@@ -29154,7 +29165,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}")
+ logger.warning(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'
@@ -29170,39 +29181,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}")
+ logger.info(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
+ logger.info(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}")
+ logger.info(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())}")
+ logger.info(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}")
+ logger.info(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}")
+ logger.info(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)}")
+ logger.warning(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}")
+ logger.error(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}")
+ logger.warning(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
@@ -29214,35 +29225,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}")
+ logger.info(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())}")
+ logger.info(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}")
+ logger.info(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}")
+ logger.info(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)}")
+ logger.warning(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}")
+ logger.error(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}'")
+ logger.warning(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}")
+ logger.info(f"[Post-Processing] Sample of existing keys: {sample_keys}")
# RESILIENT FILE-FINDING LOOP: Try up to 3 times with delays
found_file = None
@@ -29297,51 +29308,51 @@ def _run_post_processing_worker(task_id, batch_id):
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}")
+ logger.info(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")
+ logger.info(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}")
+ logger.warning(f"[Post-Processing] Attempt {retry_count + 1}/{_file_search_max_retries} to find file")
+ logger.info(f"[Post-Processing] Original filename: {task_basename}")
if expected_final_filename:
- print(f"[Post-Processing] Expected final filename: {expected_final_filename}")
+ logger.info(f"[Post-Processing] Expected final filename: {expected_final_filename}")
else:
- print(f"[Post-Processing] No expected final filename available")
+ logger.warning(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...")
+ logger.info(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}")
+ logger.info(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")
+ logger.error(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...")
+ logger.info(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}")
+ logger.info(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")
+ logger.error(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")
+ logger.warning(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}")
+ logger.warning(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}")
+ logger.error(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...")
+ logger.info(f"[Post-Processing] Waiting 5 seconds before next attempt...")
time.sleep(5)
if not found_file:
@@ -29351,7 +29362,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")
+ logger.error(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)}'
@@ -29360,7 +29371,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}")
+ logger.info(f"[Post-Processing] File found in transfer folder - already completed by stream processor: {found_file}")
# Check if metadata enhancement was completed
metadata_enhanced = False
@@ -29369,7 +29380,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")
+ logger.warning(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:
@@ -29388,13 +29399,16 @@ 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)}")
track_number = _extract_track_number_from_filename(found_file)
- print(f" -> Extracted track number: {track_number}")
+ logger.warning(
+ "[Verification] missing track_number; extracted from filename=%r -> %s",
+ os.path.basename(found_file),
+ 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")
+ logger.error(f"[Verification] Invalid track number ({track_number}), defaulting to 1")
track_number = 1
# Get clean track name
@@ -29427,42 +29441,42 @@ 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}")
+ logger.error(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']}'")
+ logger.info(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']}")
+ logger.info(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')}")
+ logger.info(f"[Post-Processing] Attempting metadata enhancement for: {found_file}")
+ logger.warning(f"[Metadata Input] Verification worker - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})")
+ logger.warning(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)}")
+ logger.info(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)}")
+ logger.info(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}")
+ logger.warning(f"[Post-Processing] Missing spotify_artist or spotify_album in context")
+ logger.info(f"[Post-Processing] spotify_artist: {spotify_artist is not None}, spotify_album: {spotify_album is not None}")
# Wipe source tags even without full enhancement ā prevents
# Soulseek uploader's MusicBrainz IDs from causing album splits
if found_file and os.path.exists(found_file):
_wipe_source_tags(found_file)
except Exception as enhancement_error:
import traceback
- print(f"[Post-Processing] Error during metadata enhancement: {enhancement_error}\n{traceback.format_exc()}")
+ logger.error(f"[Post-Processing] Error during metadata enhancement: {enhancement_error}\n{traceback.format_exc()}")
if found_file and os.path.exists(found_file):
_wipe_source_tags(found_file)
else:
- print(f"[Post-Processing] Cannot complete metadata enhancement - missing context or expected filename")
+ logger.warning(f"[Post-Processing] Cannot complete metadata enhancement - missing context or expected filename")
if found_file and os.path.exists(found_file):
_wipe_source_tags(found_file)
else:
- print(f"[Post-Processing] File already has metadata enhancement completed")
+ logger.info(f"[Post-Processing] File already has metadata enhancement completed")
with tasks_lock:
if task_id in download_tasks:
@@ -29473,7 +29487,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}")
+ logger.info(f"[Verification] Cleaned up context after successful verification: {context_key}")
_on_download_completed(batch_id, task_id, success=True)
return
@@ -29488,12 +29502,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}")
+ logger.info(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)}")
+ logger.warning(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')
@@ -29503,13 +29517,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}")
+ logger.info(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}")
+ logger.error(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'
@@ -29517,7 +29531,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}")
+ logger.error(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'
@@ -29534,33 +29548,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")
+ logger.warning(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")
+ logger.info(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")
+ logger.warning(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")
+ logger.warning(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")
+ logger.warning(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}'")
+ logger.info(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
@@ -29591,7 +29605,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'}")
+ logger.info(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):
@@ -29665,8 +29679,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}')")
+ logger.info(f"[Modal Worker] Generated {len(search_queries)} smart search queries for '{track.name}': {search_queries}")
+ logger.info(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
@@ -29675,29 +29689,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}")
+ logger.debug(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}")
+ logger.debug(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}")
+ logger.debug(f"[Modal Worker] Query {query_index + 1}/{len(search_queries)}: '{query}'")
+ logger.debug(f"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")
+ logger.debug(f"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")
+ logger.info(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")
+ logger.warning(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
@@ -29707,15 +29721,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}'")
+ logger.debug(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")
+ logger.info(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")
+ logger.warning(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)
@@ -29726,7 +29740,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")
+ logger.info(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')
@@ -29743,7 +29757,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}")
+ logger.debug(f"[Modal Worker] Search failed for query '{query}': {e}")
search_diagnostics.append(f'"{query}": search error ā {e}')
continue
@@ -29775,7 +29789,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}")
+ logger.warning(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]
@@ -29785,27 +29799,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}'")
+ logger.warning(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!")
+ logger.warning(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}")
+ logger.error(f"[Hybrid Fallback] {fallback_source} search failed: {e}")
continue
- print(f"[Hybrid Fallback] {fallback_source} returned no valid candidates")
+ logger.warning(f"[Hybrid Fallback] {fallback_source} returned no valid candidates")
except Exception as e:
- print(f"[Hybrid Fallback] Error in fallback logic: {e}")
+ logger.error(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.")
+ logger.warning(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'
@@ -29820,12 +29834,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}")
+ logger.error(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}")
+ logger.error(f"CRITICAL ERROR in download task for '{track_name_safe}' (task_id: {task_id}): {e}")
traceback.print_exc()
# Update task status safely with timeout
@@ -29836,27 +29850,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'")
+ logger.error(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")
+ logger.error(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}")
+ logger.error(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}")
+ logger.error(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}")
+ logger.error(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}")
+ logger.error(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}")
+ logger.error(f"[Exception Recovery] FATAL: Could not recover worker slot: {recovery_error}")
def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None):
"""
@@ -29877,10 +29891,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}")
+ logger.info(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}")
+ logger.warning(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
@@ -29888,14 +29902,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}")
+ logger.info(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}")
+ logger.info(f"[Modal Worker] Skipping blacklisted source: {source_key}")
continue
except Exception:
pass
@@ -29905,9 +29919,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}")
+ logger.info(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})")
+ logger.info(f"[Modal Worker] Trying candidate {candidate_index + 1}/{len(candidates)}: {candidate.filename} (Confidence: {candidate.confidence:.2f})")
try:
# Update task status to downloading
@@ -29955,7 +29969,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))")
+ logger.info(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)
@@ -29993,7 +30007,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")
+ logger.error(f"[Modal Worker] Invalid candidate data: missing username or filename")
continue
# PROTECTION: Check if there's already an active download for this task
@@ -30003,12 +30017,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")
+ logger.info(f"[Modal Worker] Task {task_id} already has active download {current_download_id} - skipping new download attempt")
+ logger.info(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)}")
+ logger.info(f"[Modal Worker] Starting download: {username} / {os.path.basename(filename)}")
download_id = run_async(soulseek_client.download(username, filename, size))
if download_id:
@@ -30027,7 +30041,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}'")
+ logger.info(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
@@ -30040,14 +30054,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}")
+ logger.info(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']}")
+ logger.info(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:
@@ -30057,7 +30071,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']}")
+ logger.info(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.)
@@ -30065,7 +30079,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']}")
+ logger.info(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'):
@@ -30075,18 +30089,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}")
+ logger.error(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")
+ logger.warning(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")
+ logger.info(f"[Context] Using explicit album context flag from artist page")
else:
# Fall back to guessing based on clean data
is_album_context = (
@@ -30105,7 +30119,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']}")
+ logger.warning(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,
@@ -30119,21 +30133,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}")
+ logger.info(f"[Context] Set is_album_download: {is_album_context} (has clean data: {has_clean_spotify_data})")
+ logger.debug(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")
+ logger.warning(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}")
+ logger.warning(f"Successfully cancelled active download {download_id}")
except Exception as cancel_error:
- print(f"Warning: Failed to cancel active download {download_id}: {cancel_error}")
+ logger.error(f"Failed to cancel active download {download_id}: {cancel_error}")
# Free worker slot
if batch_id:
@@ -30147,10 +30161,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}")
+ logger.info(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}'")
+ logger.error(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:
@@ -30159,7 +30173,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}")
+ logger.error(f"[Modal Worker] Error attempting download for '{candidate.filename}': {e}")
traceback.print_exc()
# Reset status back to searching for next attempt
with tasks_lock:
@@ -30168,7 +30182,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}'")
+ logger.error(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) āā
@@ -30209,7 +30223,7 @@ def _get_staging_file_cache(batch_id):
'extension': ext,
})
- print(f"[Staging] Scanned {len(files)} audio files in staging folder")
+ logger.info(f"[Staging] Scanned {len(files)} audio files in staging folder")
with _staging_cache_lock:
_staging_cache[batch_id] = files
return files
@@ -30277,7 +30291,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}': "
+ logger.info(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
@@ -30294,7 +30308,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}")
+ logger.info(f"[Staging] Copied to transfer: {dest_path}")
# Mark task as completed with staging context
with tasks_lock:
@@ -30414,7 +30428,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}")
+ logger.error(f"[Staging] Failed to use staging file: {e}")
return False
@@ -30630,7 +30644,7 @@ 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")
+ logger.warning(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
@@ -30691,7 +30705,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}")
+ logger.error(f"Error starting missing downloads: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/active-processes', methods=['GET'])
@@ -30749,7 +30763,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")
+ logger.info(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):
@@ -30799,21 +30813,21 @@ 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")
+ logger.info(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}")
+ logger.error(f"[Safety Valve] Error checking for completed file: {e}")
if not recovered:
if stuck_state == 'searching':
- print(f"ā° [Safety Valve] Task {task_id} stuck in searching for {task_age:.1f}s - marking not_found")
+ logger.info(f"ā° [Safety Valve] Task {task_id} stuck in searching for {task_age:.1f}s - marking not_found")
task['status'] = 'not_found'
task['error_message'] = f'Search stuck for {int(task_age // 60)} minutes with no results ā timed out'
else:
- print(f"ā° [Safety Valve] Task {task_id} stuck for {task_age:.1f}s - forcing failure")
+ logger.error(f"ā° [Safety Valve] Task {task_id} stuck for {task_age:.1f}s - forcing failure")
task['status'] = 'failed'
task['error_message'] = f'Task stuck in {stuck_state} state for {int(task_age // 60)} minutes ā forcibly stopped'
@@ -30851,7 +30865,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")
+ logger.error(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
@@ -30867,13 +30881,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})")
+ logger.info(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")
+ logger.info(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)
@@ -30881,7 +30895,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")
+ logger.info(f"Task {task_id} waiting for verification worker to complete")
elif 'InProgress' in state_str:
task_status['status'] = 'downloading'
else:
@@ -30975,7 +30989,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}")
+ logger.error(f"Error processing batch {batch_id}: {batch_error}")
response["batches"][batch_id] = {"error": str(batch_error)}
# Add metadata for debugging/monitoring
@@ -31004,12 +31018,12 @@ def get_batched_download_statuses():
response["debug_info"] = debug_info
- print(f"[Batched Status] Returning status for {len(response['batches'])} batches")
+ logger.info(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}")
+ logger.info(f"[Batched Status] Worker count discrepancies in batches: {discrepancies}")
return jsonify(response)
@@ -31219,7 +31233,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}")
+ logger.info(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'
@@ -31239,8 +31253,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}")
+ logger.info(f"[Cancel] Task {task_id} (status: {current_status}) - freeing worker slot for batch {batch_id}")
+ logger.info(f"[Cancel] Active count before: {active_count}")
# Use the completion callback with error handling
_on_download_completed(batch_id, task_id, success=False)
@@ -31248,35 +31262,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}")
+ logger.info(f"[Cancel] Active count after: {new_active}")
elif active_count == 0:
- print(f"[Cancel] Task {task_id} - no active workers to free")
+ logger.warning(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")
+ logger.warning(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")
+ logger.warning(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}")
+ logger.error(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")
+ logger.warning(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}")
+ logger.error(f"[Cancel] FATAL: Emergency recovery failed: {recovery_error}")
else:
- print(f"[Cancel] Task {task_id} cancelled (no batch_id - likely already completed)")
+ logger.warning(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}")
+ logger.warning(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}")
+ logger.error(f"Failed to cancel download on slskd, but worker already moved on: {e}")
### NEW LOGIC START: Add cancelled track to wishlist ###
try:
@@ -31347,11 +31361,11 @@ def cancel_download_task():
)
if success:
- print(f"Added cancelled track '{track_info.get('name')}' to wishlist.")
+ logger.warning(f"Added cancelled track '{track_info.get('name')}' to wishlist.")
else:
- print(f"Failed to add cancelled track '{track_info.get('name')}' to wishlist.")
+ logger.error(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}")
+ logger.error(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."})
@@ -31394,7 +31408,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}")
+ logger.info(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'
@@ -31415,23 +31429,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})")
+ logger.info(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']}")
+ logger.info(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")
+ logger.info(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']})")
+ logger.warning(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 = {
@@ -31444,11 +31458,11 @@ def _atomic_cancel_task(playlist_id, track_index):
'worker_slot_freed': worker_slot_freed
}
- print(f"[Atomic Cancel] Successfully cancelled task {task_id}")
+ logger.warning(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}")
+ logger.error(f"[Atomic Cancel] Error in atomic cancel: {e}")
import traceback
traceback.print_exc()
return False, f"Internal error: {str(e)}", None
@@ -31491,14 +31505,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}")
+ logger.error(f"[Atomic Cancel] 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}")
+ logger.error(f"[Atomic Cancel] Could not check batch completion: {e}")
# Cancel Soulseek download if active (non-blocking)
if task:
@@ -31507,27 +31521,27 @@ 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}")
+ logger.info(f"[Atomic Cancel] Task {task_id} state: status='{current_status}', original_status='{original_status}', download_id='{download_id}', username='{username}'")
+ logger.info(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))}")
+ logger.info(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" Username: {username}")
- print(f" Download ID: {download_id}")
- print(f" Base URL: {soulseek_client.base_url}")
- print(f" Expected URL: {soulseek_client.base_url}/transfers/downloads/{username}/{download_id}?remove=true")
+ logger.info(f"[Atomic Cancel] Attempting to cancel Soulseek download:")
+ logger.info(f" Username: {username}")
+ logger.info(f" Download ID: {download_id}")
+ logger.info(f" Base URL: {soulseek_client.base_url}")
+ logger.info(f" Expected URL: {soulseek_client.base_url}/transfers/downloads/{username}/{download_id}?remove=true")
# CRITICAL: Must use REAL download ID from slskd, not filename
success = False
real_download_id = None
# Step 1: Always search for real download ID first
- print(f"[Atomic Cancel] Searching slskd transfers for real download ID")
+ logger.info(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:
@@ -31541,68 +31555,68 @@ 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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.info(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}")
+ logger.info(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}")
+ logger.warning(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")
+ logger.error(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}")
+ logger.warning(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}")
+ logger.warning(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}")
+ logger.error(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}")
+ logger.error(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")
+ logger.error(f"[Atomic Cancel] Could not find real download ID in slskd transfers")
+ logger.warning(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")
+ logger.error(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}")
+ logger.error(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()}")
+ logger.error(f"[Atomic Cancel] Cancel error traceback: {traceback.format_exc()}")
else:
- print(f"ā¹ļø [Atomic Cancel] No download_id or username available - skipping slskd cancel")
+ logger.warning(f"ā¹ļø [Atomic Cancel] No download_id or username available - skipping slskd cancel")
# Add to wishlist (non-blocking, best effort)
try:
_add_cancelled_task_to_wishlist(task)
except Exception as e:
- print(f"[Atomic Cancel] Warning: Could not add to wishlist: {e}")
+ logger.error(f"[Atomic Cancel] Could not add to wishlist: {e}")
return jsonify({
"success": True,
@@ -31615,7 +31629,7 @@ def cancel_task_v2():
})
except Exception as e:
- print(f"[Cancel V2] Unexpected error: {e}")
+ logger.error(f"[Cancel V2] Unexpected error: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -31630,7 +31644,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")
+ logger.warning(f"[Completion Check V2] Batch {batch_id} not found")
return
batch = download_batches[batch_id]
@@ -31652,7 +31666,7 @@ def _check_batch_completion_v2(batch_id):
if task_status == 'searching':
task_age = current_time - task.get('status_change_time', current_time)
if task_age > 600: # 10 minutes
- print(f"ā° [Stuck Detection V2] Task {task_id} stuck in searching for {task_age:.0f}s - forcing not_found")
+ logger.info(f"ā° [Stuck Detection V2] Task {task_id} stuck in searching for {task_age:.0f}s - forcing not_found")
task['status'] = 'not_found'
task['error_message'] = f'Search stuck for {int(task_age // 60)} minutes with no results ā timed out'
finished_count += 1
@@ -31661,7 +31675,7 @@ def _check_batch_completion_v2(batch_id):
elif task_status == 'post_processing':
task_age = current_time - task.get('status_change_time', current_time)
if task_age > 300: # 5 minutes (post-processing should be fast)
- print(f"ā° [Stuck Detection V2] Task {task_id} stuck in post_processing for {task_age:.0f}s - forcing completion")
+ logger.info(f"ā° [Stuck Detection V2] Task {task_id} stuck in post_processing for {task_age:.0f}s - forcing completion")
task['status'] = 'completed' # Assume it worked if file verification is taking too long
finished_count += 1
else:
@@ -31670,18 +31684,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")
+ logger.warning(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}")
+ logger.warning(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")
+ logger.info(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)
@@ -31709,7 +31723,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")
+ logger.warning(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
@@ -31718,30 +31732,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")
+ logger.info(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")
+ logger.info(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")
+ logger.info(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")
+ logger.info(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")
+ logger.info(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
@@ -31769,32 +31783,32 @@ def _check_batch_completion_v2(batch_id):
file_lock_fn=_get_file_lock,
)
if _cons_result.get('success'):
- print(f"[Album Consistency V2] {_cons_result['tags_written']}/{_cons_result['total_files']} files "
+ logger.info(f"[Album Consistency V2] {_cons_result['tags_written']}/{_cons_result['total_files']} files "
f"harmonized to release {_cons_result.get('release_mbid', '')[:8]}...")
elif _cons_result.get('error'):
- print(f"[Album Consistency V2] Skipped: {_cons_result['error']}")
+ logger.error(f"[Album Consistency V2] Skipped: {_cons_result['error']}")
except Exception as cons_err:
- print(f"[Album Consistency V2] Failed (non-fatal): {cons_err}")
+ logger.error(f"[Album Consistency V2] Failed (non-fatal): {cons_err}")
# Process wishlist outside of the lock to prevent threading issues
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")
+ logger.info(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")
+ logger.info(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']}")
+ logger.warning(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}")
+ logger.error(f"[Completion Check V2] Error checking batch completion: {e}")
import traceback
traceback.print_exc()
return False
@@ -31870,12 +31884,12 @@ def _add_cancelled_task_to_wishlist(task):
)
if success:
- print(f"[Atomic Cancel] Added '{track_info.get('name')}' to wishlist")
+ logger.info(f"[Atomic Cancel] Added '{track_info.get('name')}' to wishlist")
else:
- print(f"[Atomic Cancel] Failed to add '{track_info.get('name')}' to wishlist")
+ logger.error(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}")
+ logger.error(f"[Atomic Cancel] Critical error adding to wishlist: {e}")
@app.route('/api/playlists//cancel_batch', methods=['POST'])
def cancel_batch(batch_id):
@@ -31903,16 +31917,16 @@ 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")
+ logger.warning(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)")
+ logger.warning(f"ā¹ļø [Wishlist Cancel] Manual wishlist batch cancelled (no flag reset needed)")
# Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist
if playlist_id and playlist_id.startswith('youtube_'):
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)")
+ logger.warning(f"Reset YouTube playlist {url_hash} to discovered phase (batch cancelled)")
# Cancel all individual tasks in the batch
cancelled_count = 0
@@ -31927,11 +31941,11 @@ def cancel_batch(batch_id):
playlist_name = download_batches[batch_id].get('playlist_name', 'Unknown Playlist')
add_activity_item("", "Batch Cancelled", f"'{playlist_name}' - {cancelled_count} downloads cancelled", "Now")
- print(f"Cancelled batch {batch_id} with {cancelled_count} tasks")
+ logger.warning(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}")
+ logger.error(f"Error cancelling batch {batch_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# NEW ENDPOINT: Add this function to web_server.py
@@ -31956,7 +31970,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")
+ logger.info(f"[Cleanup] Batch {batch_id} cleanup deferred - wishlist processing in progress")
return jsonify({
"success": False,
"error": "Batch cleanup deferred - wishlist processing in progress",
@@ -31974,15 +31988,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.")
+ logger.info(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?")
+ logger.info(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}")
+ logger.error(f"Error during batch cleanup for '{batch_id}': {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ===============================
@@ -32853,12 +32867,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" Release: {album_context.get('release_date', 'Unknown')}, Tracks: {album_context.get('total_tracks', len(tracks))}")
+ logger.info(f"[Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})")
+ logger.info(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}'")
+ logger.info(f"[Playlist Folder] Enabled for playlist: '{playlist_name}'")
# Limit concurrent analysis processes to prevent resource exhaustion
with tasks_lock:
@@ -32918,7 +32932,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})")
+ logger.info(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_'):
@@ -32927,7 +32941,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})")
+ logger.info(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_'):
@@ -32936,7 +32950,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})")
+ logger.info(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_'):
@@ -32945,7 +32959,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})")
+ logger.info(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):
@@ -33015,7 +33029,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}")
+ logger.error(f"Error starting missing downloads: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ===============================
@@ -33032,7 +33046,7 @@ def _load_sync_status_file():
return data
return {}
except Exception as e:
- print(f"Error loading sync status: {e}")
+ logger.error(f"Error loading sync status: {e}")
return {}
def _save_sync_status_file(sync_statuses):
@@ -33041,7 +33055,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}")
+ logger.error(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)."""
@@ -33066,10 +33080,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})")
+ logger.info(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}")
+ logger.error(f"Error updating sync status for {playlist_id}: {e}")
@app.route('/api/spotify/playlists', methods=['GET'])
def get_spotify_playlists():
@@ -33090,24 +33104,37 @@ 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" - Playlist snapshot: '{playlist_snapshot}'")
- print(f" - Status info: {status_info}")
-
if 'last_synced' in status_info:
stored_snapshot = status_info.get('snapshot_id')
last_sync_time = datetime.fromisoformat(status_info['last_synced']).strftime('%b %d, %H:%M')
- print(f" - Stored snapshot: '{stored_snapshot}'")
- print(f" - Snapshots match: {playlist_snapshot == stored_snapshot}")
-
if playlist_snapshot != stored_snapshot:
sync_status = f"Last Sync: {last_sync_time}"
- print(f" - Result: Needs Sync (showing: {sync_status})")
+ logger.info(
+ "Playlist sync status: name=%s id=%s snapshot=%r stored_snapshot=%r result=Needs Sync display=%s",
+ p.name,
+ p.id,
+ playlist_snapshot,
+ stored_snapshot,
+ sync_status,
+ )
else:
sync_status = f"Synced: {last_sync_time}"
- print(f" - Result: {sync_status}")
+ logger.info(
+ "Playlist sync status: name=%s id=%s snapshot=%r stored_snapshot=%r result=Synced display=%s",
+ p.name,
+ p.id,
+ playlist_snapshot,
+ stored_snapshot,
+ sync_status,
+ )
else:
- print(f" - No last_synced found - Never Synced")
+ logger.warning(
+ "Playlist sync status: name=%s id=%s snapshot=%r result=Never Synced display=%s",
+ p.name,
+ p.id,
+ playlist_snapshot,
+ sync_status,
+ )
playlist_data.append({
"id": p.id, "name": p.name, "owner": p.owner,
@@ -33143,9 +33170,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)")
+ logger.info(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}")
+ logger.error(f"Failed to add Liked Songs playlist: {liked_error}")
# Don't fail the entire request if Liked Songs fails
return jsonify(playlist_data)
@@ -33473,7 +33500,7 @@ def search_spotify():
return jsonify({'tracks': {'items': tracks_items}})
except Exception as e:
- print(f"Error searching Spotify: {e}")
+ logger.error(f"Error searching Spotify: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify/search_tracks', methods=['GET'])
@@ -33522,7 +33549,7 @@ def search_spotify_tracks():
return jsonify({'tracks': tracks_dict})
except Exception as e:
- print(f"Error searching Spotify tracks: {e}")
+ logger.error(f"Error searching Spotify tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -33572,7 +33599,7 @@ def search_itunes_tracks():
return jsonify({'tracks': tracks_dict})
except Exception as e:
- print(f"Error searching iTunes tracks: {e}")
+ logger.error(f"Error searching iTunes tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -33614,7 +33641,7 @@ def search_deezer_tracks():
return jsonify({'tracks': tracks_dict})
except Exception as e:
- print(f"Error searching Deezer tracks: {e}")
+ logger.error(f"Error searching Deezer tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -34188,7 +34215,7 @@ def get_tidal_playlists():
playlist_data.append(playlist_dict)
- print(f"Loaded {len(playlist_data)} Tidal playlists with track data")
+ logger.info(f"Loaded {len(playlist_data)} Tidal playlists with track data")
return jsonify(playlist_data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -34199,7 +34226,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}")
+ logger.info(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)
@@ -34209,7 +34236,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}")
+ logger.info(f"Loaded {len(full_playlist.tracks)} tracks from Tidal playlist: {full_playlist.name}")
# Convert playlist to dict (matches sync.py structure)
playlist_dict = {
@@ -34234,7 +34261,7 @@ def get_tidal_playlist_tracks(playlist_id):
return jsonify(playlist_dict)
except Exception as e:
- print(f"Error getting Tidal playlist tracks: {e}")
+ logger.error(f"Error getting Tidal playlist tracks: {e}")
return jsonify({"error": str(e)}), 500
@@ -34301,11 +34328,11 @@ def start_tidal_discovery(playlist_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}")
+ logger.info(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}")
+ logger.error(f"Error starting Tidal discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/discovery/status/', methods=['GET'])
@@ -34331,7 +34358,7 @@ def get_tidal_discovery_status(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"Error getting Tidal discovery status: {e}")
+ logger.error(f"Error getting Tidal discovery status: {e}")
return jsonify({"error": str(e)}), 500
@@ -34392,8 +34419,8 @@ def update_tidal_discovery_match():
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" ā {result['spotify_artist']} - {result['spotify_track']}")
+ logger.info(f"Manual match updated: tidal - {identifier} - track {track_index}")
+ logger.info(f" ā {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
try:
@@ -34425,14 +34452,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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"Error updating Tidal discovery match: {e}")
return jsonify({'error': str(e)}), 500
@@ -34462,11 +34489,11 @@ def get_tidal_playlist_states():
}
states.append(state_info)
- print(f"Returning {len(states)} stored Tidal playlist states for hydration")
+ logger.info(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}")
+ logger.error(f"Error getting Tidal playlist states: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/state/', methods=['GET'])
@@ -34499,7 +34526,7 @@ def get_tidal_playlist_state(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"Error getting Tidal playlist state: {e}")
+ logger.error(f"Error getting Tidal playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/reset/', methods=['POST'])
@@ -34528,11 +34555,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}")
+ logger.info(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}")
+ logger.error(f"Error resetting Tidal playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/delete/', methods=['POST'])
@@ -34551,11 +34578,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}")
+ logger.info(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}")
+ logger.error(f"Error deleting Tidal playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/update_phase/', methods=['POST'])
@@ -34580,11 +34607,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}")
+ logger.info(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}")
+ logger.error(f"Error updating Tidal playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@@ -34605,7 +34632,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}")
+ logger.warning(f"Paused {name} enrichment worker during {label}")
except Exception:
pass
return was_running
@@ -34623,7 +34650,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}")
+ logger.info(f"Resumed {name} enrichment worker after {label}")
except Exception:
pass
@@ -34642,10 +34669,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})")
+ logger.warning(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}")
+ logger.info(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
@@ -34703,11 +34730,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', '')}'")
+ logger.info(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}")
+ logger.error(f"Failed to sync discovery results to mirrored playlist: {e}")
traceback.print_exc()
@@ -34725,7 +34752,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")
+ logger.warning(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')
@@ -34757,7 +34784,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()})")
+ logger.info(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')
@@ -34814,7 +34841,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})")
+ logger.warning(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',
@@ -34840,7 +34867,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', '?')}")
+ logger.info(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,
@@ -34975,7 +35002,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})")
+ logger.info(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,
@@ -34993,7 +35020,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"[{i+1}/{len(undiscovered_tracks)}] Wing It: {track_name} by {artist_name}")
+ logger.info(f"[{i+1}/{len(undiscovered_tracks)}] Wing It: {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,
@@ -35018,14 +35045,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")
+ logger.error(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}")
+ logger.error(f"Error in playlist discovery worker: {e}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id, status='error', progress=100,
@@ -35086,7 +35113,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")
+ logger.info(f"Cache artist mismatch: source='{source_artist}' vs cached='{cached_artists[0]}' (sim={best_sim:.2f}), re-searching")
return False
return True
@@ -35173,7 +35200,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}")
+ logger.error(f"Error scoring candidate {idx}: {e}")
continue
return best_match, best_confidence, best_index
@@ -35196,7 +35223,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()})")
+ logger.info(f"Starting Tidal discovery for: {playlist.name} (using {discovery_source.upper()})")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -35208,7 +35235,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)}")
+ logger.info(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 '')
@@ -35216,7 +35243,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)}")
+ logger.debug(f"CACHE HIT [{i+1}/{len(playlist.tracks)}]: {tidal_track.name} by {', '.join(tidal_track.artists)}")
result = {
'tidal_track': {
'id': tidal_track.id,
@@ -35236,7 +35263,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}")
+ logger.error(f"Cache lookup error: {cache_err}")
# Use the search function with appropriate provider
track_result = _search_spotify_for_tidal_track(
@@ -35326,9 +35353,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})")
+ logger.info(f"CACHE SAVED: {tidal_track.name} (confidence: {match_confidence:.3f})")
except Exception as cache_err:
- print(f"Cache save error: {cache_err}")
+ logger.error(f"Cache save error: {cache_err}")
# Auto Wing It fallback for unmatched tracks
if result['status'] != 'found':
@@ -35355,7 +35382,7 @@ def _run_tidal_discovery_worker(playlist_id):
time.sleep(0.1)
except Exception as e:
- print(f"Error processing track {i+1}: {e}")
+ logger.error(f"Error processing track {i+1}: {e}")
# Add error result
result = {
'tidal_track': {
@@ -35380,13 +35407,13 @@ def _run_tidal_discovery_worker(playlist_id):
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")
- print(f"Tidal discovery complete ({source_label}): {successful_discoveries}/{len(playlist.tracks)} tracks found")
+ logger.info(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}")
+ logger.error(f"Error in Tidal discovery worker: {e}")
state['phase'] = 'error'
state['status'] = f'error: {str(e)}'
finally:
@@ -35424,7 +35451,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})")
+ logger.info(f"Tidal track: '{artist_name}' - '{track_name}' (searching {source_name})")
# Use matching engine to generate search queries (with fallback)
try:
@@ -35434,9 +35461,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")
+ logger.info(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}")
+ logger.error(f"Matching engine failed for Tidal, falling back to basic queries: {e}")
if use_spotify:
search_queries = [
f'track:"{track_name}" artist:"{artist_name}"',
@@ -35457,7 +35484,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})")
+ logger.debug(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)
@@ -35481,19 +35508,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})")
+ logger.info(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")
+ logger.info(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}")
+ logger.debug(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")
+ logger.info(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)
@@ -35506,17 +35533,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})")
+ logger.info(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})")
+ logger.info(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})")
+ logger.info(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 ''
@@ -35553,11 +35580,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}")
+ logger.info(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})")
+ logger.info(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}")
+ logger.error(f"[Discovery Enrich] Failed for {result_name} (ID {track_id}): {_enrich_err}")
result_data = {
'id': track_id,
@@ -35574,11 +35601,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})")
+ logger.warning(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}")
+ logger.error(f"Error searching Spotify for Tidal track: {e}")
return None
@@ -35615,7 +35642,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")
+ logger.info(f"Converted {len(spotify_tracks)} Tidal matches to Spotify tracks for sync")
return spotify_tracks
@@ -35669,11 +35696,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(), playlist_image_url)
active_sync_workers[sync_playlist_id] = future
- print(f"Started Tidal sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ logger.info(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}")
+ logger.error(f"Error starting Tidal sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/sync/status/', methods=['GET'])
@@ -35719,7 +35746,7 @@ def get_tidal_sync_status(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"Error getting Tidal sync status: {e}")
+ logger.error(f"Error getting Tidal sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/sync/cancel/', methods=['POST'])
@@ -35750,7 +35777,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}")
+ logger.error(f"Error cancelling Tidal sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -35851,7 +35878,7 @@ def get_deezer_arl_playlists():
'sync_status': 'Never Synced',
})
- print(f"Loaded {len(playlist_data)} Deezer user playlists via ARL")
+ logger.info(f"Loaded {len(playlist_data)} Deezer user playlists via ARL")
return jsonify(playlist_data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -35871,7 +35898,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')}")
+ logger.info(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
@@ -35897,7 +35924,7 @@ def get_deezer_playlist(playlist_id):
return jsonify(playlist)
except Exception as e:
- print(f"Error fetching Deezer playlist: {e}")
+ logger.error(f"Error fetching Deezer playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/discovery/start/', methods=['POST'])
@@ -35970,11 +35997,11 @@ def start_deezer_discovery(playlist_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}")
+ logger.info(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}")
+ logger.error(f"Error starting Deezer discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/discovery/status/', methods=['GET'])
@@ -36000,7 +36027,7 @@ def get_deezer_discovery_status(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"Error getting Deezer discovery status: {e}")
+ logger.error(f"Error getting Deezer discovery status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/discovery/update_match', methods=['POST'])
@@ -36060,8 +36087,8 @@ def update_deezer_discovery_match():
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" ā {result['spotify_artist']} - {result['spotify_track']}")
+ logger.info(f"Manual match updated: deezer - {identifier} - track {track_index}")
+ logger.info(f" ā {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
try:
@@ -36091,14 +36118,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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"Error updating Deezer discovery match: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/deezer/playlists/states', methods=['GET'])
@@ -36125,11 +36152,11 @@ def get_deezer_playlist_states():
}
states.append(state_info)
- print(f"Returning {len(states)} stored Deezer playlist states for hydration")
+ logger.info(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}")
+ logger.error(f"Error getting Deezer playlist states: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/state/', methods=['GET'])
@@ -36162,7 +36189,7 @@ def get_deezer_playlist_state(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"Error getting Deezer playlist state: {e}")
+ logger.error(f"Error getting Deezer playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/reset/', methods=['POST'])
@@ -36191,11 +36218,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}")
+ logger.info(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}")
+ logger.error(f"Error resetting Deezer playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/delete/', methods=['POST'])
@@ -36214,11 +36241,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}")
+ logger.info(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}")
+ logger.error(f"Error deleting Deezer playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/update_phase/', methods=['POST'])
@@ -36251,11 +36278,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}")
+ logger.info(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}")
+ logger.error(f"Error updating Deezer playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@@ -36276,7 +36303,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()})")
+ logger.info(f"Starting Deezer discovery for: {playlist['name']} (using {discovery_source.upper()})")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -36295,7 +36322,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)}")
+ logger.info(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 '')
@@ -36303,7 +36330,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)}")
+ logger.debug(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:
@@ -36341,7 +36368,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}")
+ logger.error(f"Cache lookup error: {cache_err}")
# Create a SimpleNamespace duck-type object for _search_spotify_for_tidal_track
track_ns = types.SimpleNamespace(
@@ -36455,9 +36482,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})")
+ logger.info(f"CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})")
except Exception as cache_err:
- print(f"Cache save error: {cache_err}")
+ logger.error(f"Cache save error: {cache_err}")
# Auto Wing It fallback for unmatched tracks
if result['status_class'] == 'not-found':
@@ -36487,7 +36514,7 @@ def _run_deezer_discovery_worker(playlist_id):
time.sleep(0.1)
except Exception as e:
- print(f"Error processing track {i+1}: {e}")
+ logger.error(f"Error processing track {i+1}: {e}")
# Add error result
result = {
'deezer_track': {
@@ -36517,13 +36544,13 @@ def _run_deezer_discovery_worker(playlist_id):
source_label = discovery_source.upper()
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")
+ logger.info(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}")
+ logger.error(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)}'
@@ -36562,7 +36589,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")
+ logger.info(f"Converted {len(spotify_tracks)} Deezer matches to Spotify tracks for sync")
return spotify_tracks
@@ -36616,11 +36643,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(), playlist_image_url)
active_sync_workers[sync_playlist_id] = future
- print(f"Started Deezer sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ logger.info(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}")
+ logger.error(f"Error starting Deezer sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/sync/status/', methods=['GET'])
@@ -36663,7 +36690,7 @@ def get_deezer_sync_status(playlist_id):
return jsonify(response)
except Exception as e:
- print(f"Error getting Deezer sync status: {e}")
+ logger.error(f"Error getting Deezer sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/sync/cancel/', methods=['POST'])
@@ -36694,7 +36721,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}")
+ logger.error(f"Error cancelling Deezer sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -36722,7 +36749,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']}")
+ logger.info(f"Scraping public Spotify {parsed['type']}: {parsed['id']}")
result = scrape_spotify_embed(parsed['type'], parsed['id'])
@@ -36781,11 +36808,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)")
+ logger.info(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}")
+ logger.error(f"Error parsing Spotify URL: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -36820,11 +36847,11 @@ def start_spotify_public_discovery(url_hash):
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}")
+ logger.info(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}")
+ logger.error(f"Error starting Spotify Public discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/discovery/status/', methods=['GET'])
@@ -36850,7 +36877,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}")
+ logger.error(f"Error getting Spotify Public discovery status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/discovery/update_match', methods=['POST'])
@@ -36910,8 +36937,8 @@ def update_spotify_public_discovery_match():
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" ā {result['spotify_artist']} - {result['spotify_track']}")
+ logger.info(f"Manual match updated: spotify_public - {identifier} - track {track_index}")
+ logger.info(f" ā {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
try:
@@ -36941,14 +36968,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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"Error updating Spotify Public discovery match: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/spotify-public/playlists/states', methods=['GET'])
@@ -36975,11 +37002,11 @@ def get_spotify_public_playlist_states():
}
states.append(state_info)
- print(f"Returning {len(states)} stored Spotify Public playlist states for hydration")
+ logger.info(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}")
+ logger.error(f"Error getting Spotify Public playlist states: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/state/', methods=['GET'])
@@ -37011,7 +37038,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}")
+ logger.error(f"Error getting Spotify Public playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/reset/', methods=['POST'])
@@ -37040,11 +37067,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}")
+ logger.info(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}")
+ logger.error(f"Error resetting Spotify Public playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/delete/', methods=['POST'])
@@ -37063,11 +37090,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}")
+ logger.info(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}")
+ logger.error(f"Error deleting Spotify Public playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/update_phase/', methods=['POST'])
@@ -37100,11 +37127,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}")
+ logger.info(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}")
+ logger.error(f"Error updating Spotify Public playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@@ -37125,7 +37152,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()})")
+ logger.info(f"Starting Spotify Public discovery for: {playlist['name']} (using {discovery_source.upper()})")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -37155,7 +37182,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)}")
+ logger.info(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 '')
@@ -37163,7 +37190,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)}")
+ logger.debug(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:
@@ -37201,7 +37228,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}")
+ logger.error(f"Cache lookup error: {cache_err}")
# Create a SimpleNamespace duck-type object for _search_spotify_for_tidal_track
track_ns = types.SimpleNamespace(
@@ -37315,9 +37342,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})")
+ logger.info(f"CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})")
except Exception as cache_err:
- print(f"Cache save error: {cache_err}")
+ logger.error(f"Cache save error: {cache_err}")
# Auto Wing It fallback for unmatched tracks
if result['status_class'] == 'not-found':
@@ -37347,7 +37374,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}")
+ logger.error(f"Error processing track {i+1}: {e}")
# Add error result
result = {
'spotify_public_track': {
@@ -37377,10 +37404,10 @@ def _run_spotify_public_discovery_worker(url_hash):
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")
- print(f"Spotify Public discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found")
+ logger.info(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}")
+ logger.error(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)}'
@@ -37420,7 +37447,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")
+ logger.info(f"Converted {len(spotify_tracks)} Spotify Public matches to Spotify tracks for sync")
return spotify_tracks
@@ -37474,11 +37501,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(), playlist_image_url)
active_sync_workers[sync_playlist_id] = future
- print(f"Started Spotify Public sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ logger.info(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}")
+ logger.error(f"Error starting Spotify Public sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/sync/status/', methods=['GET'])
@@ -37521,7 +37548,7 @@ def get_spotify_public_sync_status(url_hash):
return jsonify(response)
except Exception as e:
- print(f"Error getting Spotify Public sync status: {e}")
+ logger.error(f"Error getting Spotify Public sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/spotify-public/sync/cancel/', methods=['POST'])
@@ -37552,7 +37579,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}")
+ logger.error(f"Error cancelling Spotify Public sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -37586,7 +37613,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}")
+ logger.info(f"Parsing YouTube playlist: {url}")
# Parse the playlist using our function
playlist_data = parse_youtube_playlist(url)
@@ -37651,11 +37678,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)")
+ logger.info(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}")
+ logger.error(f"Error parsing YouTube playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/discovery/start/', methods=['POST'])
@@ -37691,11 +37718,11 @@ def start_youtube_discovery(url_hash):
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']}")
+ logger.info(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}")
+ logger.error(f"Error starting YouTube discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/discovery/status/', methods=['GET'])
@@ -37721,7 +37748,7 @@ def get_youtube_discovery_status(url_hash):
return jsonify(response)
except Exception as e:
- print(f"Error getting YouTube discovery status: {e}")
+ logger.error(f"Error getting YouTube discovery status: {e}")
return jsonify({"error": str(e)}), 500
@@ -37793,13 +37820,13 @@ def unmatch_discovery_track():
'unmatched_by_user': True,
})
except Exception as e:
- print(f"Error clearing mirrored track match: {e}")
+ logger.error(f"Error clearing mirrored track match: {e}")
- print(f"Unmatched discovery track {track_index}: {result.get('yt_track', result.get('lb_track', ''))}")
+ logger.info(f"Unmatched discovery track {track_index}: {result.get('yt_track', result.get('lb_track', ''))}")
return jsonify({'success': True})
except Exception as e:
- print(f"Error unmatching discovery track: {e}")
+ logger.error(f"Error unmatching discovery track: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@@ -37860,8 +37887,8 @@ def update_youtube_discovery_match():
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" ā {result['spotify_artist']} - {result['spotify_track']}")
+ logger.info(f"Manual match updated: youtube - {identifier} - track {track_index}")
+ logger.info(f" ā {result['spotify_artist']} - {result['spotify_track']}")
# Save manual fix to discovery cache so it appears in discovery pool
try:
@@ -37895,9 +37922,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}")
+ logger.info(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}")
+ logger.error(f"Error saving manual fix to discovery cache: {cache_err}")
# Persist manual fix to DB for mirrored playlists
if identifier.startswith('mirrored_'):
@@ -37916,14 +37943,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}")
+ logger.info(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}")
+ logger.error(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}")
+ logger.error(f"Error updating YouTube discovery match: {e}")
return jsonify({'error': str(e)}), 500
@@ -37957,7 +37984,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...")
+ logger.info(f"Starting {discovery_source} discovery for {len(tracks)} YouTube tracks...")
# Store the discovery source in state
state['discovery_source'] = discovery_source
@@ -37967,7 +37994,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')}')")
+ logger.warning(f"Discovery cancelled for {url_hash} (phase changed to '{state.get('phase')}')")
return
# Update progress
@@ -37981,7 +38008,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}'")
+ logger.info(f"Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'")
# Check discovery cache first
cache_key = _get_discovery_cache_key(cleaned_title, cleaned_artist)
@@ -37989,7 +38016,7 @@ 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}")
+ logger.debug(f"CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}")
result = {
'index': i,
'yt_track': cleaned_title,
@@ -38008,7 +38035,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}")
+ logger.error(f"Cache lookup error: {cache_err}")
# Try multiple search strategies using matching engine
matched_track = None
@@ -38025,14 +38052,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")
+ logger.info(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}")
+ logger.error(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}")
+ logger.debug(f"YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}")
search_results = None
@@ -38057,22 +38084,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})")
+ logger.info(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")
+ logger.info(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}")
+ logger.debug(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})")
+ logger.info(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)")
+ logger.info("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)
@@ -38086,13 +38113,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})")
+ logger.info(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}'")
+ logger.info(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)
@@ -38105,11 +38132,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})")
+ logger.info(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")
+ logger.info(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)
@@ -38122,7 +38149,7 @@ 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})")
+ logger.info(f"Strategy 4 YouTube match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Create result entry
result = {
@@ -38176,9 +38203,9 @@ 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})")
+ logger.info(f"CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})")
except Exception as cache_err:
- print(f"Cache save error: {cache_err}")
+ logger.error(f"Cache save error: {cache_err}")
else:
# Auto Wing It fallback ā build stub from raw source data
@@ -38195,10 +38222,10 @@ def _run_youtube_discovery_worker(url_hash):
state['discovery_results'].append(result)
- print(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}")
+ logger.info(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}")
except Exception as e:
- print(f"Error processing track {i}: {e}")
+ logger.error(f"Error processing track {i}: {e}")
result = {
'index': i,
'yt_track': track['name'],
@@ -38253,9 +38280,9 @@ 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}")
+ logger.info(f"Wrote discovery results to DB for {url_hash}")
except Exception as wb_err:
- print(f"Error writing discovery results to DB: {wb_err}")
+ logger.error(f"Error writing discovery results to DB: {wb_err}")
playlist_name = playlist['name']
source_label = discovery_source.upper()
@@ -38265,10 +38292,10 @@ def _run_youtube_discovery_worker(url_hash):
activity_msg += f", {wing_it_count} wing it"
add_activity_item("", f"YouTube Discovery Complete ({source_label})", activity_msg, "Now")
- print(f"YouTube discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched, {wing_it_count} wing it")
+ logger.info(f"YouTube discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched, {wing_it_count} wing it")
except Exception as e:
- print(f"Error in YouTube discovery worker: {e}")
+ logger.error(f"Error in YouTube discovery worker: {e}")
state['status'] = 'error'
state['phase'] = 'fresh'
finally:
@@ -38291,7 +38318,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...")
+ logger.info(f"Starting {discovery_source} discovery for {len(tracks)} ListenBrainz tracks...")
# Store the discovery source in state
state['discovery_source'] = discovery_source
@@ -38301,7 +38328,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')}')")
+ logger.warning(f"ListenBrainz discovery cancelled (phase changed to '{state.get('phase')}')")
return
# Update progress
@@ -38313,7 +38340,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}'")
+ logger.info(f"Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'")
# Check discovery cache first
cache_key = _get_discovery_cache_key(cleaned_title, cleaned_artist)
@@ -38321,7 +38348,7 @@ 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}")
+ logger.debug(f"CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}")
result = {
'index': i,
'lb_track': cleaned_title,
@@ -38340,7 +38367,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}")
+ logger.error(f"Cache lookup error: {cache_err}")
# Try multiple search strategies using matching engine
matched_track = None
@@ -38357,14 +38384,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")
+ logger.info(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}")
+ logger.error(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}")
+ logger.debug(f"ListenBrainz query {query_idx + 1}/{len(search_queries)}: {search_query}")
search_results = None
@@ -38389,22 +38416,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})")
+ logger.info(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")
+ logger.info(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}")
+ logger.debug(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})")
+ logger.info(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)")
+ logger.info("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)
@@ -38418,11 +38445,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})")
+ logger.info(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}'")
+ logger.info(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)
@@ -38436,11 +38463,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})")
+ logger.info(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")
+ logger.info(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)
@@ -38453,7 +38480,7 @@ 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})")
+ logger.info(f"Strategy 4 ListenBrainz match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})")
# Create result entry
result = {
@@ -38507,9 +38534,9 @@ 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})")
+ logger.info(f"CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})")
except Exception as cache_err:
- print(f"Cache save error: {cache_err}")
+ logger.error(f"Cache save error: {cache_err}")
else:
# Auto Wing It fallback ā build stub from raw source data
@@ -38526,10 +38553,10 @@ def _run_listenbrainz_discovery_worker(state_key):
state['discovery_results'].append(result)
- print(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}")
+ logger.info(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}")
except Exception as e:
- print(f"Error processing track {i}: {e}")
+ logger.error(f"Error processing track {i}: {e}")
result = {
'index': i,
'lb_track': track['track_name'],
@@ -38552,10 +38579,10 @@ def _run_listenbrainz_discovery_worker(state_key):
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")
- print(f"ListenBrainz discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched")
+ logger.info(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}")
+ logger.error(f"Error in ListenBrainz discovery worker: {e}")
state['status'] = 'error'
state['phase'] = 'fresh'
finally:
@@ -38631,11 +38658,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(), playlist_image_url)
active_sync_workers[sync_playlist_id] = future
- print(f"Started YouTube sync for: {playlist_name} ({len(spotify_tracks)} tracks)")
+ logger.info(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}")
+ logger.error(f"Error starting YouTube sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/sync/status/', methods=['GET'])
@@ -38679,7 +38706,7 @@ def get_youtube_sync_status(url_hash):
return jsonify(response)
except Exception as e:
- print(f"Error getting YouTube sync status: {e}")
+ logger.error(f"Error getting YouTube sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/sync/cancel/', methods=['POST'])
@@ -38710,7 +38737,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}")
+ logger.error(f"Error cancelling YouTube sync: {e}")
return jsonify({"error": str(e)}), 500
# New YouTube Playlist Management Endpoints (for persistent state)
@@ -38746,11 +38773,11 @@ def get_all_youtube_playlists():
}
playlists.append(playlist_info)
- print(f"Returning {len(playlists)} stored YouTube playlists for hydration")
+ logger.info(f"Returning {len(playlists)} stored YouTube playlists for hydration")
return jsonify({"playlists": playlists})
except Exception as e:
- print(f"Error getting YouTube playlists: {e}")
+ logger.error(f"Error getting YouTube playlists: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/state/', methods=['GET'])
@@ -38784,7 +38811,7 @@ def get_youtube_playlist_state(url_hash):
return jsonify(response)
except Exception as e:
- print(f"Error getting YouTube playlist state: {e}")
+ logger.error(f"Error getting YouTube playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/reset/', methods=['POST'])
@@ -38812,11 +38839,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']}")
+ logger.info(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}")
+ logger.error(f"Error resetting YouTube playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/delete/', methods=['DELETE'])
@@ -38836,11 +38863,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}")
+ logger.info(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}")
+ logger.error(f"Error deleting YouTube playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/youtube/update_phase/', methods=['POST'])
@@ -38865,11 +38892,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}")
+ logger.info(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}")
+ logger.error(f"Error updating YouTube playlist phase: {e}")
return jsonify({"error": str(e)}), 500
def convert_youtube_results_to_spotify_tracks(discovery_results):
@@ -38905,7 +38932,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")
+ logger.info(f"Converted {len(spotify_tracks)} YouTube matches to Spotify tracks for sync")
return spotify_tracks
@@ -38916,8 +38943,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")
+ logger.info(f"[TIMING] _run_sync_task STARTED for playlist '{playlist_name}' at {time.strftime('%H:%M:%S')}")
+ logger.info(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_')
@@ -38945,7 +38972,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...")
+ logger.info(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': ...}]
@@ -39011,9 +39038,9 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
)
tracks.append(track)
if i < 3: # Log first 3 tracks for debugging
- print(f" Track {i+1}: '{track.name}' by {track.artists}")
+ logger.info(f" Track {i+1}: '{track.name}' by {track.artists}")
- print(f"Created {len(tracks)} SpotifyTrack objects")
+ logger.info(f"Created {len(tracks)} SpotifyTrack objects")
playlist = SpotifyPlaylist(
id=playlist_id,
@@ -39025,7 +39052,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")
+ logger.info(f"Created SpotifyPlaylist object: '{playlist.name}' with {playlist.total_tracks} tracks")
first_callback_time = [None] # Use list to allow modification in nested function
@@ -39034,17 +39061,17 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
if first_callback_time[0] is None:
first_callback_time[0] = time.time()
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)")
+ logger.info(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)")
+ logger.info(f"PROGRESS CALLBACK: {progress.current_step} - {progress.current_track}")
+ logger.error(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}")
+ logger.info(f" Updated sync_states for {playlist_id}")
# Update automation progress card
if automation_id:
@@ -39064,7 +39091,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}")
+ logger.error(f"SETUP ERROR in _run_sync_task: {setup_error}")
import traceback
traceback.print_exc()
with sync_lock:
@@ -39078,53 +39105,53 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None,
return
try:
- print(f"Setting up sync service...")
- print(f" sync_service available: {sync_service is not None}")
+ logger.info(f"Setting up sync service...")
+ logger.info(f" sync_service available: {sync_service is not None}")
if sync_service is None:
raise Exception("sync_service is None - not initialized properly")
# Check sync service components
- print(f" spotify_client: {sync_service.spotify_client is not None}")
- print(f" plex_client: {sync_service.plex_client is not None}")
- print(f" jellyfin_client: {sync_service.jellyfin_client is not None}")
+ logger.info(f" spotify_client: {sync_service.spotify_client is not None}")
+ logger.info(f" plex_client: {sync_service.plex_client is not None}")
+ logger.info(f" jellyfin_client: {sync_service.jellyfin_client is not None}")
# Check media server connection before starting
from config.settings import config_manager
active_server = config_manager.get_active_media_server()
- print(f" Active media server: {active_server}")
+ logger.info(f" Active media server: {active_server}")
media_client, server_type = sync_service._get_active_media_client()
- print(f" Media client available: {media_client is not None}")
+ logger.info(f" Media client available: {media_client is not None}")
if media_client:
is_connected = media_client.is_connected()
- print(f" Media client connected: {is_connected}")
+ logger.info(f" Media client connected: {is_connected}")
# Check database access
try:
from database.music_database import MusicDatabase
db = MusicDatabase()
- print(f" Database initialized: {db is not None}")
+ logger.debug(f" Database initialized: {db is not None}")
except Exception as db_error:
- print(f" Database initialization failed: {db_error}")
+ logger.error(f" Database initialization failed: {db_error}")
- print(f"Attaching progress callback...")
+ logger.info(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}")
+ logger.info(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")
+ logger.info(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}")
+ logger.info(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
@@ -39146,9 +39173,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']}")
+ logger.debug(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")
+ logger.warning(f"Sync cache stale for '{original_title}' ā track gone")
except Exception:
pass
# --- End cache fast-path ---
@@ -39170,7 +39197,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})")
+ logger.info(f"Database match: '{db_track.title}' (confidence: {confidence:.2f})")
# Save to sync match cache
if spotify_id:
@@ -39193,21 +39220,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}'")
+ logger.warning(f"No database match found for: '{original_title}'")
return None, 0.0
except Exception as e:
- print(f"Database search error: {e}")
+ logger.error(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")
+ logger.info(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()...")
+ logger.info(f"ā±ļø [TIMING] Setup completed at {time.strftime('%H:%M:%S')} (took {setup_duration:.1f}ms)")
+ logger.info(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
@@ -39227,9 +39254,9 @@ 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" Result details: matched={getattr(result, 'matched_tracks', 'N/A')}, total={getattr(result, 'total_tracks', 'N/A')}")
+ logger.info(f"ā±ļø [TIMING] Sync completed at {time.strftime('%H:%M:%S')} (sync: {sync_duration:.1f}ms, total: {total_duration:.1f}ms)")
+ logger.info(f"Sync process completed! Result type: {type(result)}")
+ logger.info(f" Result details: matched={getattr(result, 'matched_tracks', 'N/A')}, total={getattr(result, 'total_tracks', 'N/A')}")
# Update final state on completion
# Convert result to JSON-serializable dict (datetime/errors can't be emitted via SocketIO)
@@ -39254,24 +39281,24 @@ 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")
+ logger.info(f"Sync finished for {playlist_id} - state updated")
# Set playlist poster image if available (Plex, Jellyfin, Emby)
_synced = getattr(result, 'synced_tracks', 0)
- print(f"[PLAYLIST IMAGE] image_url={playlist_image_url!r}, synced_tracks={_synced}")
+ logger.info(f"[PLAYLIST IMAGE] image_url={playlist_image_url!r}, synced_tracks={_synced}")
if playlist_image_url and _synced > 0:
try:
active_server = config_manager.get_active_media_server()
- print(f"[PLAYLIST IMAGE] active_server={active_server}")
+ logger.info(f"[PLAYLIST IMAGE] active_server={active_server}")
if active_server == 'plex' and plex_client:
ok = plex_client.set_playlist_image(playlist_name, playlist_image_url)
- print(f"[PLAYLIST IMAGE] Plex upload result: {ok}")
+ logger.info(f"[PLAYLIST IMAGE] Plex upload result: {ok}")
elif active_server in ('jellyfin', 'emby') and jellyfin_client:
ok = jellyfin_client.set_playlist_image(playlist_name, playlist_image_url)
- print(f"[PLAYLIST IMAGE] Jellyfin upload result: {ok}")
+ logger.info(f"[PLAYLIST IMAGE] Jellyfin upload result: {ok}")
# Navidrome doesn't support custom playlist images
except Exception as img_err:
- print(f"[PLAYLIST IMAGE] Exception: {img_err}")
+ logger.error(f"[PLAYLIST IMAGE] Exception: {img_err}")
# Record sync history completion with per-track data
try:
@@ -39298,11 +39325,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})")
+ logger.info(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}")
+ logger.error(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}")
+ logger.warning(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}")
@@ -39339,7 +39366,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}")
+ logger.error(f"SYNC FAILED for {playlist_id}: {e}")
import traceback
traceback.print_exc()
with sync_lock:
@@ -39351,21 +39378,21 @@ 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}")
+ logger.info(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}")
+ logger.info(f"Cleanup completed for {playlist_id}")
@app.route('/api/sync/start', methods=['POST'])
def start_playlist_sync():
"""Starts a new sync process for a given playlist."""
request_start_time = time.time()
- print(f"ā±ļø [TIMING] Sync request received at {time.strftime('%H:%M:%S')}")
+ logger.info(f"ā±ļø [TIMING] Sync request received at {time.strftime('%H:%M:%S')}")
data = request.get_json()
playlist_id = data.get('playlist_id')
@@ -39395,10 +39422,10 @@ def start_playlist_sync():
future = sync_executor.submit(_run_sync_task, playlist_id, playlist_name, tracks_json, None, _sync_profile_id, playlist_image_url)
active_sync_workers[playlist_id] = future
thread_submit_duration = (time.time() - thread_submit_time) * 1000
- print(f"ā±ļø [TIMING] Thread submitted at {time.strftime('%H:%M:%S')} (took {thread_submit_duration:.1f}ms)")
+ logger.info(f"ā±ļø [TIMING] Thread submitted at {time.strftime('%H:%M:%S')} (took {thread_submit_duration:.1f}ms)")
total_request_time = (time.time() - request_start_time) * 1000
- print(f"ā±ļø [TIMING] Request completed at {time.strftime('%H:%M:%S')} (total: {total_request_time:.1f}ms)")
+ logger.info(f"ā±ļø [TIMING] Request completed at {time.strftime('%H:%M:%S')} (total: {total_request_time:.1f}ms)")
return jsonify({"success": True, "message": "Sync started."})
@@ -39452,34 +39479,34 @@ 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...")
+ logger.debug(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}")
+ logger.debug(f" Database initialized: {db is not None}")
# Test basic database query
stats = db.get_database_info_for_server()
- print(f" Database stats retrieved: {stats}")
+ logger.debug(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}")
+ logger.info(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}")
+ logger.info(f" Active media server: {active_server}")
# Test media clients
- print(f" Media clients status:")
- print(f" plex_client: {plex_client is not None}")
+ logger.info(f" Media clients status:")
+ logger.info(f" plex_client: {plex_client is not None}")
if plex_client:
- print(f" plex_client.is_connected(): {plex_client.is_connected()}")
- print(f" jellyfin_client: {jellyfin_client is not None}")
+ logger.info(f" plex_client.is_connected(): {plex_client.is_connected()}")
+ logger.info(f" jellyfin_client: {jellyfin_client is not None}")
if jellyfin_client:
- print(f" jellyfin_client.is_connected(): {jellyfin_client.is_connected()}")
+ logger.info(f" jellyfin_client.is_connected(): {jellyfin_client.is_connected()}")
return jsonify({
"success": True,
@@ -39494,7 +39521,7 @@ def test_database_access():
})
except Exception as e:
- print(f" Database test failed: {e}")
+ logger.error(f" Database test failed: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -39523,7 +39550,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")
+ logger.info(f"Saved discover download snapshot: {download_count} downloads")
return jsonify({
'success': True,
@@ -39532,7 +39559,7 @@ def save_discover_download_snapshot():
})
except Exception as e:
- print(f"Error saving discover download snapshot: {e}")
+ logger.error(f"Error saving discover download snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -39568,7 +39595,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}")
+ logger.info(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,
@@ -39576,7 +39603,7 @@ def hydrate_discover_downloads():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"Error checking discover snapshot age: {e}")
+ logger.error(f"Error checking discover snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -39592,11 +39619,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}")
+ logger.error(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")
+ logger.warning(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,
@@ -39611,11 +39638,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']}")
+ logger.info(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")
+ logger.warning(f"No active process for discover download {playlist_id} - marking as completed")
# Create updated download entry
hydrated_downloads[playlist_id] = {
@@ -39631,7 +39658,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")
+ logger.info(f"Hydrated {download_count} discover downloads: {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -39644,7 +39671,7 @@ def hydrate_discover_downloads():
})
except Exception as e:
- print(f"Error hydrating discover downloads: {e}")
+ logger.error(f"Error hydrating discover downloads: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -39672,7 +39699,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")
+ logger.info(f"Saved artist bubble snapshot: {bubble_count} artists")
return jsonify({
'success': True,
@@ -39681,7 +39708,7 @@ def save_artist_bubble_snapshot():
})
except Exception as e:
- print(f"Error saving artist bubble snapshot: {e}")
+ logger.error(f"Error saving artist bubble snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -39717,7 +39744,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}")
+ logger.info(f"Cleaning up old snapshot from {snapshot_time}")
db.delete_bubble_snapshot('artist_bubbles', profile_id=get_current_profile_id())
return jsonify({
'success': True,
@@ -39725,7 +39752,7 @@ def hydrate_artist_bubbles():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"Error checking snapshot age: {e}")
+ logger.error(f"Error checking snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -39741,11 +39768,11 @@ def hydrate_artist_bubbles():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"Error fetching active processes for hydration: {e}")
+ logger.error(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")
+ logger.warning(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,
@@ -39769,11 +39796,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']}")
+ logger.info(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")
+ logger.warning(f"No active process for {download['album']['name']} - marking as completed")
# Create updated download entry
updated_download = {
@@ -39802,7 +39829,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")
+ logger.info(f"Hydrated {bubble_count} artist bubbles: {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -39816,7 +39843,7 @@ def hydrate_artist_bubbles():
})
except Exception as e:
- print(f"Error hydrating artist bubbles: {e}")
+ logger.error(f"Error hydrating artist bubbles: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -39844,7 +39871,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")
+ logger.info(f"Saved search bubble snapshot: {bubble_count} albums/tracks")
return jsonify({
'success': True,
@@ -39853,7 +39880,7 @@ def save_search_bubble_snapshot():
})
except Exception as e:
- print(f"Error saving search bubble snapshot: {e}")
+ logger.error(f"Error saving search bubble snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -39889,7 +39916,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}")
+ logger.info(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,
@@ -39897,7 +39924,7 @@ def hydrate_search_bubbles():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"Error checking snapshot age: {e}")
+ logger.error(f"Error checking snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -39913,11 +39940,11 @@ def hydrate_search_bubbles():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"Error fetching active processes for hydration: {e}")
+ logger.error(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")
+ logger.warning(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,
@@ -39940,11 +39967,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']}")
+ logger.info(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")
+ logger.warning(f"No active process for {download['item']['name']} - marking as completed")
# Create updated download entry
updated_download = {
@@ -39969,7 +39996,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")
+ logger.info(f"Hydrated {bubble_count} search bubbles (artists): {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -39983,7 +40010,7 @@ def hydrate_search_bubbles():
})
except Exception as e:
- print(f"Error hydrating search bubbles: {e}")
+ logger.error(f"Error hydrating search bubbles: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -40007,7 +40034,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")
+ logger.info(f"Saved Beatport bubble snapshot: {bubble_count} charts")
return jsonify({
'success': True,
@@ -40016,7 +40043,7 @@ def save_beatport_bubble_snapshot():
})
except Exception as e:
- print(f"Error saving Beatport bubble snapshot: {e}")
+ logger.error(f"Error saving Beatport bubble snapshot: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -40049,7 +40076,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}")
+ logger.info(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,
@@ -40057,7 +40084,7 @@ def hydrate_beatport_bubbles():
'message': 'Old snapshot cleaned up'
})
except ValueError as e:
- print(f"Error checking Beatport snapshot age: {e}")
+ logger.error(f"Error checking Beatport snapshot age: {e}")
# Get current active download processes for live status
current_processes = {}
@@ -40073,11 +40100,11 @@ def hydrate_beatport_bubbles():
'phase': batch_data.get('phase')
}
except Exception as e:
- print(f"Error fetching active processes for Beatport hydration: {e}")
+ logger.error(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")
+ logger.warning(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,
@@ -40116,7 +40143,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")
+ logger.info(f"Hydrated {bubble_count} Beatport bubbles: {active_count} active, {completed_count} completed")
return jsonify({
'success': True,
@@ -40129,7 +40156,7 @@ def hydrate_beatport_bubbles():
})
except Exception as e:
- print(f"Error hydrating Beatport bubbles: {e}")
+ logger.error(f"Error hydrating Beatport bubbles: {e}")
import traceback
traceback.print_exc()
return jsonify({
@@ -40675,7 +40702,7 @@ def get_watchlist_count():
"next_run_in_seconds": next_run_in_seconds
})
except Exception as e:
- print(f"Error getting watchlist count: {e}")
+ logger.error(f"Error getting watchlist count: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/artists', methods=['GET'])
@@ -40711,7 +40738,7 @@ def get_watchlist_artists():
return jsonify({"success": True, "artists": artists_data})
except Exception as e:
- print(f"Error getting watchlist artists: {e}")
+ logger.error(f"Error getting watchlist artists: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/add', methods=['POST'])
@@ -40782,18 +40809,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'}")
+ logger.info(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'}")
+ logger.info(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}")
+ logger.info(f"Fetching iTunes artist image: {itunes_url}")
resp = requests.get(itunes_url, timeout=5)
image_url = None
@@ -40809,11 +40836,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}")
+ logger.warning(f"Cached {fallback_source} artist image for {artist_name}")
else:
- print(f"No artwork found for {fallback_source} artist {artist_name}")
+ logger.warning(f"No artwork found for {fallback_source} artist {artist_name}")
except Exception as fb_error:
- print(f"Error fetching {fallback_source} artwork: {fb_error}")
+ logger.error(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)
@@ -40828,16 +40855,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}")
+ logger.info(f"Cached artist image for {artist_name}")
else:
- print(f"No image URL found for {artist_name}")
+ logger.warning(f"No image URL found for {artist_name}")
else:
- print(f"No images in Spotify data for {artist_name}")
+ logger.warning(f"No images in Spotify data for {artist_name}")
else:
- print(f"Spotify client not available for fetching artist image")
+ logger.info(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}")
+ logger.error(f"Could not fetch artist image for {artist_name}: {img_error}")
# Push updated count to this profile's WebSocket room immediately
try:
@@ -40859,7 +40886,7 @@ def add_to_watchlist():
return jsonify({"success": False, "error": "Failed to add artist to watchlist"}), 500
except Exception as e:
- print(f"Error adding to watchlist: {e}")
+ logger.error(f"Error adding to watchlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/remove', methods=['POST'])
@@ -40896,7 +40923,7 @@ def remove_from_watchlist():
return jsonify({"success": False, "error": "Failed to remove artist from watchlist"}), 500
except Exception as e:
- print(f"Error removing from watchlist: {e}")
+ logger.error(f"Error removing from watchlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/add-batch', methods=['POST'])
@@ -40959,7 +40986,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}")
+ logger.error(f"Could not fetch artist image for {artist_name}: {img_error}")
return jsonify({
"success": True,
@@ -40969,7 +40996,7 @@ def add_batch_to_watchlist():
})
except Exception as e:
- print(f"Error batch adding to watchlist: {e}")
+ logger.error(f"Error batch adding to watchlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/library/watchlist-all-unwatched', methods=['POST'])
@@ -41049,7 +41076,7 @@ def watchlist_all_unwatched_library_artists():
})
except Exception as e:
- print(f"Error bulk watchlisting library artists: {e}")
+ logger.error(f"Error bulk watchlisting library artists: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -41077,7 +41104,7 @@ def remove_batch_from_watchlist():
})
except Exception as e:
- print(f"Error batch removing from watchlist: {e}")
+ logger.error(f"Error batch removing from watchlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/check', methods=['POST'])
@@ -41096,7 +41123,7 @@ def check_watchlist_status():
return jsonify({"success": True, "is_watching": is_watching})
except Exception as e:
- print(f"Error checking watchlist status: {e}")
+ logger.error(f"Error checking watchlist status: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/check-batch', methods=['POST'])
@@ -41117,7 +41144,7 @@ def check_watchlist_status_batch():
return jsonify({"success": True, "results": results})
except Exception as e:
- print(f"Error batch checking watchlist status: {e}")
+ logger.error(f"Error batch checking watchlist status: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/scan', methods=['POST'])
@@ -41160,7 +41187,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}")
+ logger.info(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()
@@ -41195,17 +41222,17 @@ def start_watchlist_scan():
pass
for _bf_provider in providers_to_backfill:
try:
- print(f"Checking for missing {_bf_provider} IDs in watchlist...")
+ logger.debug(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}")
+ logger.error(f"Error during {_bf_provider} ID backfilling: {backfill_error}")
# Continue with next provider
try:
filled = scanner.backfill_watchlist_artist_images(scan_profile_id)
if filled:
- print(f"Backfilled {filled} watchlist artist images")
+ logger.info(f"Backfilled {filled} watchlist artist images")
except Exception as img_err:
- print(f"Image backfill error: {img_err}")
+ logger.error(f"Image backfill error: {img_err}")
# Initialize detailed progress tracking
watchlist_scan_state.update({
@@ -41255,26 +41282,26 @@ def start_watchlist_scan():
'tracks_added_to_wishlist': total_added_to_wishlist
}
- 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")
+ logger.info(f"Watchlist scan completed: {len(successful_scans)}/{len(scan_results)} artists scanned successfully")
+ logger.info(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist")
else:
- print(f"Watchlist scan cancelled ā skipping post-scan steps")
+ logger.warning(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...")
+ logger.info("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")
+ logger.info("Discovery pool population complete")
except Exception as discovery_error:
- print(f"Error populating discovery pool: {discovery_error}")
+ logger.error(f"Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
# Update ListenBrainz playlists cache
- print("Starting ListenBrainz playlists update...")
+ logger.info("Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
try:
from core.listenbrainz_manager import ListenBrainzManager
@@ -41287,22 +41314,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', {})}")
+ logger.info(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', {})}")
+ logger.info(f"ListenBrainz update complete (global): {lb_result.get('summary', {})}")
elif lb_result.get('error'):
- print(f"ListenBrainz update skipped: {lb_result.get('error')}")
+ logger.error(f"ListenBrainz update skipped: {lb_result.get('error')}")
except Exception as lb_error:
- print(f"Error updating ListenBrainz: {lb_error}")
+ logger.error(f"Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
# Update current seasonal playlist (weekly refresh)
- print("Starting seasonal content update...")
+ logger.info("Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
try:
from core.seasonal_discovery import get_seasonal_discovery_service
@@ -41312,39 +41339,39 @@ 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...")
+ logger.info(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")
+ logger.info(f"{current_season.capitalize()} seasonal content updated")
else:
- print(f"{current_season.capitalize()} seasonal content recently updated, skipping")
+ logger.info(f"{current_season.capitalize()} seasonal content recently updated, skipping")
else:
- print("ā¹ļø No active season at this time")
+ logger.warning("ā¹ļø No active season at this time")
except Exception as seasonal_error:
- print(f"Error updating seasonal content: {seasonal_error}")
+ logger.error(f"Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
# Generate Last.fm radio playlists (weekly refresh)
- print("Starting Last.fm radio generation...")
+ logger.info("Starting Last.fm radio generation...")
watchlist_scan_state['current_phase'] = 'generating_lastfm_radio'
try:
scanner._generate_lastfm_radio_playlists()
- print("Last.fm radio generation complete")
+ logger.info("Last.fm radio generation complete")
except Exception as lastfm_error:
- print(f"Error generating Last.fm radio playlists: {lastfm_error}")
+ logger.error(f"Error generating Last.fm radio playlists: {lastfm_error}")
# Sync Spotify library cache
- print("Syncing Spotify library cache...")
+ logger.info("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")
+ logger.info("Spotify library cache sync complete")
except Exception as lib_error:
- print(f"Error syncing Spotify library: {lib_error}")
+ logger.error(f"Error syncing Spotify library: {lib_error}")
except Exception as e:
- print(f"Error during watchlist scan: {e}")
+ logger.error(f"Error during watchlist scan: {e}")
watchlist_scan_state['status'] = 'error'
watchlist_scan_state['error'] = str(e)
@@ -41362,7 +41389,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")
+ logger.info("[Manual Watchlist Scan] Flag reset - scan complete")
# Initialize scan state
global watchlist_scan_state
@@ -41383,7 +41410,7 @@ def start_watchlist_scan():
return jsonify({"success": True, "message": "Watchlist scan started"})
except Exception as e:
- print(f"Error starting watchlist scan: {e}")
+ logger.error(f"Error starting watchlist scan: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/scan/status', methods=['GET'])
@@ -41413,7 +41440,7 @@ def get_watchlist_scan_status():
return jsonify({"success": True, **state})
except Exception as e:
- print(f"Error getting watchlist scan status: {e}")
+ logger.error(f"Error getting watchlist scan status: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/scan/cancel', methods=['POST'])
@@ -41425,11 +41452,11 @@ 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")
+ logger.info("[Watchlist Scan] Cancel requested by user")
return jsonify({"success": True, "message": "Cancel request sent"})
except Exception as e:
- print(f"Error cancelling watchlist scan: {e}")
+ logger.error(f"Error cancelling watchlist scan: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# Similar Artists Update State
@@ -41469,7 +41496,7 @@ def update_similar_artists_endpoint():
return jsonify({"success": True, "message": "Similar artists update started"})
except Exception as e:
- print(f"Error starting similar artists update: {e}")
+ logger.error(f"Error starting similar artists update: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/similar-artists-status', methods=['GET'])
@@ -41479,7 +41506,7 @@ def get_similar_artists_update_status():
global similar_artists_update_state
return jsonify({"success": True, **similar_artists_update_state})
except Exception as e:
- print(f"Error getting similar artists status: {e}")
+ logger.error(f"Error getting similar artists status: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/artist//config', methods=['GET', 'POST'])
@@ -41533,7 +41560,7 @@ def watchlist_artist_config(artist_id):
'genres': artist_data.get('genres', [])
}
except Exception as e:
- print(f"Warning: Could not fetch artist info from Spotify: {e}")
+ logger.error(f"Could not fetch artist info from Spotify: {e}")
# Fallback to database info if Spotify fetch failed
if not artist_info:
@@ -41590,7 +41617,7 @@ def watchlist_artist_config(artist_id):
]
conn2.close()
except Exception as e:
- print(f"Warning: Could not enrich artist from library: {e}")
+ logger.error(f"Could not enrich artist from library: {e}")
releases = []
config = {
@@ -41681,7 +41708,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}")
+ logger.info(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,
@@ -41699,7 +41726,7 @@ def watchlist_artist_config(artist_id):
})
except Exception as e:
- print(f"Error in watchlist artist config: {e}")
+ logger.error(f"Error in watchlist artist config: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -41764,7 +41791,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'}")
+ logger.info(f"{action} watchlist artist '{artist_name}' {provider} ID: {new_provider_id or 'NULL'}")
return jsonify({
"success": True,
@@ -41773,7 +41800,7 @@ def watchlist_artist_link_provider(artist_id):
})
except Exception as e:
- print(f"Error linking watchlist artist provider: {e}")
+ logger.error(f"Error linking watchlist artist provider: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -41828,7 +41855,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}, "
+ logger.info(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}, "
@@ -41852,7 +41879,7 @@ def watchlist_global_config():
})
except Exception as e:
- print(f"Error in watchlist global config: {e}")
+ logger.error(f"Error in watchlist global config: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -41866,7 +41893,7 @@ def _update_similar_artists_worker():
from database.music_database import get_database
import time
- print("[Similar Artists] Starting similar artists update...")
+ logger.info("[Similar Artists] Starting similar artists update...")
database = get_database()
all_profiles = database.get_all_profiles()
@@ -41883,11 +41910,11 @@ def _update_similar_artists_worker():
if not artist_profiles:
similar_artists_update_state['status'] = 'completed'
- print("[Similar Artists] No watchlist artists to process")
+ logger.warning("[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")
+ logger.info(f"[Similar Artists] Processing {len(artist_profiles)} unique watchlist artists across {len(all_profiles)} profiles")
scanner = get_watchlist_scanner(spotify_client)
@@ -41896,7 +41923,7 @@ def _update_similar_artists_worker():
similar_artists_update_state['artists_processed'] = idx
similar_artists_update_state['current_artist'] = artist.artist_name
- print(f"[{idx}/{len(artist_profiles)}] Updating similar artists for {artist.artist_name} (profiles: {profile_ids})")
+ logger.info(f"[{idx}/{len(artist_profiles)}] Updating similar artists for {artist.artist_name} (profiles: {profile_ids})")
# Update similar artists for each profile that watches this artist
for pid in profile_ids:
@@ -41907,17 +41934,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}")
+ logger.error(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")
+ logger.info(f"[Similar Artists] Update complete! Processed {len(artist_profiles)} artists")
except Exception as e:
- print(f"[Similar Artists] Critical error: {e}")
+ logger.error(f"[Similar Artists] Critical error: {e}")
import traceback
traceback.print_exc()
@@ -41944,7 +41971,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})...")
+ logger.info(f"[Auto-Watchlist] Timer triggered - starting automatic watchlist scan ({scope_label})...")
_ew_state = {}
@@ -41952,20 +41979,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.")
+ logger.info("[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.")
+ logger.info("[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}")
+ logger.info(f"[Auto-Watchlist] Flag set at timestamp {watchlist_auto_scanning_timestamp}")
# Use app context for database operations
with app.app_context():
@@ -41984,23 +42011,23 @@ 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})")
+ logger.info(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.")
+ logger.warning("ā¹ļø [Auto-Watchlist] No artists in watchlist for auto-scanning.")
with watchlist_timer_lock:
watchlist_auto_scanning = False
watchlist_auto_scanning_timestamp = 0
return
if not spotify_client or not spotify_client.is_authenticated():
- print("ā¹ļø [Auto-Watchlist] Spotify client not available or not authenticated.")
+ logger.info("ā¹ļø [Auto-Watchlist] Spotify client not available or not authenticated.")
with watchlist_timer_lock:
watchlist_auto_scanning = False
watchlist_auto_scanning_timestamp = 0
return
- print(f"[Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...")
+ logger.info(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')
@@ -42015,9 +42042,9 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
try:
filled = scanner.backfill_watchlist_artist_images(p['id'])
if filled:
- print(f"Backfilled {filled} watchlist artist images for profile {p['id']}")
+ logger.info(f"Backfilled {filled} watchlist artist images for profile {p['id']}")
except Exception as img_err:
- print(f"Image backfill error for profile {p['id']}: {img_err}")
+ logger.error(f"Image backfill error for profile {p['id']}: {img_err}")
# Initialize detailed progress tracking (same as manual scan)
watchlist_scan_state = {
@@ -42138,20 +42165,20 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
'tracks_added_to_wishlist': total_added_to_wishlist
}
- print(f"Automatic 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")
+ logger.info(f"Automatic watchlist scan completed: {len(successful_scans)}/{len(scan_results)} artists scanned successfully")
+ logger.info(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist")
_update_automation_progress(automation_id, progress=95, phase='Scan complete',
log_line=f'Scanned {len(successful_scans)} artists ā {total_new_tracks} new tracks, {total_added_to_wishlist} added to wishlist',
log_type='success' if total_new_tracks > 0 else 'info')
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")
+ logger.warning(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...")
+ logger.info("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')
@@ -42173,16 +42200,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")
+ logger.info("Discovery pool population complete")
except Exception as discovery_error:
- print(f"Error populating discovery pool: {discovery_error}")
+ logger.error(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...")
+ logger.info("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')
@@ -42197,7 +42224,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}")
+ logger.info(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:
@@ -42205,22 +42232,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}")
+ logger.info(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')}")
+ logger.error(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}")
+ logger.error(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...")
+ logger.info("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')
@@ -42232,54 +42259,54 @@ 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...")
+ logger.info(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")
+ logger.info(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")
+ logger.info(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:
- print("ā¹ļø No active season at this time")
+ logger.warning("ā¹ļø No active season at this time")
_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}")
+ logger.error(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')
# Generate Last.fm radio playlists (weekly refresh)
- print("Starting Last.fm radio generation...")
+ logger.info("Starting Last.fm radio generation...")
watchlist_scan_state['current_phase'] = 'generating_lastfm_radio'
_update_automation_progress(automation_id, progress=99, phase='Generating Last.fm radio',
log_line='Building Last.fm radio playlists...', log_type='info')
try:
scanner._generate_lastfm_radio_playlists()
- print("Last.fm radio generation complete")
+ logger.info("Last.fm radio generation complete")
_update_automation_progress(automation_id,
log_line='Last.fm radio playlists updated', log_type='success')
except Exception as lastfm_error:
- print(f"Error generating Last.fm radio playlists: {lastfm_error}")
+ logger.error(f"Error generating Last.fm radio playlists: {lastfm_error}")
_update_automation_progress(automation_id,
log_line=f'Last.fm radio error: {lastfm_error}', log_type='error')
# Sync Spotify library cache
- print("Syncing Spotify library cache...")
+ logger.info("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")
+ logger.info("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}")
+ logger.error(f"Error syncing Spotify library: {lib_error}")
_update_automation_progress(automation_id,
log_line=f'Library cache error: {lib_error}', log_type='error')
@@ -42298,7 +42325,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}")
+ logger.error(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')
@@ -42322,7 +42349,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")
+ logger.info("Automatic watchlist scanning complete")
# --- Metadata Updater System ---
@@ -42371,7 +42398,7 @@ def get_discover_hero():
# Determine active source
active_source = _get_active_discovery_source()
- print(f"Discover hero using source: {active_source}")
+ logger.info(f"Discover hero using source: {active_source}")
# Import fallback client for non-Spotify lookups
itunes_client = _get_metadata_fallback_client()
@@ -42379,12 +42406,12 @@ def get_discover_hero():
# Get top similar artists (excluding watchlist, cycled by last_featured)
# Fetch more than needed since strict source filtering may drop many
pid = get_current_profile_id()
- print(f"[Discover Hero] Profile ID: {pid}, Active source: {active_source}")
+ logger.info(f"[Discover Hero] Profile ID: {pid}, Active source: {active_source}")
similar_artists = database.get_top_similar_artists(limit=200, profile_id=pid, require_source=active_source)
# FALLBACK: If no similar artists exist, use watchlist artists for Hero section
if not similar_artists:
- print("[Discover Hero] No similar artists found, falling back to watchlist artists")
+ logger.warning("[Discover Hero] No similar artists found, falling back to watchlist artists")
watchlist_artists = database.get_watchlist_artists(profile_id=pid)
if not watchlist_artists:
@@ -42423,7 +42450,7 @@ def get_discover_hero():
hero_artists.append(artist_data)
- print(f"[Discover Hero] Returning {len(hero_artists)} watchlist artists as fallback")
+ logger.warning(f"[Discover Hero] Returning {len(hero_artists)} watchlist artists as fallback")
return jsonify({"success": True, "artists": hero_artists, "source": active_source, "fallback": "watchlist"})
# Artists are already filtered by source in SQL ā no post-filter needed
@@ -42431,7 +42458,7 @@ def get_discover_hero():
# FALLBACK: If no valid artists for fallback source, try to resolve IDs on-the-fly
if active_source in ('itunes', 'deezer') and not valid_artists:
- print(f"[{active_source} Fallback] No artists with {active_source} IDs found, attempting on-the-fly resolution for {len(similar_artists)} artists")
+ logger.warning(f"[{active_source} Fallback] No artists with {active_source} IDs found, attempting on-the-fly resolution for {len(similar_artists)} artists")
resolved_count = 0
for artist in similar_artists:
existing_id = getattr(artist, f'similar_artist_{active_source}_id', None) or (artist.similar_artist_itunes_id if active_source == 'itunes' else None)
@@ -42452,15 +42479,15 @@ def get_discover_hero():
artist.similar_artist_itunes_id = resolved_id
valid_artists.append(artist)
resolved_count += 1
- print(f" [Resolved] {artist.similar_artist_name} -> {active_source} ID: {resolved_id}")
+ logger.info(f" [Resolved] {artist.similar_artist_name} -> {active_source} ID: {resolved_id}")
except Exception as resolve_err:
- print(f" [Failed] Could not resolve {active_source} ID for {artist.similar_artist_name}: {resolve_err}")
+ logger.error(f" [Failed] Could not resolve {active_source} ID for {artist.similar_artist_name}: {resolve_err}")
# Stop after 10 successful resolutions to avoid rate limiting
if len(valid_artists) >= 10:
break
- print(f"[{active_source} Fallback] Resolved {resolved_count} artists with IDs")
+ logger.warning(f"[{active_source} Fallback] Resolved {resolved_count} artists with IDs")
- print(f"[Discover Hero] Found {len(valid_artists)} valid artists for source: {active_source}")
+ logger.info(f"[Discover Hero] Found {len(valid_artists)} valid artists for source: {active_source}")
# Filter out blacklisted artists
blacklisted = database.get_discovery_blacklist_names()
@@ -42528,7 +42555,7 @@ def get_discover_hero():
artist_data.get('genres'), artist_data.get('popularity')
)
except Exception as img_err:
- print(f"Could not fetch artist image: {img_err}")
+ logger.error(f"Could not fetch artist image: {img_err}")
hero_artists.append(artist_data)
@@ -42539,7 +42566,7 @@ def get_discover_hero():
return jsonify({"success": True, "artists": hero_artists, "source": active_source})
except Exception as e:
- print(f"Error getting discover hero: {e}")
+ logger.error(f"Error getting discover hero: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -42584,7 +42611,7 @@ def get_discover_similar_artists():
artist_data["popularity"] = artist.popularity
result_artists.append(artist_data)
- print(f"[Similar Artists] {len(similar_artists)} from DB, {len(result_artists)} valid for {active_source}")
+ logger.info(f"[Similar Artists] {len(similar_artists)} from DB, {len(result_artists)} valid for {active_source}")
return jsonify({
"success": True,
@@ -42594,7 +42621,7 @@ def get_discover_similar_artists():
})
except Exception as e:
- print(f"Error getting similar artists: {e}")
+ logger.error(f"Error getting similar artists: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -42668,7 +42695,7 @@ def enrich_similar_artists():
except Exception as e:
from core.spotify_client import _detect_and_set_rate_limit
_detect_and_set_rate_limit(e, 'enrich_similar_artists')
- print(f"Error enriching Spotify batch: {e}")
+ logger.error(f"Error enriching Spotify batch: {e}")
else:
fallback_client = _get_metadata_fallback_client()
fallback_source = _get_metadata_fallback_source()
@@ -42695,12 +42722,12 @@ def enrich_similar_artists():
cached_count = len(enriched) - len([aid for aid in uncached_ids if aid in enriched])
api_count = len([aid for aid in uncached_ids if aid in enriched])
if uncached_ids:
- print(f"[Enrich] {cached_count} from cache, {api_count} from API ({len(uncached_ids) - api_count} missed)")
+ logger.warning(f"[Enrich] {cached_count} from cache, {api_count} from API ({len(uncached_ids) - api_count} missed)")
return jsonify({"success": True, "artists": enriched})
except Exception as e:
- print(f"Error enriching similar artists: {e}")
+ logger.error(f"Error enriching similar artists: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -42783,7 +42810,7 @@ def get_spotify_library():
})
except Exception as e:
- print(f"Error getting Spotify library: {e}")
+ logger.error(f"Error getting Spotify library: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -42801,9 +42828,9 @@ 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")
+ logger.info("Manual Spotify library refresh complete")
except Exception as e:
- print(f"Error in manual Spotify library refresh: {e}")
+ logger.error(f"Error in manual Spotify library refresh: {e}")
import threading
thread = threading.Thread(target=_run_sync, daemon=True)
@@ -42812,7 +42839,7 @@ def refresh_spotify_library():
return jsonify({"success": True, "message": "Spotify library refresh started"})
except Exception as e:
- print(f"Error starting Spotify library refresh: {e}")
+ logger.error(f"Error starting Spotify library refresh: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -42870,7 +42897,7 @@ def get_discover_recent_releases():
return jsonify({"success": True, "albums": albums, "source": active_source})
except Exception as e:
- print(f"Error getting recent releases: {e}")
+ logger.error(f"Error getting recent releases: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -42936,7 +42963,7 @@ def get_discover_release_radar():
return jsonify({"success": True, "tracks": [], "source": active_source})
except Exception as e:
- print(f"Error getting release radar: {e}")
+ logger.error(f"Error getting release radar: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -43249,7 +43276,7 @@ def get_discover_weekly():
return jsonify({"success": True, "tracks": [], "source": active_source})
except Exception as e:
- print(f"Error getting discovery weekly: {e}")
+ logger.error(f"Error getting discovery weekly: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -43265,16 +43292,16 @@ def refresh_discover_data():
database = get_database()
scanner = WatchlistScanner(spotify_client, database)
- print("[Discover Refresh] Starting forced refresh of discover data...")
+ logger.info("[Discover Refresh] Starting forced refresh of discover data...")
refresh_pid = get_current_profile_id()
# Cache recent albums from watchlist and similar artists
- print("[Discover Refresh] Caching recent albums...")
+ logger.info("[Discover Refresh] Caching recent albums...")
scanner.cache_discovery_recent_albums(profile_id=refresh_pid)
# Curate playlists
- print("[Discover Refresh] Curating discovery playlists...")
+ logger.info("[Discover Refresh] Curating discovery playlists...")
scanner.curate_discovery_playlists(profile_id=refresh_pid)
# Get counts for response
@@ -43284,7 +43311,7 @@ def refresh_discover_data():
release_radar = database.get_curated_playlist(f'release_radar_{active_source}', profile_id=pid) or []
discovery_weekly = database.get_curated_playlist(f'discovery_weekly_{active_source}', profile_id=pid) or []
- print(f"[Discover Refresh] Complete! Recent albums: {len(recent_albums)}, Release Radar: {len(release_radar)} tracks, Discovery Weekly: {len(discovery_weekly)} tracks")
+ logger.info(f"[Discover Refresh] Complete! Recent albums: {len(recent_albums)}, Release Radar: {len(release_radar)} tracks, Discovery Weekly: {len(discovery_weekly)} tracks")
return jsonify({
"success": True,
@@ -43296,7 +43323,7 @@ def refresh_discover_data():
})
except Exception as e:
- print(f"Error refreshing discover data: {e}")
+ logger.error(f"Error refreshing discover data: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -43367,7 +43394,7 @@ def diagnose_discover_data():
})
except Exception as e:
- print(f"Error diagnosing discover data: {e}")
+ logger.error(f"Error diagnosing discover data: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@@ -43412,7 +43439,7 @@ def get_current_seasonal_content():
})
except Exception as e:
- print(f"Error getting current seasonal content: {e}")
+ logger.error(f"Error getting current seasonal content: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/seasonal//albums', methods=['GET'])
@@ -43441,7 +43468,7 @@ def get_seasonal_albums(season_key):
})
except Exception as e:
- print(f"Error getting seasonal albums: {e}")
+ logger.error(f"Error getting seasonal albums: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/seasonal//playlist', methods=['GET'])
@@ -43539,7 +43566,7 @@ def get_seasonal_playlist(season_key):
})
except Exception as e:
- print(f"Error getting seasonal playlist: {e}")
+ logger.error(f"Error getting seasonal playlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -43559,14 +43586,14 @@ def refresh_seasonal_content():
try:
current_season = seasonal_service.get_current_season()
if current_season:
- print(f"Force-refreshing seasonal content for: {current_season}")
+ logger.info(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}")
+ logger.info(f"Seasonal content refreshed for: {current_season}")
else:
- print("ā¹ļø No active season to refresh")
+ logger.warning("ā¹ļø No active season to refresh")
except Exception as e:
- print(f"Error in background seasonal population: {e}")
+ logger.error(f"Error in background seasonal population: {e}")
thread = threading.Thread(target=populate_all, daemon=True)
thread.start()
@@ -43574,7 +43601,7 @@ def refresh_seasonal_content():
return jsonify({"success": True, "message": "Seasonal content refresh started"})
except Exception as e:
- print(f"Error refreshing seasonal content: {e}")
+ logger.error(f"Error refreshing seasonal content: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ========================================
@@ -43598,7 +43625,7 @@ def get_recently_added_playlist():
})
except Exception as e:
- print(f"Error getting recently added playlist: {e}")
+ logger.error(f"Error getting recently added playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/top-tracks', methods=['GET'])
@@ -43618,7 +43645,7 @@ def get_top_tracks_playlist():
})
except Exception as e:
- print(f"Error getting top tracks playlist: {e}")
+ logger.error(f"Error getting top tracks playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/forgotten-favorites', methods=['GET'])
@@ -43638,7 +43665,7 @@ def get_forgotten_favorites_playlist():
})
except Exception as e:
- print(f"Error getting forgotten favorites playlist: {e}")
+ logger.error(f"Error getting forgotten favorites playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/decade/', methods=['GET'])
@@ -43659,7 +43686,7 @@ def get_decade_playlist(decade):
})
except Exception as e:
- print(f"Error getting decade playlist: {e}")
+ logger.error(f"Error getting decade playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/popular-picks', methods=['GET'])
@@ -43679,7 +43706,7 @@ def get_popular_picks_playlist():
})
except Exception as e:
- print(f"Error getting popular picks playlist: {e}")
+ logger.error(f"Error getting popular picks playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/hidden-gems', methods=['GET'])
@@ -43699,7 +43726,7 @@ def get_hidden_gems_playlist():
})
except Exception as e:
- print(f"Error getting hidden gems playlist: {e}")
+ logger.error(f"Error getting hidden gems playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/daily-mixes', methods=['GET'])
@@ -43719,7 +43746,7 @@ def get_daily_mixes():
})
except Exception as e:
- print(f"Error getting daily mixes: {e}")
+ logger.error(f"Error getting daily mixes: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -43742,7 +43769,7 @@ def get_discovery_shuffle():
})
except Exception as e:
- print(f"Error getting discovery shuffle playlist: {e}")
+ logger.error(f"Error getting discovery shuffle playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/familiar-favorites', methods=['GET'])
@@ -43763,7 +43790,7 @@ def get_familiar_favorites():
})
except Exception as e:
- print(f"Error getting familiar favorites playlist: {e}")
+ logger.error(f"Error getting familiar favorites playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/artist-blacklist', methods=['GET'])
@@ -43907,7 +43934,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")
+ logger.info(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:
@@ -43988,9 +44015,9 @@ def _fetch_and_match_liked_artists(profile_id: int):
# 1. Fetch from Spotify (followed artists)
try:
if 'spotify' not in enabled_sources:
- print("[Your Artists] Spotify skipped (disabled in sources config)")
+ logger.warning("[Your Artists] Spotify skipped (disabled in sources config)")
elif spotify_client and spotify_client.is_spotify_authenticated():
- print("[Your Artists] Fetching followed artists from Spotify...")
+ logger.info("[Your Artists] Fetching followed artists from Spotify...")
artists = spotify_client.get_followed_artists()
for a in artists:
database.upsert_liked_artist(
@@ -44000,18 +44027,18 @@ 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")
+ logger.info(f"[Your Artists] Fetched {len(artists)} from Spotify")
except Exception as e:
logger.error(f"[Your Artists] Spotify fetch error: {e}")
# 2. Fetch from Tidal (favorite artists)
try:
if 'tidal' not in enabled_sources:
- print("[Your Artists] Tidal skipped (disabled in sources config)")
+ logger.warning("[Your Artists] Tidal skipped (disabled in sources config)")
elif 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...")
+ logger.info("[Your Artists] Fetching favorite artists from Tidal...")
artists = tidal_client.get_favorite_artists(limit=200)
for a in artists:
database.upsert_liked_artist(
@@ -44019,26 +44046,26 @@ 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")
+ logger.info(f"[Your Artists] Fetched {len(artists)} from Tidal")
except Exception as e:
logger.error(f"[Your Artists] Tidal fetch error: {e}")
# 3. Fetch from Last.fm (top artists)
try:
if 'lastfm' not in enabled_sources:
- print("[Your Artists] Last.fm skipped (disabled in sources config)")
+ logger.warning("[Your Artists] Last.fm skipped (disabled in sources config)")
else:
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'}")
+ logger.info(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'}")
+ logger.info(f"[Your Artists] Last.fm username resolved: {username or 'NONE'}")
if username:
- print(f"[Your Artists] Fetching top artists from Last.fm ({username})...")
+ logger.info(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(
@@ -44046,23 +44073,23 @@ 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")
+ logger.info(f"[Your Artists] Fetched {len(artists)} from Last.fm")
except Exception as e:
logger.error(f"[Your Artists] Last.fm fetch error: {e}")
# 4. Fetch from Deezer (favorite artists ā OAuth or ARL)
try:
if 'deezer' not in enabled_sources:
- print("[Your Artists] Deezer skipped (disabled in sources config)")
+ logger.warning("[Your Artists] Deezer skipped (disabled in sources config)")
else:
deezer_cl = _get_deezer_client()
artists = []
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
- print("[Your Artists] Fetching favorite artists from Deezer (OAuth)...")
+ logger.info("[Your Artists] Fetching favorite artists from Deezer (OAuth)...")
artists = deezer_cl.get_user_favorite_artists(limit=200)
elif (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.deezer_dl
and soulseek_client.deezer_dl.is_authenticated()):
- print("[Your Artists] Fetching favorite artists from Deezer (ARL)...")
+ logger.info("[Your Artists] Fetching favorite artists from Deezer (ARL)...")
artists = soulseek_client.deezer_dl.get_user_favorite_artists(limit=200)
for a in artists:
database.upsert_liked_artist(
@@ -44072,11 +44099,11 @@ def _fetch_and_match_liked_artists(profile_id: int):
)
fetched += len(artists)
if artists:
- print(f"[Your Artists] Fetched {len(artists)} from Deezer")
+ logger.info(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}")
+ logger.info(f"[Your Artists] Total fetched: {fetched}")
# 5. Match pending artists to active source
_match_liked_artists_to_all_sources(database, profile_id)
@@ -44279,7 +44306,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)")
+ logger.info(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)
@@ -44303,7 +44330,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...")
+ logger.info(f"[Your Artists] Backfilling images for {len(rows)} artists...")
filled = 0
for row in rows:
@@ -44339,7 +44366,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")
+ logger.info(f"[Your Artists] Backfilled {filled}/{len(rows)} artist images")
except Exception as e:
logger.debug(f"[Your Artists] Image backfill error: {e}")
@@ -44441,7 +44468,7 @@ def refresh_your_albums():
if request.args.get('clear', '').lower() == 'true':
database = get_database()
cleared = database.clear_liked_albums(profile_id)
- print(f"[Your Albums] Cleared {cleared} entries before refresh")
+ logger.info(f"[Your Albums] Cleared {cleared} entries before refresh")
_trigger_your_albums_refresh(profile_id)
return jsonify({"success": True, "message": "Refresh started"})
except Exception as e:
@@ -44516,9 +44543,9 @@ def _fetch_liked_albums(profile_id: int):
# 1. Fetch from Spotify (saved albums)
try:
if 'spotify' not in enabled_sources:
- print("[Your Albums] Spotify skipped (disabled in sources config)")
+ logger.warning("[Your Albums] Spotify skipped (disabled in sources config)")
elif spotify_client and spotify_client.is_spotify_authenticated():
- print("[Your Albums] Fetching saved albums from Spotify...")
+ logger.info("[Your Albums] Fetching saved albums from Spotify...")
albums = spotify_client.get_saved_albums()
for a in albums:
database.upsert_liked_album(
@@ -44529,18 +44556,18 @@ def _fetch_liked_albums(profile_id: int):
total_tracks=a.get('total_tracks', 0), profile_id=profile_id
)
fetched += len(albums)
- print(f"[Your Albums] Fetched {len(albums)} from Spotify")
+ logger.info(f"[Your Albums] Fetched {len(albums)} from Spotify")
except Exception as e:
logger.error(f"[Your Albums] Spotify fetch error: {e}")
# 2. Fetch from Tidal (favorite albums)
try:
if 'tidal' not in enabled_sources:
- print("[Your Albums] Tidal skipped (disabled in sources config)")
+ logger.warning("[Your Albums] Tidal skipped (disabled in sources config)")
elif tidal_client and hasattr(tidal_client, 'get_favorite_albums'):
tidal_auth = tidal_client._ensure_valid_token() if hasattr(tidal_client, '_ensure_valid_token') else False
if tidal_auth:
- print("[Your Albums] Fetching favorite albums from Tidal...")
+ logger.info("[Your Albums] Fetching favorite albums from Tidal...")
albums = tidal_client.get_favorite_albums(limit=500)
for a in albums:
database.upsert_liked_album(
@@ -44551,23 +44578,23 @@ def _fetch_liked_albums(profile_id: int):
total_tracks=a.get('total_tracks', 0), profile_id=profile_id
)
fetched += len(albums)
- print(f"[Your Albums] Fetched {len(albums)} from Tidal")
+ logger.info(f"[Your Albums] Fetched {len(albums)} from Tidal")
except Exception as e:
logger.error(f"[Your Albums] Tidal fetch error: {e}")
# 3. Fetch from Deezer (favorite albums ā OAuth or ARL)
try:
if 'deezer' not in enabled_sources:
- print("[Your Albums] Deezer skipped (disabled in sources config)")
+ logger.warning("[Your Albums] Deezer skipped (disabled in sources config)")
else:
deezer_cl = _get_deezer_client()
albums = []
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
- print("[Your Albums] Fetching favorite albums from Deezer (OAuth)...")
+ logger.info("[Your Albums] Fetching favorite albums from Deezer (OAuth)...")
albums = deezer_cl.get_user_favorite_albums(limit=500)
elif (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.deezer_dl
and soulseek_client.deezer_dl.is_authenticated()):
- print("[Your Albums] Fetching favorite albums from Deezer (ARL)...")
+ logger.info("[Your Albums] Fetching favorite albums from Deezer (ARL)...")
albums = soulseek_client.deezer_dl.get_user_favorite_albums(limit=500)
for a in albums:
database.upsert_liked_album(
@@ -44579,11 +44606,11 @@ def _fetch_liked_albums(profile_id: int):
)
fetched += len(albums)
if albums:
- print(f"[Your Albums] Fetched {len(albums)} from Deezer")
+ logger.info(f"[Your Albums] Fetched {len(albums)} from Deezer")
except Exception as e:
logger.error(f"[Your Albums] Deezer fetch error: {e}")
- print(f"[Your Albums] Total fetched: {fetched}")
+ logger.info(f"[Your Albums] Total fetched: {fetched}")
@app.route('/api/discover/your-artists/info/', methods=['GET'])
@@ -45268,11 +45295,11 @@ def get_artist_map_genres():
_img_count = sum(1 for n in nodes if n.get('image_url'))
_deezer_count = sum(1 for n in nodes if n.get('image_url', '').startswith('https://api.deezer'))
_none_count = sum(1 for n in nodes if not n.get('image_url'))
- print(f"[Genre Map] {len(nodes)} artists, {len(sorted_genres)} genres")
- print(f"[Genre Map] Images: {_img_count} have URLs, {_deezer_count} Deezer fallback, {_none_count} missing")
+ logger.info(f"[Genre Map] {len(nodes)} artists, {len(sorted_genres)} genres")
+ logger.warning(f"[Genre Map] Images: {_img_count} have URLs, {_deezer_count} Deezer fallback, {_none_count} missing")
if _none_count > 0:
samples = [n['name'] for n in nodes if not n.get('image_url')][:5]
- print(f"[Genre Map] Missing image samples: {samples}")
+ logger.warning(f"[Genre Map] Missing image samples: {samples}")
result = {
'success': True,
@@ -45395,7 +45422,7 @@ def get_artist_map_explore():
center_image = ia.image_url if hasattr(ia, 'image_url') else ''
artist_found = True
except Exception as e:
- print(f"[Artist Explorer] API validation failed for '{artist_name}': {e}")
+ logger.debug(f"[Artist Explorer] API validation failed for '{artist_name}': {e}")
if not artist_found:
return jsonify({"success": False, "error": f"Artist '{artist_name}' not found"}), 404
@@ -45458,7 +45485,7 @@ def get_artist_map_explore():
# If no similar artists in DB, fetch from MusicMap on-the-fly
if not ring1_artists:
try:
- print(f"[Artist Explorer] No stored similar artists for '{center_name}', fetching from MusicMap...")
+ logger.debug(f"[Artist Explorer] No stored similar artists for '{center_name}', fetching from MusicMap...")
from core.watchlist_scanner import WatchlistScanner
scanner = WatchlistScanner(spotify_client=spotify_client) if spotify_client else None
if scanner:
@@ -45505,10 +45532,10 @@ def get_artist_map_explore():
ORDER BY similarity_rank ASC
""", (source_artist_id, profile_id))
ring1_artists = cursor.fetchall()
- print(f"[Artist Explorer] Fetched {len(ring1_artists)} similar artists from MusicMap for '{center_name}'")
+ logger.debug(f"[Artist Explorer] Fetched {len(ring1_artists)} similar artists from MusicMap for '{center_name}'")
_artmap_cache_invalidate(profile_id) # New similar artists added
except Exception as e:
- print(f"[Artist Explorer] MusicMap fetch failed for '{center_name}': {e}")
+ logger.debug(f"[Artist Explorer] MusicMap fetch failed for '{center_name}': {e}")
# Deduplicate ring 1
for r in ring1_artists:
@@ -45658,7 +45685,7 @@ def get_artist_map_explore():
if alb:
n['image_url'] = alb['image_url']
- print(f"[Artist Explorer] Center: {center_name}, Ring 1: {sum(1 for n in nodes if n.get('ring')==1)}, Ring 2: {sum(1 for n in nodes if n.get('ring')==2)}, Edges: {len(edges)}")
+ logger.info(f"[Artist Explorer] Center: {center_name}, Ring 1: {sum(1 for n in nodes if n.get('ring')==1)}, Ring 2: {sum(1 for n in nodes if n.get('ring')==2)}, Edges: {len(edges)}")
return jsonify({
'success': True,
@@ -45732,7 +45759,7 @@ def search_artists_for_playlist():
})
except Exception as e:
- print(f"Error searching for artists: {e}")
+ logger.error(f"Error searching for artists: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/build-playlist/generate', methods=['POST'])
@@ -45765,7 +45792,7 @@ def generate_custom_playlist():
})
except Exception as e:
- print(f"Error generating custom playlist: {e}")
+ logger.error(f"Error generating custom playlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -45807,7 +45834,7 @@ def get_available_decades():
})
except Exception as e:
- print(f"Error getting available decades: {e}")
+ logger.error(f"Error getting available decades: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/decade/', methods=['GET'])
@@ -45850,7 +45877,7 @@ def get_discover_decade_playlist(decade):
})
except Exception as e:
- print(f"Error getting decade playlist: {e}")
+ logger.error(f"Error getting decade playlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -45872,7 +45899,7 @@ def get_available_genres():
})
except Exception as e:
- print(f"Error getting available genres: {e}")
+ logger.error(f"Error getting available genres: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -45917,7 +45944,7 @@ def get_discover_genre_playlist(genre_name):
})
except Exception as e:
- print(f"Error getting genre playlist: {e}")
+ logger.error(f"Error getting genre playlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -45948,7 +45975,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...")
+ logger.warning(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)
@@ -45979,7 +46006,7 @@ def get_listenbrainz_created_for():
try:
return _get_lb_discover_playlists('created_for')
except Exception as e:
- print(f"Error getting cached ListenBrainz created-for playlists: {e}")
+ logger.error(f"Error getting cached ListenBrainz created-for playlists: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -45990,7 +46017,7 @@ def get_listenbrainz_user_playlists():
try:
return _get_lb_discover_playlists('user')
except Exception as e:
- print(f"Error getting cached ListenBrainz user playlists: {e}")
+ logger.error(f"Error getting cached ListenBrainz user playlists: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46001,7 +46028,7 @@ def get_listenbrainz_collaborative():
try:
return _get_lb_discover_playlists('collaborative')
except Exception as e:
- print(f"Error getting cached ListenBrainz collaborative playlists: {e}")
+ logger.error(f"Error getting cached ListenBrainz collaborative playlists: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46016,7 +46043,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...")
+ logger.debug(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'
@@ -46039,7 +46066,7 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
})
except Exception as e:
- print(f"Error getting cached ListenBrainz playlist tracks: {e}")
+ logger.error(f"Error getting cached ListenBrainz playlist tracks: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46056,7 +46083,7 @@ def refresh_listenbrainz():
return jsonify(result)
except Exception as e:
- print(f"Error refreshing ListenBrainz: {e}")
+ logger.error(f"Error refreshing ListenBrainz: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46114,7 +46141,7 @@ def lastfm_search_tracks():
return jsonify({"success": True, "results": results})
except Exception as e:
- print(f"Error searching Last.fm tracks: {e}")
+ logger.error(f"Error searching Last.fm tracks: {e}")
return jsonify({"success": False, "error": str(e), "results": []}), 500
@@ -46194,7 +46221,7 @@ def lastfm_radio_generate():
state['spotify_total'] = len(similar)
state['last_accessed'] = time.time()
- print(f"Last.fm Radio generated: '{title}' ({len(similar)} tracks) ā {playlist_mbid}")
+ logger.info(f"Last.fm Radio generated: '{title}' ({len(similar)} tracks) ā {playlist_mbid}")
return jsonify({
"success": True,
"playlist_mbid": playlist_mbid,
@@ -46203,7 +46230,7 @@ def lastfm_radio_generate():
})
except Exception as e:
- print(f"Error generating Last.fm radio: {e}")
+ logger.error(f"Error generating Last.fm radio: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46234,7 +46261,7 @@ def get_listenbrainz_lastfm_radio():
]
return jsonify({"success": True, "playlists": formatted, "count": len(formatted), "username": username, "source": source})
except Exception as e:
- print(f"Error getting Last.fm radio playlists: {e}")
+ logger.error(f"Error getting Last.fm radio playlists: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46282,11 +46309,11 @@ def get_all_listenbrainz_playlists():
}
playlists.append(playlist_info)
- print(f"Returning {len(playlists)} stored ListenBrainz playlists for profile {profile_id}")
+ logger.info(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}")
+ logger.error(f"Error getting ListenBrainz playlists: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/state/', methods=['GET'])
@@ -46321,7 +46348,7 @@ def get_listenbrainz_playlist_state(playlist_mbid):
return jsonify(response)
except Exception as e:
- print(f"Error getting ListenBrainz playlist state: {e}")
+ logger.error(f"Error getting ListenBrainz playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/reset/', methods=['POST'])
@@ -46350,11 +46377,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']}")
+ logger.info(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}")
+ logger.error(f"Error resetting ListenBrainz playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/remove/', methods=['POST'])
@@ -46374,11 +46401,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}")
+ logger.info(f"Removed ListenBrainz playlist from state: {playlist_mbid}")
return jsonify({"success": True})
except Exception as e:
- print(f"Error removing ListenBrainz playlist: {e}")
+ logger.error(f"Error removing ListenBrainz playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/discovery/start/', methods=['POST'])
@@ -46407,7 +46434,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')}")
+ logger.info(f"Created new ListenBrainz playlist state: {playlist_data.get('name', 'Unknown')}")
else:
# State already exists, update it
state = listenbrainz_playlist_states[state_key]
@@ -46433,11 +46460,11 @@ def start_listenbrainz_discovery(playlist_mbid):
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}")
+ logger.info(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}")
+ logger.error(f"Error starting ListenBrainz discovery: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@@ -46466,7 +46493,7 @@ def get_listenbrainz_discovery_status(playlist_mbid):
return jsonify(response)
except Exception as e:
- print(f"Error getting ListenBrainz discovery status: {e}")
+ logger.error(f"Error getting ListenBrainz discovery status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/update-phase/', methods=['POST'])
@@ -46505,7 +46532,7 @@ def update_listenbrainz_phase(playlist_mbid):
})
except Exception as e:
- print(f"Error updating ListenBrainz playlist phase: {e}")
+ logger.error(f"Error updating ListenBrainz playlist phase: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/discovery/update_match', methods=['POST'])
@@ -46563,13 +46590,13 @@ def update_listenbrainz_discovery_match():
result['manual_match'] = True
- print(f"Updated ListenBrainz match for track {track_index}: {result['status']}")
+ logger.info(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}")
+ logger.error(f"Error updating ListenBrainz discovery match: {e}")
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@@ -46607,7 +46634,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")
+ logger.info(f"Converted {len(spotify_tracks)} ListenBrainz matches to Spotify tracks for sync")
return spotify_tracks
@app.route('/api/wing-it/sync', methods=['POST'])
@@ -46718,11 +46745,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)")
+ logger.info(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}")
+ logger.error(f"Error starting ListenBrainz sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/sync/status/', methods=['GET'])
@@ -46767,7 +46794,7 @@ def get_listenbrainz_sync_status(playlist_mbid):
return jsonify(response)
except Exception as e:
- print(f"Error getting ListenBrainz sync status: {e}")
+ logger.error(f"Error getting ListenBrainz sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/listenbrainz/sync/cancel/', methods=['POST'])
@@ -46799,7 +46826,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}")
+ logger.error(f"Error cancelling ListenBrainz sync: {e}")
return jsonify({"error": str(e)}), 500
@@ -46824,7 +46851,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")
+ logger.info(f"Processing {len(jspf_tracks)} tracks from playlist")
# First pass: extract all track data without cover art
track_data_list = []
@@ -46842,8 +46869,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'}")
+ logger.debug(f"Sample track extension data: {extension}")
+ logger.debug(f"Sample mb_data keys: {mb_data.keys() if mb_data else 'None'}")
# Extract release MBID for cover art
release_mbid = None
@@ -46857,7 +46884,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
release_mbid = mb_data['release_mbid']
if idx == 0:
- print(f"š First track release_mbid: {release_mbid}")
+ logger.debug(f"š First track release_mbid: {release_mbid}")
track_data = {
'track_name': track.get('title', 'Unknown Track'),
@@ -46903,7 +46930,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...")
+ logger.info(f"Fetching cover art for {len(track_data_list)} tracks in parallel...")
start_time = time.time()
# Fetch up to 10 covers at a time
@@ -46922,7 +46949,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")
+ logger.info(f"Fetched {covers_found}/{len(track_data_list)} covers in {elapsed:.2f}s")
tracks = track_data_list
@@ -46933,7 +46960,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
})
except Exception as e:
- print(f"Error getting ListenBrainz playlist tracks: {e}")
+ logger.error(f"Error getting ListenBrainz playlist tracks: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@@ -46972,20 +46999,21 @@ def start_metadata_update():
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
- print(f"[DEBUG] Active server: {active_server}")
- print(f"[DEBUG] Plex client: {media_client}")
+ logger.debug("Plex connection details: active_server=%s client=%s", active_server, media_client)
if hasattr(media_client, 'server') and media_client.server:
- print(f"[DEBUG] Plex server URL: {getattr(media_client.server, '_baseurl', 'NO_URL')}")
- print(f"[DEBUG] Plex server name: {getattr(media_client.server, 'friendlyName', 'NO_NAME')}")
+ logger.debug(
+ "Plex server details: url=%s name=%s",
+ getattr(media_client.server, '_baseurl', 'NO_URL'),
+ getattr(media_client.server, 'friendlyName', 'NO_NAME'),
+ )
# Check available libraries
try:
sections = media_client.server.library.sections()
- print(f"[DEBUG] Available Plex libraries: {[(s.title, s.type) for s in sections]}")
+ logger.debug("Available Plex libraries: %s", [(s.title, s.type) for s in sections])
except Exception as e:
- print(f"[DEBUG] Error getting Plex libraries: {e}")
+ logger.debug("Error getting Plex libraries: %s", e)
else:
- print(f"[DEBUG] Plex server is NOT connected!")
+ logger.debug("Plex server is NOT connected!")
# Check Spotify client - EXACTLY like dashboard.py
if not spotify_client:
@@ -47021,7 +47049,7 @@ def start_metadata_update():
metadata_update_runtime_worker = metadata_worker
metadata_worker.run()
except Exception as e:
- print(f"Error in metadata update worker: {e}")
+ logger.error(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")
@@ -47035,7 +47063,7 @@ def start_metadata_update():
return jsonify({"success": True})
except Exception as e:
- print(f"Error starting metadata update: {e}")
+ logger.error(f"Error starting metadata update: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
return jsonify({"success": False, "error": str(e)}), 500
@@ -47054,7 +47082,7 @@ def stop_metadata_update():
return jsonify({"success": True})
except Exception as e:
- print(f"Error stopping metadata update: {e}")
+ logger.error(f"Error stopping metadata update: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/metadata/status', methods=['GET'])
@@ -47073,7 +47101,7 @@ def get_metadata_update_status():
return jsonify({"success": True, "status": state_copy})
except Exception as e:
- print(f"Error getting metadata update status: {e}")
+ logger.error(f"Error getting metadata update status: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/active-media-server', methods=['GET'])
@@ -47083,7 +47111,7 @@ def get_active_media_server():
active_server = config_manager.get_active_media_server()
return jsonify({"success": True, "active_server": active_server})
except Exception as e:
- print(f"Error getting active media server: {e}")
+ logger.error(f"Error getting active media server: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ================================= #
@@ -48633,21 +48661,21 @@ def start_beatport_discovery(url_hash):
# Get chart data from request body
data = request.get_json() or {}
- print(f"Raw request data: {data}")
+ logger.debug(f"Raw request data: {data}")
chart_data = data.get('chart_data')
- print(f"Chart data extracted: {chart_data is not None}")
+ logger.debug(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'}")
+ logger.debug(f"Chart data keys: {list(chart_data.keys()) if isinstance(chart_data, dict) else 'Not a dict'}")
+ logger.debug(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'])}")
+ logger.debug(f"Number of tracks: {len(chart_data['tracks'])}")
if chart_data['tracks']:
- print(f"First track: {chart_data['tracks'][0]}")
+ logger.debug(f"First track: {chart_data['tracks'][0]}")
else:
- print("No chart data received")
+ logger.warning("No chart data received")
if not chart_data or not chart_data.get('tracks'):
return jsonify({"error": "Chart data with tracks is required"}), 400
@@ -48687,7 +48715,7 @@ def start_beatport_discovery(url_hash):
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}")
+ logger.info(f"Started Spotify discovery for Beatport chart: {chart_name}")
return jsonify({"success": True, "message": "Discovery started", "status": "discovering"})
except Exception as e:
@@ -48821,7 +48849,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...")
+ logger.info(f"Starting {discovery_source.upper()} discovery for {len(tracks)} Beatport tracks...")
# Store discovery source in state for frontend
state['discovery_source'] = discovery_source
@@ -48831,7 +48859,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')}')")
+ logger.warning(f"Beatport discovery cancelled (phase changed to '{state.get('phase')}')")
return
# Update progress
@@ -48850,7 +48878,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}'")
+ logger.debug(f"Searching {discovery_source.upper()} for: '{track_artist}' - '{track_title}'")
# Check discovery cache first
cache_key = _get_discovery_cache_key(track_title, track_artist)
@@ -48858,7 +48886,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}")
+ logger.debug(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):
@@ -48878,7 +48906,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}")
+ logger.error(f"Cache lookup error: {cache_err}")
# Use matching engine for track matching
found_track = None
@@ -48894,9 +48922,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")
+ logger.debug(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}")
+ logger.error(f"Matching engine failed for Beatport, falling back to basic queries: {e}")
if use_spotify:
search_queries = [
f"{track_artist} {track_title}",
@@ -48912,7 +48940,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()})")
+ logger.debug(f"Query {query_idx + 1}/{len(search_queries)}: {search_query} ({discovery_source.upper()})")
search_results = None
@@ -48937,19 +48965,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})")
+ logger.debug(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")
+ logger.debug(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}")
+ logger.debug(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")
+ logger.debug(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)
@@ -48962,12 +48990,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})")
+ logger.debug(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})")
+ logger.info(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})")
+ logger.warning(f"No suitable match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})")
# Create result entry
result_entry = {
@@ -48986,7 +49014,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)}")
+ logger.debug(f"Spotify track attributes: {dir(found_track)}")
# Format artists correctly for frontend compatibility
formatted_artists = []
@@ -49063,9 +49091,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})")
+ logger.debug(f"CACHE SAVED: {track_artist} - {track_title} (confidence: {best_confidence:.3f})")
except Exception as cache_err:
- print(f"Cache save error: {cache_err}")
+ logger.error(f"Cache save error: {cache_err}")
# Auto Wing It fallback for unmatched tracks
if result_entry.get('status_class') == 'not-found':
@@ -49090,7 +49118,7 @@ def _run_beatport_discovery_worker(url_hash):
time.sleep(0.1)
except Exception as e:
- print(f"Error processing Beatport track {i}: {e}")
+ logger.error(f"Error processing Beatport track {i}: {e}")
# Add error result
state['discovery_results'].append({
'index': i, # Add index for frontend table row identification
@@ -49115,13 +49143,13 @@ def _run_beatport_discovery_worker(url_hash):
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")
+ logger.info(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}")
+ logger.error(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'
@@ -49132,19 +49160,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}")
+ logger.info(f"Beatport sync start requested for: {url_hash}")
if url_hash not in beatport_chart_states:
- print(f"Beatport chart not found: {url_hash}")
+ logger.warning(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', []))}")
+ logger.info(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']}")
+ logger.info(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
@@ -49178,11 +49206,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']}")
+ logger.info(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}")
+ logger.error(f"Error starting Beatport sync: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/sync/status/', methods=['GET'])
@@ -49226,7 +49254,7 @@ def get_beatport_sync_status(url_hash):
return jsonify(response)
except Exception as e:
- print(f"Error getting Beatport sync status: {e}")
+ logger.error(f"Error getting Beatport sync status: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/sync/cancel/', methods=['POST'])
@@ -49254,11 +49282,11 @@ def cancel_beatport_sync(url_hash):
state['sync_playlist_id'] = None
state['sync_progress'] = {}
- print(f"Cancelled Beatport sync for: {url_hash}")
+ logger.warning(f"Cancelled Beatport sync for: {url_hash}")
return jsonify({"success": True})
except Exception as e:
- print(f"Error cancelling Beatport sync: {e}")
+ logger.error(f"Error cancelling Beatport sync: {e}")
return jsonify({"error": str(e)}), 500
# ===================================================================
@@ -49919,13 +49947,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}")
+ logger.error(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")
+ logger.error(f"Retrying failed discovery for {url_hash}: {retry_count} tracks to retry, {already_found} already found")
return jsonify({
"success": True,
"retry_count": retry_count,
@@ -50417,7 +50445,7 @@ class WebMetadataUpdateWorker:
pass
all_artists = self.media_client.get_all_artists()
- print(f"[DEBUG] Raw artists returned: {[getattr(a, 'title', 'NO_TITLE') for a in (all_artists or [])]}")
+ logger.debug(f"Raw artists returned: {[getattr(a, 'title', 'NO_TITLE') for a in (all_artists or [])]}")
if not all_artists:
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = f"No artists found in {self.server_type.title()} library"
@@ -50504,7 +50532,7 @@ class WebMetadataUpdateWorker:
add_activity_item("", "Metadata Complete", summary, "Now")
except Exception as e:
- print(f"Metadata update failed: {e}")
+ logger.error(f"Metadata update failed: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
add_activity_item("", "Metadata Error", str(e), "Now")
@@ -50520,7 +50548,7 @@ class WebMetadataUpdateWorker:
return self.media_client.needs_update_by_age(artist, self.refresh_interval_days)
except Exception as e:
- print(f"Error checking artist {getattr(artist, 'title', 'Unknown')}: {e}")
+ logger.error(f"Error checking artist {getattr(artist, 'title', 'Unknown')}: {e}")
return True # Process if we can't determine status
def _check_db_artist(self, artist_name):
@@ -50607,9 +50635,9 @@ 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}")
+ logger.debug(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}")
+ logger.debug(f"Direct Spotify lookup failed for {db_spotify_id}: {e}")
spotify_artist = None
# Fall back to search if direct lookup didn't work
@@ -50680,7 +50708,7 @@ class WebMetadataUpdateWorker:
if albums_updated > 0:
changes_made.append(f"{albums_updated} album art")
elif self.server_type != "plex":
- print(f"Skipping album artwork updates for Jellyfin artist: {artist.title}")
+ logger.info(f"Skipping album artwork updates for Jellyfin artist: {artist.title}")
if changes_made:
biography_updated = self.media_client.update_artist_biography(artist)
@@ -50716,15 +50744,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')})")
+ logger.info(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")
+ logger.warning(f"Skipping {artist.title}: no Spotify image URL available")
return False
- print(f"Processing {artist.title}: downloading from Spotify...")
+ logger.info(f"Processing {artist.title}: downloading from Spotify...")
image_url = spotify_artist.image_url
@@ -50736,7 +50764,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)")
+ logger.info(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)
@@ -50747,7 +50775,7 @@ class WebMetadataUpdateWorker:
return self.media_client.update_artist_poster(artist, image_data)
except Exception as e:
- print(f"Error updating photo for {getattr(artist, 'title', 'Unknown')}: {e}")
+ logger.error(f"Error updating photo for {getattr(artist, 'title', 'Unknown')}: {e}")
return False
def update_artist_genres(self, artist, spotify_artist):
@@ -50791,7 +50819,7 @@ class WebMetadataUpdateWorker:
return False
except Exception as e:
- print(f"Error updating genres for {getattr(artist, 'title', 'Unknown')}: {e}")
+ logger.error(f"Error updating genres for {getattr(artist, 'title', 'Unknown')}: {e}")
return False
def update_album_artwork(self, artist, spotify_artist):
@@ -50805,11 +50833,11 @@ class WebMetadataUpdateWorker:
try:
albums = list(artist.albums())
except Exception:
- print(f"Could not access albums for artist '{artist.title}'")
+ logger.error(f"Could not access albums for artist '{artist.title}'")
return 0
if not albums:
- print(f"No albums found for artist '{artist.title}'")
+ logger.warning(f"No albums found for artist '{artist.title}'")
return 0
import time
@@ -50852,13 +50880,13 @@ class WebMetadataUpdateWorker:
updated_count += 1
except Exception as e:
- print(f"Error processing album '{getattr(album, 'title', 'Unknown')}': {e}")
+ logger.error(f"Error processing album '{getattr(album, 'title', 'Unknown')}': {e}")
continue
return updated_count
except Exception as e:
- print(f"Error updating album artwork for artist '{getattr(artist, 'title', 'Unknown')}': {e}")
+ logger.error(f"Error updating album artwork for artist '{getattr(artist, 'title', 'Unknown')}': {e}")
return 0
def album_has_valid_artwork(self, album):
@@ -50906,7 +50934,7 @@ class WebMetadataUpdateWorker:
return success
except Exception as e:
- print(f"Error downloading/uploading artwork for album '{getattr(album, 'title', 'Unknown')}': {e}")
+ logger.error(f"Error downloading/uploading artwork for album '{getattr(album, 'title', 'Unknown')}': {e}")
return False
def artist_has_valid_photo(self, artist):
@@ -50981,7 +51009,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")
+ logger.warning("Jellyfin configuration missing for image upload")
return False
upload_url = f"{jellyfin_base_url.rstrip('/')}/Items/{artist.ratingKey}/Images/Primary"
@@ -50996,7 +51024,7 @@ class WebMetadataUpdateWorker:
# Navidrome: Currently not supported (Subsonic API doesn't support image uploads)
elif self.server_type == "navidrome":
- print("ā¹ļø Navidrome does not support artist image uploads via Subsonic API")
+ logger.info("ā¹ļø Navidrome does not support artist image uploads via Subsonic API")
return False
else:
@@ -51004,7 +51032,7 @@ class WebMetadataUpdateWorker:
return False
except Exception as e:
- print(f"Error uploading poster: {e}")
+ logger.error(f"Error uploading poster: {e}")
return False
# --- Docker Helper Functions ---
@@ -51158,33 +51186,33 @@ def start_oauth_callback_servers():
_env_val = os.environ.get('SOULSYNC_SPOTIFY_CALLBACK_PORT')
spotify_port = int(_env_val) if _env_val else 8888
if _env_val:
- print(f"[OAuth] SOULSYNC_SPOTIFY_CALLBACK_PORT={_env_val!r} ā binding Spotify callback server on port {spotify_port}")
+ logger.info(f"[OAuth] SOULSYNC_SPOTIFY_CALLBACK_PORT={_env_val!r} ā binding Spotify callback server on port {spotify_port}")
else:
- print(f"[OAuth] SOULSYNC_SPOTIFY_CALLBACK_PORT not set ā using default port {spotify_port}")
+ logger.info(f"[OAuth] SOULSYNC_SPOTIFY_CALLBACK_PORT not set ā using default port {spotify_port}")
try:
bind_addr = ('0.0.0.0', spotify_port)
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]}")
+ logger.info(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 {spotify_port}: {e} ā port may already be in use")
- print(f"Failed to start Spotify callback server on port {spotify_port}: {e}")
+ logger.error(f"Failed to start Spotify callback server on port {spotify_port}: {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}")
+ logger.error(f"Failed to start Spotify callback server: {e}")
# Tidal callback server
class TidalCallbackHandler(BaseHTTPRequestHandler):
def do_GET(self):
- print("TIDAL CALLBACK SERVER RECEIVED REQUEST ")
+ logger.info("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}")
+ logger.info(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]}...")
+ logger.info(f"Received Tidal authorization code: {auth_code[:10]}...")
# Exchange the authorization code for tokens
try:
@@ -51199,7 +51227,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'}...")
+ logger.info(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)
@@ -51219,7 +51247,7 @@ 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}")
+ logger.error(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')
@@ -51227,7 +51255,7 @@ def start_oauth_callback_servers():
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}")
+ logger.error(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')
@@ -51241,18 +51269,18 @@ def start_oauth_callback_servers():
_env_val = os.environ.get('SOULSYNC_TIDAL_CALLBACK_PORT')
tidal_port = int(_env_val) if _env_val else 8889
if _env_val:
- print(f"[OAuth] SOULSYNC_TIDAL_CALLBACK_PORT={_env_val!r} ā binding Tidal callback server on port {tidal_port}")
+ logger.info(f"[OAuth] SOULSYNC_TIDAL_CALLBACK_PORT={_env_val!r} ā binding Tidal callback server on port {tidal_port}")
else:
- print(f"[OAuth] SOULSYNC_TIDAL_CALLBACK_PORT not set ā using default port {tidal_port}")
+ logger.info(f"[OAuth] SOULSYNC_TIDAL_CALLBACK_PORT not set ā using default port {tidal_port}")
try:
tidal_server = HTTPServer(('0.0.0.0', tidal_port), TidalCallbackHandler)
- print(f"Started Tidal OAuth callback server on port {tidal_port}")
- print(f"Tidal server listening on all interfaces, port {tidal_port}")
+ logger.info(f"Started Tidal OAuth callback server on port {tidal_port}")
+ logger.info(f"Tidal server listening on all interfaces, port {tidal_port}")
tidal_server.serve_forever()
except Exception as e:
- print(f"Failed to start Tidal callback server: {e}")
+ logger.error(f"Failed to start Tidal callback server: {e}")
import traceback
- print(f"Full error: {traceback.format_exc()}")
+ logger.error(f"Full error: {traceback.format_exc()}")
# Start both servers in background threads
spotify_thread = threading.Thread(target=run_spotify_server, daemon=True)
@@ -51261,7 +51289,7 @@ def start_oauth_callback_servers():
spotify_thread.start()
tidal_thread.start()
- print("OAuth callback servers started")
+ logger.info("OAuth callback servers started")
# ================================================================================================
# MUSICBRAINZ ENRICHMENT - PHASE 5 WEB UI INTEGRATION
@@ -51282,11 +51310,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)")
+ logger.info("MusicBrainz enrichment worker initialized (paused ā restored from config)")
else:
- print("MusicBrainz enrichment worker initialized and started")
+ logger.info("MusicBrainz enrichment worker initialized and started")
except Exception as e:
- print(f"MusicBrainz worker initialization failed: {e}")
+ logger.error(f"MusicBrainz worker initialization failed: {e}")
mb_worker = None
# --- MusicBrainz API Endpoints ---
@@ -51359,11 +51387,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)")
+ logger.info("AudioDB enrichment worker initialized (paused ā restored from config)")
else:
- print("AudioDB enrichment worker initialized and started")
+ logger.info("AudioDB enrichment worker initialized and started")
except Exception as e:
- print(f"AudioDB worker initialization failed: {e}")
+ logger.error(f"AudioDB worker initialization failed: {e}")
audiodb_worker = None
# --- AudioDB API Endpoints ---
@@ -51432,11 +51460,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)")
+ logger.info("Discogs enrichment worker initialized (paused ā restored from config)")
else:
- print("Discogs enrichment worker initialized and started")
+ logger.info("Discogs enrichment worker initialized and started")
except Exception as e:
- print(f"Discogs worker initialization failed: {e}")
+ logger.error(f"Discogs worker initialization failed: {e}")
discogs_worker = None
# --- Discogs API Endpoints ---
@@ -51494,11 +51522,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)")
+ logger.info("Deezer enrichment worker initialized (paused ā restored from config)")
else:
- print("Deezer enrichment worker initialized and started")
+ logger.info("Deezer enrichment worker initialized and started")
except Exception as e:
- print(f"Deezer worker initialization failed: {e}")
+ logger.error(f"Deezer worker initialization failed: {e}")
deezer_worker = None
# --- Deezer API Endpoints ---
@@ -51572,11 +51600,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)")
+ logger.info("Spotify enrichment worker initialized (paused ā restored from config)")
else:
- print("Spotify enrichment worker initialized and started")
+ logger.info("Spotify enrichment worker initialized and started")
except Exception as e:
- print(f"Spotify enrichment worker initialization failed: {e}")
+ logger.error(f"Spotify enrichment worker initialization failed: {e}")
spotify_enrichment_worker = None
# --- API Rate Monitor Endpoints ---
@@ -51671,11 +51699,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)")
+ logger.info("iTunes enrichment worker initialized (paused ā restored from config)")
else:
- print("iTunes enrichment worker initialized and started")
+ logger.info("iTunes enrichment worker initialized and started")
except Exception as e:
- print(f"iTunes enrichment worker initialization failed: {e}")
+ logger.error(f"iTunes enrichment worker initialization failed: {e}")
itunes_enrichment_worker = None
# --- iTunes API Endpoints ---
@@ -51747,11 +51775,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)")
+ logger.info("Last.fm enrichment worker initialized (paused ā restored from config)")
else:
- print("Last.fm enrichment worker initialized and started")
+ logger.info("Last.fm enrichment worker initialized and started")
except Exception as e:
- print(f"Last.fm worker initialization failed: {e}")
+ logger.error(f"Last.fm worker initialization failed: {e}")
lastfm_worker = None
# --- Last.fm API Endpoints ---
@@ -51890,11 +51918,11 @@ try:
genius_worker.paused = True
genius_worker.start()
if genius_worker.paused:
- print("Genius enrichment worker initialized (paused ā restored from config)")
+ logger.info("Genius enrichment worker initialized (paused ā restored from config)")
else:
- print("Genius enrichment worker initialized and started")
+ logger.info("Genius enrichment worker initialized and started")
except Exception as e:
- print(f"Genius worker initialization failed: {e}")
+ logger.error(f"Genius worker initialization failed: {e}")
genius_worker = None
# --- Genius API Endpoints ---
@@ -51967,11 +51995,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)")
+ logger.info("Tidal enrichment worker initialized (paused ā restored from config)")
else:
- print("Tidal enrichment worker initialized and started")
+ logger.info("Tidal enrichment worker initialized and started")
except Exception as e:
- print(f"Tidal worker initialization failed: {e}")
+ logger.error(f"Tidal worker initialization failed: {e}")
tidal_enrichment_worker = None
# --- Tidal Enrichment API Endpoints ---
@@ -52041,11 +52069,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)")
+ logger.info("Qobuz enrichment worker initialized (paused ā restored from config)")
else:
- print("Qobuz enrichment worker initialized and started")
+ logger.info("Qobuz enrichment worker initialized and started")
except Exception as e:
- print(f"Qobuz worker initialization failed: {e}")
+ logger.error(f"Qobuz worker initialization failed: {e}")
qobuz_enrichment_worker = None
# --- Qobuz Enrichment API Endpoints ---
@@ -52119,13 +52147,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")
+ logger.info("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}")
+ logger.error(f"Hydrabase initialization failed: {e}")
hydrabase_worker = None
hydrabase_client = None
@@ -52142,9 +52170,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']}")
+ logger.info(f"Hydrabase auto-connected to {_hydra_cfg['url']}")
except Exception as e:
- print(f"Hydrabase auto-reconnect failed: {e}")
+ logger.error(f"Hydrabase auto-reconnect failed: {e}")
# --- Hydrabase Worker API Endpoints ---
@@ -52216,9 +52244,9 @@ try:
soulid_db = MusicDatabase()
soulid_worker = SoulIDWorker(database=soulid_db)
soulid_worker.start()
- print("SoulID worker initialized and started")
+ logger.info("SoulID worker initialized and started")
except Exception as e:
- print(f"SoulID worker initialization failed: {e}")
+ logger.error(f"SoulID worker initialization failed: {e}")
soulid_worker = None
@app.route('/api/soulid/status', methods=['GET'])
@@ -52250,9 +52278,9 @@ try:
navidrome_client=navidrome_client,
)
listening_stats_worker.start()
- print("Listening stats worker initialized and started")
+ logger.info("Listening stats worker initialized and started")
except Exception as e:
- print(f"Listening stats worker initialization failed: {e}")
+ logger.error(f"Listening stats worker initialization failed: {e}")
listening_stats_worker = None
# --- Stats API Endpoints ---
@@ -52543,13 +52571,13 @@ def listening_stats_sync():
import threading
def _do_sync():
try:
- print("[Stats Sync] Starting manual poll...")
+ logger.info("[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")
+ logger.info("[Stats Sync] Manual poll completed")
except Exception as e:
- print(f"[Stats Sync] Manual poll failed: {e}")
+ logger.error(f"[Stats Sync] Manual poll failed: {e}")
import traceback
traceback.print_exc()
logger.error(f"Manual stats sync failed: {e}")
@@ -52639,9 +52667,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")
+ logger.info("Repair worker initialized and started")
except Exception as e:
- print(f"Repair worker initialization failed: {e}")
+ logger.error(f"Repair worker initialization failed: {e}")
repair_worker = None
# --- Repair Worker API Endpoints ---
@@ -53942,11 +53970,11 @@ try:
)
if config_manager.get('auto_import.enabled', False):
auto_import_worker.start()
- print("Auto-import worker started")
+ logger.info("Auto-import worker started")
else:
- print("Auto-import worker initialized (disabled)")
+ logger.info("Auto-import worker initialized (disabled)")
except Exception as _ai_err:
- print(f"Auto-import worker init failed: {_ai_err}")
+ logger.error(f"Auto-import worker init failed: {_ai_err}")
@app.route('/api/auto-import/status', methods=['GET'])
@@ -54173,13 +54201,13 @@ def _hydrabase_reconnect_loop():
)
_hydrabase_ws = ws
_consecutive_failures = 0
- print(f"[Hydrabase] Auto-reconnected to {hydra_cfg['url']}")
+ logger.info(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}")
+ logger.error(f"[Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}")
elif _consecutive_failures == 4:
- print(f"[Hydrabase] Reconnect failing repeatedly ā suppressing further logs until success")
+ logger.error(f"[Hydrabase] Reconnect failing repeatedly ā suppressing further logs until success")
except Exception:
pass # Don't crash the monitor loop
@@ -54524,10 +54552,10 @@ def _emit_live_log_loop():
_last_pos = {} # {source: file_position}
_active_source = 'app'
log_map = {
- 'app': os.path.join('logs', 'app.log'),
- 'post_processing': os.path.join('logs', 'post_processing.log'),
- 'acoustid': os.path.join('logs', 'acoustid.log'),
- 'source_reuse': os.path.join('logs', 'source_reuse.log'),
+ 'app': Path(_log_path),
+ 'acoustid': _log_dir / 'acoustid.log',
+ 'post_processing': _log_dir / 'post_processing.log',
+ 'source_reuse': _log_dir / 'source_reuse.log',
}
while not globals().get('IS_SHUTTING_DOWN', False):
socketio.sleep(0.5)
@@ -54788,36 +54816,36 @@ def start_runtime_services():
if _runtime_started:
return
- print("Starting SoulSync runtime services...")
+ logger.info("Starting SoulSync runtime services...")
# Dump SOULSYNC_* env vars for diagnostics (helps debug Docker/Unraid env issues)
_soulsync_env = {k: v for k, v in os.environ.items() if k.startswith('SOULSYNC_')}
if _soulsync_env:
- print(f"[Startup] SOULSYNC environment variables: {_soulsync_env}")
+ logger.info(f"[Startup] SOULSYNC environment variables: {_soulsync_env}")
else:
- print("[Startup] No SOULSYNC_* environment variables detected")
+ logger.warning("[Startup] No SOULSYNC_* environment variables detected")
# Start OAuth callback servers
- print("Starting OAuth callback servers...")
+ logger.info("Starting OAuth callback servers...")
start_oauth_callback_servers()
# Startup diagnostics: Check and recover stuck flags
- print("Running startup diagnostics...")
+ logger.info("Running startup diagnostics...")
stuck_flags_recovered = check_and_recover_stuck_flags()
if stuck_flags_recovered:
- print("Recovered stuck flags from previous session")
+ logger.warning("Recovered stuck flags from previous session")
else:
- print("No stuck flags detected - system healthy")
+ logger.warning("No stuck flags detected - system healthy")
# Start simple background monitor when server starts
- print("Starting simple background monitor...")
+ logger.info("Starting simple background monitor...")
start_simple_background_monitor()
- print("Simple background monitor started (includes automatic search cleanup)")
+ logger.info("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...")
+ logger.info("Pre-building import suggestions cache...")
start_import_suggestions_cache()
# Initialize app start time for uptime tracking
@@ -54827,26 +54855,26 @@ def start_runtime_services():
_register_automation_handlers()
if automation_engine:
try:
- print("Starting automation engine...")
+ logger.info("Starting automation engine...")
automation_engine.start()
- print("Automation engine started")
+ logger.info("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(" If using Docker, check that your volume mount is /app/data (not /app/database)")
+ logger.error(f"Automation engine failed to start: {e}")
+ logger.info(" If using Docker, check that your volume mount is /app/data (not /app/database)")
logger.error(f"Automation engine start error (possible stale Docker volume): {e}")
except Exception as e:
- print(f"Automation engine failed to start: {e}")
+ logger.error(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")
# Start WebSocket background emitters
- print("Starting WebSocket background emitters...")
+ logger.info("Starting WebSocket background emitters...")
# Phase 1: Global pollers
socketio.start_background_task(_emit_service_status_loop)
socketio.start_background_task(_emit_watchlist_count_loop)
@@ -54874,7 +54902,7 @@ def start_runtime_services():
socketio.start_background_task(_emit_rate_monitor_loop)
# Live log tail ā streams new log lines to the log viewer
socketio.start_background_task(_emit_live_log_loop)
- print("WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor + live logs)")
+ logger.info("WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor + live logs)")
_runtime_started = True
@@ -54882,8 +54910,8 @@ def start_runtime_services():
# Direct execution: python web_server.py (dev/Windows fallback)
# Production should use: gunicorn -c gunicorn.conf.py wsgi:application
if _DIRECT_RUN:
- print("Starting SoulSync Web UI Server...")
- print("Open your browser and navigate to http://127.0.0.1:8008")
- print("Tip: For production, use gunicorn -c gunicorn.conf.py wsgi:application")
+ logger.info("Starting SoulSync Web UI Server...")
+ logger.info("Open your browser and navigate to http://127.0.0.1:8008")
+ logger.info("Tip: For production, use gunicorn -c gunicorn.conf.py wsgi:application")
start_runtime_services()
socketio.run(app, host='0.0.0.0', port=8008, debug=False, allow_unsafe_werkzeug=True)