diff --git a/beatport_unified_scraper.py b/beatport_unified_scraper.py index f5bef9ff..eced8d24 100644 --- a/beatport_unified_scraper.py +++ b/beatport_unified_scraper.py @@ -127,7 +127,7 @@ class BeatportUnifiedScraper: response.raise_for_status() return BeautifulSoup(response.content, 'html.parser') except requests.RequestException as e: - print(f"❌ Error fetching {url}: {e}") + print(f"Error fetching {url}: {e}") return None def clean_artist_track_data(self, raw_artist: str, raw_title: str) -> Dict[str, str]: @@ -171,12 +171,12 @@ class BeatportUnifiedScraper: def discover_genres_from_homepage(self) -> List[Dict]: """Dynamically discover all genres from Beatport homepage dropdown""" - print("🔍 Discovering genres from Beatport homepage...") + print("Discovering genres from Beatport homepage...") try: soup = self.get_page(self.base_url) if not soup: - print("❌ Could not fetch homepage") + print("Could not fetch homepage") return self.fallback_genres genres = [] @@ -185,14 +185,14 @@ class BeatportUnifiedScraper: genres_dropdown = soup.find('div', {'id': 'genres-dropdown-menu'}) if genres_dropdown: - print("✅ Found genres-dropdown-menu") + print("Found genres-dropdown-menu") # Look for the two main div containers as described genre_containers = genres_dropdown.find_all('div', recursive=False) - print(f"🔍 Found {len(genre_containers)} top-level containers in dropdown") + print(f"Found {len(genre_containers)} top-level containers in dropdown") for container_idx, container in enumerate(genre_containers): - print(f"📦 Processing container {container_idx + 1}") + print(f"Processing container {container_idx + 1}") # Look specifically for .dropdown_menu classes dropdown_menus = container.find_all(class_='dropdown_menu') @@ -202,17 +202,17 @@ class BeatportUnifiedScraper: dropdown_menus = container.find_all(class_=re.compile(r'dropdown.*menu', re.I)) if not dropdown_menus: - print(f"⚠️ No .dropdown_menu found in container {container_idx + 1}") + print(f"No .dropdown_menu found in container {container_idx + 1}") continue for menu_idx, menu in enumerate(dropdown_menus): - print(f"📋 Processing dropdown_menu {menu_idx + 1} in container {container_idx + 1}") + print(f"Processing dropdown_menu {menu_idx + 1} in container {container_idx + 1}") # Look for
  • elements first, then elements within them list_items = menu.find_all('li') if list_items: - print(f"📝 Found {len(list_items)} list items in menu") + print(f"Found {len(list_items)} list items in menu") for li in list_items: # Find anchor tag within the list item @@ -239,16 +239,16 @@ class BeatportUnifiedScraper: 'id': genre_id, 'url': urljoin(self.base_url, href) }) - print(f" ✅ Added: {name} ({slug}/{genre_id})") + print(f" Added: {name} ({slug}/{genre_id})") else: - print(f" 🚫 Filtered out: '{name}' (appears to be a section title)") + print(f" Filtered out: '{name}' (appears to be a section title)") else: # Fallback: try the old method if no
  • elements found - print(f"⚠️ No
  • elements found, trying direct search...") + print(f"No
  • elements found, trying direct 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)") + print(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 +266,16 @@ class BeatportUnifiedScraper: 'id': genre_id, 'url': urljoin(self.base_url, href) }) - print(f" ✅ Added: {name} ({slug}/{genre_id})") + print(f" Added: {name} ({slug}/{genre_id})") else: - print(f"⚠️ No genre links found in dropdown_menu {menu_idx + 1}") + print(f"No genre links found in dropdown_menu {menu_idx + 1}") if genres: - print(f"🎯 Successfully extracted {len(genres)} genres from dropdown menu") + print(f"Successfully extracted {len(genres)} genres from dropdown menu") else: - print("⚠️ No genre links found in dropdown menu structure") + print("No genre links found in dropdown menu structure") else: - print("❌ Could not find genres-dropdown-menu, trying fallback methods...") + print("Could not find genres-dropdown-menu, trying fallback methods...") # Fallback: Look for other potential dropdown structures potential_dropdowns = [ @@ -289,11 +289,11 @@ class BeatportUnifiedScraper: for dropdown in potential_dropdowns: if dropdown: - print(f"✅ Found fallback dropdown: {dropdown.name} with class {dropdown.get('class')}") + print(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") + print(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 +313,14 @@ class BeatportUnifiedScraper: }) if genres: - print(f"🎯 Successfully extracted {len(genres)} genres from fallback dropdown") + print(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...") + print("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") + print(f"Found {len(all_genre_links)} potential genre links on page") seen_genres = set() for link in all_genre_links: @@ -343,18 +343,18 @@ class BeatportUnifiedScraper: # Method 3: Try to find a genres page link and scrape from there if not genres: - print("🔍 Searching for genres page...") + print("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}") + print(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") + print(f"Found {len(genre_links)} genre links on genres page") seen_genres = set() for link in genre_links: @@ -386,19 +386,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") + print(f"Discovered {len(final_genres)} unique genres from homepage") return final_genres else: - print("⚠️ No genres found, using fallback list") + print("No genres found, using fallback list") return self.fallback_genres except Exception as e: - print(f"❌ Error discovering genres: {e}") + print(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...") + print("Discovering chart sections from Beatport homepage...") soup = self.get_page(self.base_url) if not soup: @@ -411,7 +411,7 @@ class BeatportUnifiedScraper: } # Method 1: Find H2 section headings - print(" 📋 Finding H2 section headings...") + print(" Finding H2 section headings...") h2_headings = soup.find_all('h2') for heading in h2_headings: @@ -429,7 +429,7 @@ class BeatportUnifiedScraper: print(f" Found: '{text}' -> {category}") # Method 2: Find specific chart links - print(" 🔗 Finding chart page links...") + print(" Finding chart page links...") chart_links = [] # Look for the specific links we discovered @@ -453,7 +453,7 @@ class BeatportUnifiedScraper: print(f" Found: '{link.get_text(strip=True)}' -> {href}") # Method 3: Count individual DJ charts - print(" 🎧 Counting individual DJ charts...") + print(" Counting individual DJ charts...") dj_chart_links = soup.find_all('a', href=re.compile(r'/chart/')) individual_dj_charts = [] @@ -529,20 +529,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}") + print(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}") + print(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}") + print(f" Found artwork image (fallback): {first_artwork_src}") return first_artwork_src # Priority 2: Original method - Look for hero release slideshow images @@ -553,19 +553,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}") + print(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}") + print(f" Found hero image (fallback): {fallback_src}") return fallback_src - print(f" ⚠️ No suitable images found on page") + print(f" No suitable images found on page") return None except Exception as e: - print(f"⚠️ Could not get image for {genre_url}: {e}") + print(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 +573,16 @@ class BeatportUnifiedScraper: genres = self.discover_genres_from_homepage() if include_images: - print("🖼️ Fetching genre images...") + print("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))})") + print(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") + print(f" No URL available for {genre['name']}, skipping image") genre['image_url'] = None # Small delay to be respectful @@ -649,7 +649,7 @@ class BeatportUnifiedScraper: } except Exception as e: - print(f"❌ Error extracting release data: {e}") + print(f"Error extracting release data: {e}") return None def extract_chart_data_from_card(self, chart_card) -> Optional[Dict]: @@ -695,7 +695,7 @@ class BeatportUnifiedScraper: } except Exception as e: - print(f"❌ Error extracting chart data: {e}") + print(f"Error extracting chart data: {e}") return None def extract_tracks_from_page(self, soup: BeautifulSoup, list_name: str, limit: int = 100) -> List[Dict]: @@ -800,7 +800,7 @@ class BeatportUnifiedScraper: def scrape_top_100(self, limit: int = 100, enrich: bool = True) -> List[Dict]: """Scrape Beatport Top 100""" - print("\n🔥 Scraping Beatport Top 100...") + print("\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,7 +809,7 @@ class BeatportUnifiedScraper: if tracks and enrich: tracks = self.enrich_chart_tracks(tracks) - print(f"✅ Extracted {len(tracks)} tracks from Top 100") + print(f"Extracted {len(tracks)} tracks from Top 100") return tracks def scrape_new_releases(self, limit: int = 40) -> List[Dict]: @@ -824,7 +824,7 @@ class BeatportUnifiedScraper: # Step 2: Extract individual tracks from each release all_tracks = [] for i, release_url in enumerate(release_urls): - print(f"\n📀 Processing release {i+1}/{len(release_urls)}") + print(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 +833,7 @@ class BeatportUnifiedScraper: import time time.sleep(0.5) - print(f"✅ Extracted {len(all_tracks)} individual tracks from {len(release_urls)} releases") + print(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]: @@ -866,7 +866,7 @@ class BeatportUnifiedScraper: 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}") + print(f"Extracting tracks from: {release_url}") soup = self.get_page(release_url) if not soup: @@ -875,13 +875,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") + print(" 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") + print(" No matching tracks found") return [] # Convert to our standard format @@ -891,7 +891,7 @@ class BeatportUnifiedScraper: if track: converted_tracks.append(track) - print(f" ✅ Extracted {len(converted_tracks)} tracks") + print(f" Extracted {len(converted_tracks)} tracks") return converted_tracks def extract_json_object_from_release_page(self, soup): @@ -930,7 +930,7 @@ class BeatportUnifiedScraper: converted.append(track) if len(converted) >= 5: - print(f" ✅ JSON extraction found {len(converted)} tracks with rich metadata") + print(f" JSON extraction found {len(converted)} tracks with rich metadata") return converted return [] @@ -954,12 +954,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") + print(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") + print(f" Found single track in queries[{idx}].data") return [data] # Pattern 3: data.tracks[] @@ -968,11 +968,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") + print(f" Found {len(tracks)} tracks in queries[{idx}].data.tracks") return tracks except Exception as e: - print(f" ❌ Error extracting tracks from JSON: {e}") + print(f" Error extracting tracks from JSON: {e}") return [] @@ -1065,7 +1065,7 @@ class BeatportUnifiedScraper: return track except Exception as e: - print(f" ❌ Error converting chart JSON track: {e}") + print(f" Error converting chart JSON track: {e}") return None def enrich_chart_tracks(self, tracks: List[Dict], progress_callback=None) -> List[Dict]: @@ -1080,7 +1080,7 @@ class BeatportUnifiedScraper: enriched = [] total = len(tracks) - print(f" 🔍 Enriching {total} chart tracks with per-track metadata...") + print(f" Enriching {total} chart tracks with per-track metadata...") for i, track in enumerate(tracks): track_url = track.get('url', '') @@ -1113,14 +1113,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") + print(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}'") + print(f" [{i+1}] Title matched: '{jt_title}'") break # Fallback: use first track if only one result @@ -1132,14 +1132,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')}") + print(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}") + print(f" [{i+1}/{total}] Error enriching track: {e}") enriched.append(track) # Report progress (always runs — success, failure, or exception) @@ -1152,7 +1152,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") + print(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 +1182,7 @@ class BeatportUnifiedScraper: return matching_tracks except Exception as e: - print(f" ❌ Error filtering tracks: {e}") + print(f" Error filtering tracks: {e}") return [] @@ -1246,7 +1246,7 @@ class BeatportUnifiedScraper: return track except Exception as e: - print(f" ❌ Error converting track data: {e}") + print(f" Error converting track data: {e}") return None def get_release_metadata(self, release_url: str) -> Dict: @@ -1402,7 +1402,7 @@ class BeatportUnifiedScraper: } except Exception as e: - print(f"❌ Error getting release metadata from {release_url}: {e}") + print(f"Error getting release metadata from {release_url}: {e}") import traceback traceback.print_exc() return {'success': False, 'error': str(e)} @@ -1436,7 +1436,7 @@ class BeatportUnifiedScraper: return tracks except Exception as e: - print(f" ❌ Error extracting tracks from {release_url}: {e}") + print(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,22 +1456,22 @@ class BeatportUnifiedScraper: # Validate input if not release_urls or len(release_urls) == 0: - print("⚠️ No release URLs provided") + print("No release URLs provided") return [] - print(f"\n🎯 SCRAPING {len(release_urls)} RELEASE URL{'S' if len(release_urls) > 1 else ''}") + print(f"\nSCRAPING {len(release_urls)} RELEASE URL{'S' if len(release_urls) > 1 else ''}") print("=" * 60) all_tracks = [] for i, release_url in enumerate(release_urls, 1): - print(f"\n📀 Processing release {i}/{len(release_urls)}: {release_url}") + print(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") + print(f" Found {len(tracks)} tracks") # Show first few tracks for verification for j, track in enumerate(tracks[:3], 1): @@ -1483,10 +1483,10 @@ class BeatportUnifiedScraper: if len(tracks) > 3: print(f" ... and {len(tracks) - 3} more tracks") else: - print(f" ❌ No tracks found") + print(f" No tracks found") except Exception as e: - print(f" ❌ Error processing release: {e}") + print(f" Error processing release: {e}") continue # Small delay between requests to be respectful @@ -1494,7 +1494,7 @@ class BeatportUnifiedScraper: time.sleep(0.5) print(f"\n" + "=" * 60) - print(f"🎉 SCRAPING COMPLETE") + print(f"SCRAPING COMPLETE") print(f" Total releases processed: {len(release_urls)}") print(f" Total tracks extracted: {len(all_tracks)}") @@ -1502,7 +1502,7 @@ class BeatportUnifiedScraper: 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("\n🔥 Scraping Beatport Hype Top 100...") + print("\nScraping Beatport Hype Top 100...") # Use the correct URL discovered by parser soup = self.get_page(f"{self.base_url}/hype-100") @@ -1511,10 +1511,10 @@ class BeatportUnifiedScraper: if tracks and enrich: print(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") + print(f"Extracted {len(tracks)} tracks from Hype Top 100") return tracks else: - print("⚠️ Could not access /hype-100, trying homepage Hype Picks section...") + print("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 +1534,7 @@ class BeatportUnifiedScraper: else: tracks = [] - print(f"✅ Extracted {len(tracks)} tracks from Hype Top 100 (fallback)") + print(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]: @@ -1557,13 +1557,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") + print(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") + print(f" Row {i+1}: Empty release title") continue # Find the release URL from the title link @@ -1604,7 +1604,7 @@ class BeatportUnifiedScraper: print(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}") + print(f" Error extracting row {i+1}: {e}") continue print(f" Successfully extracted {len(releases)} releases from {len(table_rows)} rows") @@ -1612,12 +1612,12 @@ class BeatportUnifiedScraper: def scrape_top_100_releases(self, limit: int = 100) -> List[Dict]: """Scrape Beatport Top 100 Releases - Extract individual tracks using URL crawling""" - print("\n📊 Scraping Beatport Top 100 Releases...") + print("\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") + print(" Could not access /top-100-releases page") return [] # Look for rows with release links (Top 100 uses [class*="row"] elements, not tables) @@ -1645,7 +1645,7 @@ class BeatportUnifiedScraper: break if not release_urls: - print(" ❌ No Top 100 release URLs found") + print(" No Top 100 release URLs found") return [] # Step 2: Crawl each release URL to extract individual tracks @@ -1656,21 +1656,21 @@ class BeatportUnifiedScraper: # 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") + print(f" Found {len(tracks)} individual tracks") all_individual_tracks.extend(tracks) else: - print(f" ❌ No tracks found") + print(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") + print(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("\n🎧 Scraping Beatport DJ Charts...") + print("\nScraping Beatport DJ Charts...") soup = self.get_page(self.base_url) if not soup: @@ -1710,7 +1710,7 @@ 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...") + print(" 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") @@ -1730,12 +1730,12 @@ class BeatportUnifiedScraper: } charts.append(chart_info) - print(f"✅ Extracted {len(charts)} DJ charts") + print(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("\n📊 Scraping Beatport Featured Charts...") + print("\nScraping Beatport Featured Charts...") soup = self.get_page(self.base_url) if not soup: @@ -1765,12 +1765,12 @@ class BeatportUnifiedScraper: } charts.append(track_data) - print(f"✅ Extracted {len(charts)} charts from Featured Charts") + print(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("\n🔥 Scraping Beatport Hype Picks (individual tracks)...") + print("\nScraping Beatport Hype Picks (individual tracks)...") # Step 1: Get release URLs from homepage cards release_urls = self.extract_hype_picks_urls(limit) @@ -1780,7 +1780,7 @@ class BeatportUnifiedScraper: # Step 2: Extract individual tracks from each release all_tracks = [] for i, release_url in enumerate(release_urls): - print(f"\n📀 Processing release {i+1}/{len(release_urls)}") + print(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 +1789,7 @@ class BeatportUnifiedScraper: import time time.sleep(0.5) - print(f"✅ Extracted {len(all_tracks)} individual tracks from {len(release_urls)} hype picks releases") + print(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]: @@ -1822,7 +1822,7 @@ class BeatportUnifiedScraper: 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}") + print(f"Extracting tracks from: {release_url}") soup = self.get_page(release_url) if not soup: @@ -1831,13 +1831,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") + print(" 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") + print(" No matching tracks found") return [] # Convert to our standard format (with Hype Picks branding) @@ -1847,7 +1847,7 @@ class BeatportUnifiedScraper: if track: converted_tracks.append(track) - print(f" ✅ Extracted {len(converted_tracks)} tracks") + print(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 +1912,12 @@ class BeatportUnifiedScraper: return track except Exception as e: - print(f" ❌ Error converting track data: {e}") + print(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("\n🏆 Scraping Top 10 Lists from homepage...") + print("\nScraping Top 10 Lists from homepage...") soup = self.get_page(self.base_url) if not soup: @@ -1934,7 +1934,7 @@ class BeatportUnifiedScraper: if track_data: beatport_tracks.append(track_data) except Exception as e: - print(f" ❌ Error extracting Beatport track {i}: {e}") + print(f" Error extracting Beatport track {i}: {e}") # Extract Hype Top 10 tracks hype_top10_items = soup.select('[data-testid="hype-top-10-item"]') @@ -1947,9 +1947,9 @@ class BeatportUnifiedScraper: if track_data: hype_tracks.append(track_data) except Exception as e: - print(f" ❌ Error extracting Hype track {i}: {e}") + print(f" Error extracting Hype track {i}: {e}") - print(f"✅ Extracted {len(beatport_tracks)} Beatport Top 10 + {len(hype_tracks)} Hype Top 10 tracks") + print(f"Extracted {len(beatport_tracks)} Beatport Top 10 + {len(hype_tracks)} Hype Top 10 tracks") return { "beatport_top10": beatport_tracks, @@ -2036,11 +2036,11 @@ class BeatportUnifiedScraper: def scrape_homepage_top10_releases(self) -> List[Dict]: """Scrape Top 10 Releases from homepage - FIXED VERSION""" - print("\n💿 FIXED: Scraping Top 10 Releases from homepage...") + print("\nFIXED: Scraping Top 10 Releases from homepage...") soup = self.get_page(self.base_url) if not soup: - print(" ❌ Could not get homepage") + print(" Could not get homepage") return [] # Extract Top 10 Releases items - EXACT same as test script @@ -2048,7 +2048,7 @@ class BeatportUnifiedScraper: print(f" FOUND {len(top10_releases_items)} Top 10 Releases items") if len(top10_releases_items) == 0: - print(" ❌ No items found - trying alternatives") + print(" No items found - trying alternatives") return [] releases = [] @@ -2058,13 +2058,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']}") + print(f" {i}. {release_data['artist']} - {release_data['title']}") else: - print(f" ❌ {i}. No data extracted") + print(f" {i}. No data extracted") except Exception as e: - print(f" ❌ Error extracting release {i}: {e}") + print(f" Error extracting release {i}: {e}") - print(f"✅ FINAL: Extracted {len(releases)} Top 10 Releases") + print(f"FINAL: Extracted {len(releases)} Top 10 Releases") return releases def extract_release_from_item_FIXED(self, item, rank): @@ -2293,7 +2293,7 @@ class BeatportUnifiedScraper: 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("\n🎯 Scraping 'New on Beatport' hero slideshow...") + print("\nScraping 'New on Beatport' hero slideshow...") soup = self.get_page(self.base_url) if not soup: @@ -2304,7 +2304,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'") + print(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 +2314,14 @@ class BeatportUnifiedScraper: if len(tracks) < 5: hero_wrapper = soup.select_one('[class*="Homepage-style__NewOnBeatportWrapper"]') if hero_wrapper: - print(" ✅ Found Homepage NewOnBeatportWrapper (fallback)") + print(" 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)") + print(" 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,7 +2331,7 @@ 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)...") + print(" 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") @@ -2343,7 +2343,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") + print(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 +2529,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}'") + print(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}") + print(f" {context}: {title} - {artist}") return track_data else: - print(f" ❌ {context}: No usable data found") + print(f" {context}: No usable data found") return None except Exception as e: - print(f" ❌ Error extracting from {context}: {e}") + print(f" Error extracting from {context}: {e}") return None def _extract_title_artist_from_url(self, url: str) -> Dict[str, str]: @@ -2792,7 +2792,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("\n🔟 Scraping Top 10 Releases from homepage...") + print("\nScraping Top 10 Releases from homepage...") soup = self.get_page(self.base_url) if not soup: @@ -2812,7 +2812,7 @@ class BeatportUnifiedScraper: print(f" {i+1}. Found Top 10 release URL: {release_url}") if not release_urls: - print(" ❌ No Top 10 release URLs found") + print(" No Top 10 release URLs found") return [] # Step 2: Crawl each release URL to extract individual tracks @@ -2823,16 +2823,16 @@ class BeatportUnifiedScraper: # 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") + print(f" Found {len(tracks)} individual tracks") all_individual_tracks.extend(tracks) else: - print(f" ❌ No tracks found") + print(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") + print(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,17 +2852,17 @@ class BeatportUnifiedScraper: ] for chart_url in chart_urls_to_try: - print(f" 🎯 Trying chart URL: {chart_url}") + print(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}") + print(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...") + print(f" Only found {len(tracks)} tracks at {chart_url}, trying next URL...") else: - print(f" ❌ No tracks found at {chart_url}") + print(f" No tracks found at {chart_url}") if tracks and enrich: print(f" Enriching {len(tracks)} {genre['name']} chart tracks with per-track metadata...") @@ -2892,7 +2892,7 @@ class BeatportUnifiedScraper: ] for release_url in release_urls_to_try: - print(f" 🎯 Trying release URL: {release_url}") + print(f" Trying release URL: {release_url}") soup = self.get_page(release_url) if soup: # Try to find releases section on the page @@ -2900,19 +2900,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']}") + print(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}") + print(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...") + print(f" Only found {len(releases)} releases at {release_url}, trying next URL...") else: - print(f" ❌ No releases found at {release_url}") + print(f" No releases found at {release_url}") return releases @@ -2933,22 +2933,22 @@ class BeatportUnifiedScraper: ] for hype_url in hype_urls_to_try: - print(f" 🔥 Trying hype URL: {hype_url}") + print(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}") + print(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...") + print(f" Only found {len(tracks)} hype tracks at {hype_url}, trying next URL...") else: - print(f" ❌ No hype tracks found at {hype_url}") + print(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...") + print(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 +2958,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"\n🔥 Scraping {genre['name']} Hype Picks (individual tracks)...") + print(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 +2968,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"\n🔥 Processing {genre['name']} hype pick {i+1}/{len(release_urls)}") + print(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 +2980,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") + print(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]: @@ -3002,7 +3002,7 @@ class BeatportUnifiedScraper: break if not hype_container: - print(f" ❌ Could not find Hype Picks section for {genre['name']}") + print(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) @@ -3041,7 +3041,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)}") + print(f" Found hype heading: {heading.get_text(strip=True)}") # Get the section after this heading section_container = heading.find_parent() @@ -3146,7 +3146,7 @@ class BeatportUnifiedScraper: } tracks.append(track_data) - print(f" 🎵 Release Track: {artist_text} - {track_title}") + print(f" Release Track: {artist_text} - {track_title}") except Exception: continue @@ -3171,7 +3171,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)}") + print(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 +3193,7 @@ class BeatportUnifiedScraper: if not soup: return tracks - print(f" 🔍 Looking for HYPE labeled tracks on page...") + print(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 +3261,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']}") + print(f" Found HYPE track: {track_data['artist']} - {track_data['title']}") except Exception as e: continue @@ -3269,7 +3269,7 @@ class BeatportUnifiedScraper: except Exception as e: continue - print(f" ✅ Extracted {len(tracks)} HYPE labeled tracks") + print(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 +3279,7 @@ class BeatportUnifiedScraper: if not soup: return tracks - print(f" 🔍 Extracting hype tracks from Beatport page...") + print(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 +3295,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") + print(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 +3342,7 @@ class BeatportUnifiedScraper: } tracks.append(track_data) - print(f" 🔥 Hype Pick: {artist_text} - {release_title}") + print(f" Hype Pick: {artist_text} - {release_title}") except Exception as e: continue @@ -3395,7 +3395,7 @@ class BeatportUnifiedScraper: } tracks.append(track_data) - print(f" 🎵 Hype Track {position}: {artist_text} - {track_title}") + print(f" Hype Track {position}: {artist_text} - {track_title}") except Exception as e: continue @@ -3452,7 +3452,7 @@ class BeatportUnifiedScraper: } tracks.append(track_data) - print(f" 📊 Hype Track {position}: {artist_text} - {track_title}") + print(f" Hype Track {position}: {artist_text} - {track_title}") except Exception as e: continue @@ -3461,7 +3461,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"\n📝 Scraping {genre['name']} Staff Picks (individual tracks)...") + print(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 +3471,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"\n📝 Processing {genre['name']} staff pick {i+1}/{len(release_urls)}") + print(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 +3483,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") + print(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]: @@ -3505,7 +3505,7 @@ class BeatportUnifiedScraper: break if not staff_container: - print(f" ❌ Could not find Staff Picks section for {genre['name']}") + print(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) @@ -3547,7 +3547,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"\n📀 Processing {genre['name']} latest release {i+1}/{len(release_urls)}") + print(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 +3559,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") + print(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]: @@ -3581,7 +3581,7 @@ class BeatportUnifiedScraper: break if not latest_container: - print(f" ❌ Could not find Latest Releases section for {genre['name']}") + print(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) @@ -3622,7 +3622,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") + print(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 +3642,9 @@ class BeatportUnifiedScraper: } charts.append(chart_info) - print(f" 📊 Chart {len(charts)}: {chart_name}") + print(f" Chart {len(charts)}: {chart_name}") - print(f" ✅ Found {len(charts)} charts in New Charts Collection") + print(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 +3656,14 @@ class BeatportUnifiedScraper: if not soup: return tracks - print(f" 🔍 Extracting tracks from chart page: {chart_url}") - print(f" 📋 Chart name: {chart_name}") + print(f" Extracting tracks from chart page: {chart_url}") + print(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...") + print(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 +3673,7 @@ class BeatportUnifiedScraper: if len(table_tracks) > len(tracks): tracks = table_tracks - print(f" 📊 Found {len(tracks)} tracks, enriching with per-track metadata...") + print(f" Found {len(tracks)} tracks, enriching with per-track metadata...") # Step 2: Enrich each track by visiting its individual page if tracks: @@ -3682,35 +3682,35 @@ class BeatportUnifiedScraper: return tracks except Exception as e: - print(f" ❌ Error extracting tracks from chart {chart_name}: {e}") + print(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...") + print(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") + print(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") + print(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)}") return tracks - print(f" ✅ Found tracks-table container with classes: {tracks_table.get('class', [])}") + print(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" 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)}") @@ -3719,10 +3719,10 @@ class BeatportUnifiedScraper: 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") + print(f" No track rows found in any format") return tracks - print(f" 🔍 Using {len(track_rows)} track rows for extraction") + print(f" Using {len(track_rows)} track rows for extraction") for i, row in enumerate(track_rows[:limit]): try: @@ -3760,7 +3760,7 @@ class BeatportUnifiedScraper: # DEBUG: Print track details for first few if len(tracks) < 3: - print(f" 🔍 DEBUG Track {len(tracks)+1}:") + print(f" DEBUG Track {len(tracks)+1}:") print(f" Title: '{track_title}'") print(f" Artist: '{artist_text}'") print(f" URL: {track_url}") @@ -3783,13 +3783,13 @@ class BeatportUnifiedScraper: # Debug output for first few tracks if len(tracks) <= 5: - print(f" 🎵 Track {len(tracks)}: {artist_text} - {track_title}") + print(f" Track {len(tracks)}: {artist_text} - {track_title}") except Exception as e: - print(f" ⚠️ Error parsing track row {i+1}: {e}") + print(f" Error parsing track row {i+1}: {e}") continue - print(f" ✅ Chart table extraction completed: {len(tracks)} tracks found") + print(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 +3799,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") + print(f" Found {len(table_rows)} potential table rows") for i, row in enumerate(table_rows[:limit]): try: @@ -3837,7 +3837,7 @@ class BeatportUnifiedScraper: tracks.append(track_data) if len(tracks) <= 3: # Debug first few - print(f" 🎵 Track {len(tracks)}: {artist_text} - {track_title}") + print(f" Track {len(tracks)}: {artist_text} - {track_title}") except Exception as e: continue @@ -3848,7 +3848,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...") + print(f"Discovering sections for {genre['name']} genre page...") soup = self.get_page(genre_url) if not soup: @@ -3886,7 +3886,7 @@ 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']}:") + print(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") @@ -3896,7 +3896,7 @@ class BeatportUnifiedScraper: def scrape_genre_hero_slider(self, genre_slug: str, genre_id: str) -> List[Dict]: """Scrape hero slider data from a genre page""" - print(f"\n🎠 Scraping hero slider for {genre_slug}...") + print(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 +3906,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}") + print(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}") + print(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") + print(f" Found {len(hero_releases)} hero releases") releases_data = [] for i, release in enumerate(hero_releases): @@ -3925,18 +3925,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')}") + print(f" Extracted: {release_data.get('title', 'Unknown')} by {release_data.get('artists_string', 'Unknown')}") else: - print(f" ⚠️ Skipped release {i+1} - incomplete data") + print(f" Skipped release {i+1} - incomplete data") except Exception as e: - print(f" ❌ Error extracting release {i+1}: {e}") + print(f" Error extracting release {i+1}: {e}") - print(f" 📊 Successfully extracted {len(releases_data)} hero releases") + print(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})") + print(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 +3948,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}") + print(f"No tracks-list-item elements found on {genre_url}") return { 'beatport_top10': [], 'hype_top10': [], @@ -3956,7 +3956,7 @@ class BeatportUnifiedScraper: 'has_hype_section': False } - print(f"📊 Found {len(track_items)} total track items") + print(f"Found {len(track_items)} total track items") # Extract track data from all items all_tracks = [] @@ -3983,7 +3983,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") + print(f"Extracted {len(beatport_top10)} Beatport Top 10 + {len(hype_top10)} Hype Top 10 tracks") return { 'beatport_top10': beatport_top10, @@ -4059,12 +4059,12 @@ class BeatportUnifiedScraper: } except Exception as e: - print(f"❌ Error extracting track data: {e}") + print(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})") + print(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 +4076,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}") + print(f"No .partial-artwork elements found on {genre_url}") return [] - print(f"📊 Found {len(partial_artwork_elements)} .partial-artwork elements") + print(f"Found {len(partial_artwork_elements)} .partial-artwork elements") # Extract release data from each element releases = [] @@ -4088,7 +4088,7 @@ class BeatportUnifiedScraper: if release_data: releases.append(release_data) - print(f"✅ Extracted {len(releases)} Top 10 releases") + print(f"Extracted {len(releases)} Top 10 releases") return releases def extract_release_data_from_partial_artwork(self, artwork_element, rank): @@ -4144,7 +4144,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}]") + print(f" Release #{rank}: '{title}' by '{artist}' [{label}]") return { 'title': title, @@ -4158,7 +4158,7 @@ class BeatportUnifiedScraper: } except Exception as e: - print(f"❌ Error extracting release data from .partial-artwork: {e}") + print(f"Error extracting release data from .partial-artwork: {e}") return None def extract_hero_release_data(self, release_element) -> Dict: @@ -4234,7 +4234,7 @@ class BeatportUnifiedScraper: return data except Exception as e: - print(f"⚠️ Error extracting hero release data: {e}") + print(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 +4243,7 @@ class BeatportUnifiedScraper: if not self.all_genres: self.all_genres = self.discover_genres_with_images(include_images=include_images) - print(f"\n🎵 Scraping {len(self.all_genres)} genres...") + print(f"\nScraping {len(self.all_genres)} genres...") all_results = {} completed = 0 @@ -4251,14 +4251,14 @@ class BeatportUnifiedScraper: def scrape_single_genre(genre): nonlocal completed - print(f"🎯 Scraping {genre['name']}...") + print(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)") + print(f"{genre['name']}: {len(tracks)} tracks ({completed}/{len(self.all_genres)} complete)") return genre['name'], tracks @@ -4273,7 +4273,7 @@ class BeatportUnifiedScraper: try: future.result() except Exception as e: - print(f"❌ Error processing {genre['name']}: {e}") + print(f"Error processing {genre['name']}: {e}") return all_results @@ -4304,16 +4304,16 @@ class BeatportUnifiedScraper: def test_dynamic_genre_discovery(): """Test the dynamic genre discovery functionality""" - print("🚀 Dynamic Genre Discovery Test") + print("Dynamic Genre Discovery Test") print("=" * 80) scraper = BeatportUnifiedScraper() # Test genre discovery - print("\n🔍 TEST 1: Genre Discovery") + print("\nTEST 1: Genre Discovery") genres = scraper.discover_genres_from_homepage() - print(f"\n✅ Discovered {len(genres)} genres:") + print(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']})") if 'url' in genre: @@ -4323,41 +4323,41 @@ def test_dynamic_genre_discovery(): print(f" ... and {len(genres) - 10} more genres") # Test with images (limit to 3 for demo) - print("\n📷 TEST 2: Genre Discovery with Images (Sample)") + print("\nTEST 2: Genre Discovery with Images (Sample)") genres_with_images = scraper.discover_genres_with_images(include_images=True) - print(f"\n🖼️ Sample genres with images:") + print(f"\nSample genres with images:") for genre in genres_with_images[:3]: print(f" • {genre['name']}: {genre.get('image_url', 'No image')}") # Test a few genre scrapes - print("\n🎵 TEST 3: Sample Genre Chart Scraping") + print("\nTEST 3: Sample Genre Chart Scraping") sample_genres = genres[:3] for genre in sample_genres: - print(f"\n🎯 Testing {genre['name']}...") + print(f"\nTesting {genre['name']}...") tracks = scraper.scrape_genre_charts(genre, limit=3) if tracks: - print(f" ✅ Found {len(tracks)} tracks:") + print(f" Found {len(tracks)} tracks:") for track in tracks: print(f" • {track['artist']} - {track['title']}") else: - print(f" ❌ No tracks found") + print(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("Testing Improved Chart Section Discovery & Scraping") print("=" * 80) scraper = BeatportUnifiedScraper() # Test 1: Chart Section Discovery - print("\n🔍 TEST 1: Chart Section Discovery") + print("\nTEST 1: Chart Section Discovery") chart_discovery = scraper.discover_chart_sections() - print(f"\n📊 Discovery Results:") + print(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)}") @@ -4366,57 +4366,57 @@ def test_improved_chart_sections(): print(f" • Individual DJ charts: {summary.get('individual_dj_charts', 0)}") # Test 2: New/Improved Scraping Methods - print("\n🔥 TEST 2: Improved Chart Scraping Methods") + print("\nTEST 2: Improved Chart Scraping Methods") # Test Hype Top 100 (fixed URL) print("\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:") + print(f" Found {len(hype_tracks)} tracks:") for track in hype_tracks[:3]: print(f" • {track['artist']} - {track['title']}") else: - print(" ❌ No tracks found") + print(" No tracks found") # Test Top 100 Releases (new method) print("\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:") + print(f" Found {len(releases_tracks)} tracks:") for track in releases_tracks[:3]: print(f" • {track['artist']} - {track['title']}") else: - print(" ❌ No tracks found") + print(" No tracks found") # Test Improved New Releases print("\n2c. Testing Improved New Releases...") new_releases = scraper.scrape_new_releases(limit=5) if new_releases: - print(f" ✅ Found {len(new_releases)} tracks:") + print(f" Found {len(new_releases)} tracks:") for track in new_releases[:3]: print(f" • {track['artist']} - {track['title']}") else: - print(" ❌ No tracks found") + print(" No tracks found") # Test Improved DJ Charts print("\n2d. Testing Improved DJ Charts...") dj_charts = scraper.scrape_dj_charts(limit=5) if dj_charts: - print(f" ✅ Found {len(dj_charts)} charts:") + print(f" Found {len(dj_charts)} charts:") for chart in dj_charts[:3]: print(f" • {chart['title']} by {chart['artist']}") else: - print(" ❌ No charts found") + print(" No charts found") # Test Improved Featured Charts print("\n2e. Testing Improved Featured Charts...") featured_charts = scraper.scrape_featured_charts(limit=5) if featured_charts: - print(f" ✅ Found {len(featured_charts)} items:") + print(f" Found {len(featured_charts)} items:") for item in featured_charts[:3]: print(f" • {item['title']} by {item['artist']}") else: - print(" ❌ No items found") + print(" No items found") return { 'chart_discovery': chart_discovery, @@ -4429,22 +4429,22 @@ def test_improved_chart_sections(): def main(): """Test the unified Beatport scraper""" - print("🚀 Beatport Unified Scraper - Improved Chart Discovery") + print("Beatport Unified Scraper - Improved Chart Discovery") print("=" * 80) scraper = BeatportUnifiedScraper() # Test New on Beatport Hero first - print("\n🎯 NEW ON BEATPORT HERO TEST") + print("\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") + print(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')}") else: - print("❌ No tracks found in hero slideshow") + print("No tracks found in hero slideshow") # Test improved chart sections print("\n🆕 IMPROVED CHART SECTIONS TEST") @@ -4458,21 +4458,21 @@ def main(): scraper.all_genres = discovered_genres # Test 1: Top 100 - print("\n📊 TEST 1: Top 100 Chart") + print("\nTEST 1: Top 100 Chart") top_100 = scraper.scrape_top_100(limit=10) # Test with 10 for now if top_100: - print(f"\n✅ Top 100 Sample (showing first 5):") + print(f"\nTop 100 Sample (showing first 5):") for track in top_100[:5]: print(f" {track['position']}. {track['artist']} - {track['title']}") quality = scraper.test_data_quality(top_100) - print(f"\n📈 Data Quality: {quality['quality_score']:.1f}% ({quality['valid_tracks']}/{quality['total_tracks']} tracks)") + print(f"\nData Quality: {quality['quality_score']:.1f}% ({quality['valid_tracks']}/{quality['total_tracks']} tracks)") else: - print("❌ Failed to extract Top 100") + print("Failed to extract Top 100") # Test 2: Sample of discovered genres - print("\n🎵 TEST 2: Dynamic Genre Charts Sample") + print("\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...") @@ -4482,12 +4482,12 @@ def main(): 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:") + print(f"\n{genre['name']} Top 5:") for track in tracks[:3]: print(f" • {track['artist']} - {track['title']}") # Test 3: Full genre scraping (smaller sample) - print("\n🚀 TEST 3: Full Multi-Genre Scraping") + print("\nTEST 3: Full Multi-Genre Scraping") print("Testing parallel scraping of 10 genres...") sample_genres = scraper.all_genres[:10] @@ -4497,7 +4497,7 @@ def main(): # Results summary print("\n" + "=" * 80) - print("📋 FINAL RESULTS SUMMARY") + print("FINAL RESULTS SUMMARY") print("=" * 80) total_tracks = len(top_100) if top_100 else 0 @@ -4513,7 +4513,7 @@ def main(): 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"\n📊 OVERALL DATA QUALITY") + 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']}") @@ -4536,27 +4536,27 @@ 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"\n💾 Results saved to beatport_unified_results.json") + print(f"\nResults saved to beatport_unified_results.json") except Exception as e: - print(f"❌ Failed to save results: {e}") + print(f"Failed to save results: {e}") # Virtual playlist possibilities if overall_quality['quality_score'] > 70: - print(f"\n🎉 SUCCESS! Ready for virtual playlist creation") - print(f"📱 You can now create playlists for:") + print(f"\nSUCCESS! Ready for virtual playlist creation") + print(f"You can now create playlists for:") print(f" • Beatport Top 100") for genre_name in list(all_genre_results.keys())[:5]: print(f" • {genre_name} Top 100") if len(all_genre_results) > 5: print(f" • ...and {len(all_genre_results) - 5} more genres!") - print(f"\n🔧 Integration Notes:") + 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}%") else: - print(f"\n⚠️ Data quality needs improvement ({overall_quality['quality_score']:.1f}%)") - print(f"💡 Consider refining extraction methods") + print(f"\nData quality needs improvement ({overall_quality['quality_score']:.1f}%)") + print(f"Consider refining extraction methods") if __name__ == "__main__": diff --git a/config/settings.py b/config/settings.py index a43131d9..6d78763b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -33,7 +33,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}") + print(f"ConfigManager initialized with path: {self.config_path}") self.config_data: Dict[str, Any] = {} self._fernet: Optional[Fernet] = None @@ -45,7 +45,7 @@ class ConfigManager: else: self.database_path = self.base_dir / "database" / "music_library.db" - print(f"💾 Database path set to: {self.database_path}") + print(f"Database path set to: {self.database_path}") self.load_config(str(self.config_path)) @@ -107,7 +107,7 @@ class ConfigManager: try: import shutil shutil.move(str(old_key_file), str(key_file)) - print(f"[MIGRATE] 🔑 Moved encryption key to {key_file}") + print(f"[MIGRATE] Moved encryption key to {key_file}") except Exception: key_file = old_key_file # Fall back to old location if key_file.exists(): @@ -155,7 +155,7 @@ 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. " + 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.") return value except Exception: @@ -243,9 +243,9 @@ class ConfigManager: needs_migration = True break if needs_migration: - print("[MIGRATE] 🔐 Encrypting sensitive config values at rest...") + print("[MIGRATE] Encrypting sensitive config values at rest...") self._save_to_database(self.config_data) - print("[OK] ✅ Sensitive config values encrypted successfully") + print("[OK] Sensitive config values encrypted successfully") except Exception as e: print(f"[WARN] Could not migrate encryption: {e}") @@ -505,7 +505,7 @@ class ConfigManager: 2. config.json (migration from file-based config) 3. Defaults (fresh install) """ - print(f"📥 Loading configuration...") + print(f"Loading configuration...") # Try loading from database first config_data = self._load_from_database() @@ -518,18 +518,18 @@ class ConfigManager: return # Database is empty - try migration from config.json - print(f"⚠️ Configuration not found in database. Attempting migration from: {self.config_path}") + print(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...") + print("[MIGRATE] Migrating configuration from config.json to database...") if self._save_to_database(config_data): - print("[OK] ✅ Configuration migrated successfully to database.") + print("[OK] Configuration migrated successfully to database.") self.config_data = config_data return else: - print("[WARN] ⚠️ Migration failed - using file-based config temporarily.") + print("[WARN] Migration failed - using file-based config temporarily.") self.config_data = config_data return @@ -539,9 +539,9 @@ class ConfigManager: # Try to save defaults to database if self._save_to_database(config_data): - print("[OK] ✅ Default configuration saved to database") + print("[OK] Default configuration saved to database") else: - print("[WARN] ⚠️ Could not save defaults to database - using in-memory config") + print("[WARN] Could not save defaults to database - using in-memory config") self.config_data = config_data diff --git a/core/database_update_worker.py b/core/database_update_worker.py index 9748bab6..99eee8c0 100644 --- a/core/database_update_worker.py +++ b/core/database_update_worker.py @@ -143,7 +143,7 @@ class DatabaseUpdateWorker(QThread): else: freed_items = "unknown" self.media_client.clear_cache() - logger.info(f"🧹 Cleared {self.server_type} cache after user stop - freed ~{freed_items} items from memory") + logger.info(f"Cleared {self.server_type} cache after user stop - freed ~{freed_items} items from memory") except Exception as e: logger.warning(f"Could not clear {self.server_type} cache on stop: {e}") @@ -168,7 +168,7 @@ class DatabaseUpdateWorker(QThread): # Connect Navidrome client progress to UI if hasattr(self.media_client, 'set_progress_callback'): self.media_client.set_progress_callback(lambda msg: self._emit_signal('phase_changed', msg)) - logger.info("✅ Connected Navidrome progress callback") + logger.info("Connected Navidrome progress callback") # For full refresh, get all artists artists_to_process = self._get_all_artists() @@ -189,7 +189,7 @@ class DatabaseUpdateWorker(QThread): merge_results = self.database.merge_duplicate_artists() merged = merge_results.get('artists_merged', 0) if merged > 0: - logger.info(f"🧹 Merged {merged} duplicate artists") + logger.info(f"Merged {merged} duplicate artists") except Exception as e: logger.warning(f"Could not merge duplicate artists: {e}") self._emit_signal('finished', 0, 0, 0, 0, 0) @@ -204,7 +204,7 @@ class DatabaseUpdateWorker(QThread): self._process_jellyfin_new_tracks_directly(artists_to_process) else: # Standard artist processing for Plex or full refresh - logger.info(f"🎯 About to process {len(artists_to_process) if artists_to_process else 0} artists for {self.server_type}") + logger.info(f"About to process {len(artists_to_process) if artists_to_process else 0} artists for {self.server_type}") self._process_all_artists(artists_to_process) # Record full refresh completion for tracking purposes @@ -224,7 +224,7 @@ class DatabaseUpdateWorker(QThread): else: freed_items = "cache data" self.media_client.clear_cache() - logger.info(f"🧹 Cleared {self.server_type} cache after full refresh - freed ~{freed_items} items from memory") + logger.info(f"Cleared {self.server_type} cache after full refresh - freed ~{freed_items} items from memory") except Exception as e: logger.warning(f"Could not clear {self.server_type} cache: {e}") @@ -240,7 +240,7 @@ class DatabaseUpdateWorker(QThread): r_albums = removal_results.get('albums_removed', 0) r_tracks = removal_results.get('tracks_removed', 0) if r_artists > 0 or r_albums > 0: - logger.info(f"🗑️ Removal detection: {r_artists} artists, " + logger.info(f"Removal detection: {r_artists} artists, " f"{r_albums} albums, {r_tracks} tracks removed") except Exception as e: logger.warning(f"Removal detection failed (non-fatal): {e}") @@ -253,9 +253,9 @@ class DatabaseUpdateWorker(QThread): orphaned_albums = cleanup_results.get('orphaned_albums_removed', 0) if orphaned_artists > 0 or orphaned_albums > 0: - logger.info(f"🧹 Cleanup complete: {orphaned_artists} orphaned artists, {orphaned_albums} orphaned albums removed") + logger.info(f"Cleanup complete: {orphaned_artists} orphaned artists, {orphaned_albums} orphaned albums removed") else: - logger.debug("🧹 Cleanup complete: No orphaned records found") + logger.debug("Cleanup complete: No orphaned records found") except Exception as e: logger.warning(f"Could not cleanup orphaned records: {e}") @@ -265,7 +265,7 @@ class DatabaseUpdateWorker(QThread): merge_results = self.database.merge_duplicate_artists() merged = merge_results.get('artists_merged', 0) if merged > 0: - logger.info(f"🧹 Merged {merged} duplicate artists") + logger.info(f"Merged {merged} duplicate artists") except Exception as e: logger.warning(f"Could not merge duplicate artists: {e}") @@ -437,9 +437,9 @@ class DatabaseUpdateWorker(QThread): logger.error(f"Could not connect to {self.server_type} server — check URL, credentials, and network (Docker users: use container name or host.docker.internal instead of host IP)") return [] - logger.info(f"🎯 _get_all_artists: Calling media_client.get_all_artists() for {self.server_type}") + logger.info(f"_get_all_artists: Calling media_client.get_all_artists() for {self.server_type}") artists = self.media_client.get_all_artists() - logger.info(f"🎯 _get_all_artists: Received {len(artists) if artists else 0} artists from {self.server_type}") + logger.info(f"_get_all_artists: Received {len(artists) if artists else 0} artists from {self.server_type}") return artists except Exception as e: @@ -581,9 +581,9 @@ class DatabaseUpdateWorker(QThread): if not track_exists: missing_tracks_count += 1 album_has_new_tracks = True - logger.debug(f"📀 Track '{track_title}' is new - album needs processing") + logger.debug(f"Track '{track_title}' is new - album needs processing") else: - logger.debug(f"✅ Track '{track_title}' already exists") + logger.debug(f"Track '{track_title}' already exists") except Exception as track_error: logger.debug(f"Error checking individual track: {track_error}") @@ -595,23 +595,23 @@ class DatabaseUpdateWorker(QThread): if album_has_new_tracks: albums_with_new_content += 1 consecutive_complete_albums = 0 # Reset counter - logger.info(f"📀 Album '{album_title}' has {missing_tracks_count} new tracks - needs processing") + logger.info(f"Album '{album_title}' has {missing_tracks_count} new tracks - needs processing") else: # Check if existing tracks have metadata changes (catches Plex corrections) metadata_changed = self._check_for_metadata_changes(tracks) if metadata_changed: albums_with_new_content += 1 consecutive_complete_albums = 0 # Reset counter - logger.info(f"🔄 Album '{album_title}' has metadata changes - needs processing") + logger.info(f"Album '{album_title}' has metadata changes - needs processing") album_has_new_tracks = True # Mark for artist processing else: consecutive_complete_albums += 1 - logger.debug(f"✅ Album '{album_title}' is fully up-to-date (consecutive complete: {consecutive_complete_albums})") + logger.debug(f"Album '{album_title}' is fully up-to-date (consecutive complete: {consecutive_complete_albums})") # Very conservative stopping criteria: 25 consecutive complete albums after metadata fixes # This ensures we don't miss scattered updated content from manual corrections if consecutive_complete_albums >= 25: - logger.info(f"🛑 Found 25 consecutive complete albums - stopping incremental scan after checking {total_tracks_checked} tracks from {i+1} albums") + logger.info(f"Found 25 consecutive complete albums - stopping incremental scan after checking {total_tracks_checked} tracks from {i+1} albums") stopped_early = True break @@ -633,7 +633,7 @@ class DatabaseUpdateWorker(QThread): if artist_id not in processed_artist_ids: processed_artist_ids.add(artist_id) artists_to_process.append(album_artist) - logger.info(f"✅ Added artist '{album_artist.title}' for processing (from album '{album_title}' with new tracks)") + logger.info(f"Added artist '{album_artist.title}' for processing (from album '{album_title}' with new tracks)") except Exception as artist_error: logger.warning(f"Error getting artist for album '{album_title}': {artist_error}") @@ -649,7 +649,7 @@ class DatabaseUpdateWorker(QThread): else: result_msg += f" (checked all {total_tracks_checked} tracks from {len(recent_albums)} recent albums)" - logger.info(f"📊 Incremental scan stats: {len(recent_albums)} recent albums examined, {albums_with_new_content} needed processing") + logger.info(f"Incremental scan stats: {len(recent_albums)} recent albums examined, {albums_with_new_content} needed processing") logger.info(result_msg) return artists_to_process @@ -662,7 +662,7 @@ class DatabaseUpdateWorker(QThread): def _get_artists_for_navidrome_incremental_update(self) -> List: """Get artists for Navidrome incremental update using smart early-stopping logic like Plex/Jellyfin""" try: - logger.info("🎵 Navidrome incremental: Getting recent albums and checking for new content...") + logger.info("Navidrome incremental: Getting recent albums and checking for new content...") # Get recent albums from Navidrome (use the generic method that calls Navidrome-specific logic) recent_albums = self._get_recent_albums_for_server() @@ -720,11 +720,11 @@ class DatabaseUpdateWorker(QThread): # If no new tracks found, increment consecutive complete counter if not album_has_new_tracks: consecutive_complete_albums += 1 - logger.debug(f"✅ Album '{album_title}' is up-to-date (consecutive: {consecutive_complete_albums})") + logger.debug(f"Album '{album_title}' is up-to-date (consecutive: {consecutive_complete_albums})") # Early stopping after 25 consecutive complete albums (same as Plex/Jellyfin) if consecutive_complete_albums >= 25: - logger.info(f"🛑 Found 25 consecutive complete albums - stopping incremental scan after checking {total_tracks_checked} tracks from {i+1} albums") + logger.info(f"Found 25 consecutive complete albums - stopping incremental scan after checking {total_tracks_checked} tracks from {i+1} albums") break except Exception as tracks_error: @@ -744,7 +744,7 @@ class DatabaseUpdateWorker(QThread): if artist_id not in processed_artist_ids: processed_artist_ids.add(artist_id) artists_to_process.append(album_artist) - logger.info(f"✅ Added artist '{album_artist.title}' for processing (from album '{album_title}' with new tracks)") + logger.info(f"Added artist '{album_artist.title}' for processing (from album '{album_title}' with new tracks)") except Exception as artist_error: logger.warning(f"Error getting artist for album '{album_title}': {artist_error}") @@ -753,7 +753,7 @@ class DatabaseUpdateWorker(QThread): consecutive_complete_albums = 0 # Reset on error continue - logger.info(f"🎵 Navidrome incremental complete: {len(artists_to_process)} artists need processing (checked {total_tracks_checked} tracks from {len(recent_albums)} recent albums)") + logger.info(f"Navidrome incremental complete: {len(artists_to_process)} artists need processing (checked {total_tracks_checked} tracks from {len(recent_albums)} recent albums)") return artists_to_process except Exception as e: @@ -763,7 +763,7 @@ class DatabaseUpdateWorker(QThread): def _get_artists_for_jellyfin_track_incremental_update(self) -> List: """FAST Jellyfin incremental update using recent tracks directly (no caching needed)""" try: - logger.info("🚀 FAST Jellyfin incremental: getting recent tracks directly...") + logger.info("FAST Jellyfin incremental: getting recent tracks directly...") # Get recent tracks directly from Jellyfin (FAST - 2 API calls) recent_added_tracks = self.media_client.get_recently_added_tracks(5000) @@ -799,14 +799,14 @@ class DatabaseUpdateWorker(QThread): if not track_exists: new_tracks.append(track) consecutive_existing_tracks = 0 # Reset counter - logger.debug(f"🎵 New track: {track.title}") + logger.debug(f"New track: {track.title}") else: consecutive_existing_tracks += 1 - logger.debug(f"✅ Track exists: {track.title}") + logger.debug(f"Track exists: {track.title}") # Early stopping: if we find 100 consecutive existing tracks, we're done if consecutive_existing_tracks >= 100: - logger.info(f"🛑 Found 100 consecutive existing tracks - stopping after checking {i+1} tracks") + logger.info(f"Found 100 consecutive existing tracks - stopping after checking {i+1} tracks") break except Exception as e: @@ -833,12 +833,12 @@ class DatabaseUpdateWorker(QThread): if artist_id not in processed_artists: processed_artists.add(artist_id) artists_to_process.append(track_artist) - logger.info(f"✅ Added artist '{track_artist.title}' (from new track '{track.title}')") + logger.info(f"Added artist '{track_artist.title}' (from new track '{track.title}')") except Exception as e: logger.debug(f"Error getting artist for track {getattr(track, 'title', 'Unknown')}: {e}") continue - logger.info(f"🚀 FAST incremental complete: {len(artists_to_process)} artists need processing (from {len(new_tracks)} new tracks)") + logger.info(f"FAST incremental complete: {len(artists_to_process)} artists need processing (from {len(new_tracks)} new tracks)") return artists_to_process except Exception as e: @@ -853,7 +853,7 @@ class DatabaseUpdateWorker(QThread): logger.warning("No new tracks to process directly") return - logger.info(f"🚀 FAST PROCESSING: Directly processing {len(new_tracks)} new tracks...") + logger.info(f"FAST PROCESSING: Directly processing {len(new_tracks)} new tracks...") # Group tracks by album and artist for efficient processing tracks_by_album = {} @@ -921,7 +921,7 @@ class DatabaseUpdateWorker(QThread): track_success = self.database.insert_or_update_media_track(track, album_id, artist_id, server_source=self.server_type) if track_success: total_processed_tracks += 1 - logger.debug(f"✅ Processed new track: {track.title}") + logger.debug(f"Processed new track: {track.title}") except Exception as e: logger.warning(f"Failed to process track '{getattr(track, 'title', 'Unknown')}': {e}") except Exception as e: @@ -943,7 +943,7 @@ class DatabaseUpdateWorker(QThread): self.processed_tracks += total_processed_tracks self.successful_operations += total_processed_artists # Count successful artists - logger.info(f"🚀 FAST PROCESSING COMPLETE: {total_processed_artists} artists, {total_processed_albums} albums, {total_processed_tracks} tracks") + logger.info(f"FAST PROCESSING COMPLETE: {total_processed_artists} artists, {total_processed_albums} albums, {total_processed_tracks} tracks") # Clean up delattr(self, '_jellyfin_new_tracks') @@ -976,7 +976,7 @@ class DatabaseUpdateWorker(QThread): 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"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}'") @@ -987,7 +987,7 @@ class DatabaseUpdateWorker(QThread): continue if changes_detected > 0: - logger.info(f"🔄 Found {changes_detected} tracks with metadata changes") + logger.info(f"Found {changes_detected} tracks with metadata changes") return True return False @@ -1014,7 +1014,7 @@ class DatabaseUpdateWorker(QThread): return None # Fetch current IDs from media server (lightweight calls) - logger.info(f"🔍 Removal detection: fetching current IDs from {self.server_type}...") + logger.info(f"Removal detection: fetching current IDs from {self.server_type}...") self._emit_signal('phase_changed', f"Fetching artist catalog from {self.server_type}...") server_artist_ids = self.media_client.get_all_artist_ids() self._emit_signal('phase_changed', f"Fetching album catalog from {self.server_type}...") @@ -1022,7 +1022,7 @@ class DatabaseUpdateWorker(QThread): # Safety: if both come back empty, the server is unreachable if not server_artist_ids and not server_album_ids: - logger.warning("🛡️ SAFETY: Server returned zero artists AND zero albums — " + logger.warning("SAFETY: Server returned zero artists AND zero albums — " "skipping removal detection") return None @@ -1044,19 +1044,19 @@ class DatabaseUpdateWorker(QThread): if check_artists and db_artist_count > 100: if len(server_artist_ids) < db_artist_count * 0.5: logger.warning( - f"🛡️ SAFETY: Server reported {len(server_artist_ids)} artists but " + f"SAFETY: Server reported {len(server_artist_ids)} artists but " f"database has {db_artist_count} — skipping artist removal check") check_artists = False if check_albums and db_album_count > 100: if len(server_album_ids) < db_album_count * 0.5: logger.warning( - f"🛡️ SAFETY: Server reported {len(server_album_ids)} albums but " + f"SAFETY: Server reported {len(server_album_ids)} albums but " f"database has {db_album_count} — skipping album removal check") check_albums = False if not check_artists and not check_albums: - logger.warning("🛡️ SAFETY: Both artist and album checks disabled — " + logger.warning("SAFETY: Both artist and album checks disabled — " "skipping removal detection") return None @@ -1090,11 +1090,11 @@ class DatabaseUpdateWorker(QThread): pass # If this optimization fails, double-delete is harmless if not removed_artist_ids and not removed_album_ids: - logger.info("🔍 Removal detection: no stale content found") + logger.info("Removal detection: no stale content found") self._emit_signal('phase_changed', "No removed content detected") return {'artists_removed': 0, 'albums_removed': 0, 'tracks_removed': 0} - logger.info(f"🗑️ Removal detection: found {len(removed_artist_ids)} removed artists, " + logger.info(f"Removal detection: found {len(removed_artist_ids)} removed artists, " f"{len(removed_album_ids)} removed albums") self._emit_signal('phase_changed', @@ -1219,7 +1219,7 @@ class DatabaseUpdateWorker(QThread): def _process_all_artists(self, artists: List): """Process all artists and their albums/tracks using thread pool""" total_artists = len(artists) - logger.info(f"🎯 Processing {total_artists} artists with progress tracking") + logger.info(f"Processing {total_artists} artists with progress tracking") def process_single_artist(artist): """Process a single artist and return results""" @@ -1240,7 +1240,7 @@ class DatabaseUpdateWorker(QThread): total_artists, progress_percent ) - logger.debug(f"🔄 Progress: {self.processed_artists}/{total_artists} ({progress_percent:.1f}%) - {artist_name}") + logger.debug(f"Progress: {self.processed_artists}/{total_artists} ({progress_percent:.1f}%) - {artist_name}") # Process the artist success, details, album_count, track_count = self._process_artist_with_content(artist) diff --git a/core/download_orchestrator.py b/core/download_orchestrator.py index e48f4278..d1d630f3 100644 --- a/core/download_orchestrator.py +++ b/core/download_orchestrator.py @@ -51,7 +51,7 @@ class DownloadOrchestrator: self.lidarr = self._safe_init('Lidarr', LidarrDownloadClient) if self._init_failures: - logger.warning(f"⚠️ Download clients failed to initialize: {', '.join(self._init_failures)}") + logger.warning(f"Download clients failed to initialize: {', '.join(self._init_failures)}") # Load mode from config self.mode = config_manager.get('download_source.mode', 'soulseek') @@ -59,7 +59,7 @@ class DownloadOrchestrator: self.hybrid_secondary = config_manager.get('download_source.hybrid_secondary', 'youtube') self.hybrid_order = config_manager.get('download_source.hybrid_order', ['hifi', 'youtube', 'soulseek']) - logger.info(f"🎛️ Download Orchestrator initialized - Mode: {self.mode}") + 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)}") @@ -71,7 +71,7 @@ class DownloadOrchestrator: try: return cls() except Exception as e: - logger.error(f"❌ {name} download client failed to initialize: {e}") + logger.error(f"{name} download client failed to initialize: {e}") self._init_failures.append(name) return None @@ -85,7 +85,7 @@ class DownloadOrchestrator: # Reload underlying client configs (SLSKD URL, API key, etc.) if self.soulseek: self.soulseek._setup_client() - logger.info(f"🔄 Soulseek client config reloaded") + logger.info(f"Soulseek client config reloaded") # Reconnect Deezer if ARL changed deezer_arl = config_manager.get('deezer_download.arl', '') @@ -93,7 +93,7 @@ class DownloadOrchestrator: self.deezer_dl.reconnect(deezer_arl) self.deezer_dl._quality = config_manager.get('deezer_download.quality', 'flac') - logger.info(f"🔄 Download Orchestrator settings reloaded - Mode: {self.mode}") + logger.info(f"Download Orchestrator settings reloaded - Mode: {self.mode}") def _client(self, name): """Get a client by name, returning None if not initialized.""" @@ -143,7 +143,7 @@ class DownloadOrchestrator: except Exception: results[source] = False - status_parts = [f"{s}: {'✅' if ok else '❌'}" for s, ok in results.items()] + status_parts = [f"{s}: {'' if ok else ''}" for s, ok in results.items()] logger.info(f" {' | '.join(status_parts)}") return any(results.values()) @@ -168,9 +168,9 @@ class DownloadOrchestrator: if self.mode != 'hybrid': client = self._client(self.mode) if not client: - logger.error(f"❌ {source_names.get(self.mode, self.mode)} client not available (failed to initialize)") + logger.error(f"{source_names.get(self.mode, self.mode)} client not available (failed to initialize)") return [], [] - logger.info(f"🔍 Searching {source_names.get(self.mode, self.mode)}: {query}") + logger.info(f"Searching {source_names.get(self.mode, self.mode)}: {query}") return await client.search(query, timeout, progress_callback) elif self.mode == 'hybrid': @@ -189,33 +189,33 @@ class DownloadOrchestrator: if not source_order: source_order = ['soulseek'] - logger.info(f"🔍 Hybrid search ({' → '.join(source_order)}): {query}") + logger.info(f"Hybrid search ({' → '.join(source_order)}): {query}") # Try each source in priority order (skip unconfigured/unavailable ones) for i, source_name in enumerate(source_order): client = clients.get(source_name) if not client: - logger.info(f"⏭️ Skipping {source_name} (not available)") + logger.info(f"Skipping {source_name} (not available)") continue if hasattr(client, 'is_configured') and not client.is_configured(): - logger.info(f"⏭️ Skipping {source_name} (not configured)") + logger.info(f"Skipping {source_name} (not configured)") continue try: if i == 0: - logger.info(f"🔍 Trying {source_name} (priority {i+1}): {query}") + logger.info(f"Trying {source_name} (priority {i+1}): {query}") else: - logger.info(f"🔄 Trying {source_name} (priority {i+1}): {query}") + logger.info(f"Trying {source_name} (priority {i+1}): {query}") tracks, albums = await client.search(query, timeout, progress_callback) if tracks: - logger.info(f"✅ {source_name} found {len(tracks)} tracks") + logger.info(f"{source_name} found {len(tracks)} tracks") return (tracks, albums) except Exception as e: - logger.warning(f"⚠️ {source_name} search failed: {e}") + logger.warning(f"{source_name} search failed: {e}") # Nothing found from any source - logger.warning(f"❌ Hybrid search: all sources ({', '.join(source_order)}) found nothing for: {query}") + logger.warning(f"Hybrid search: all sources ({', '.join(source_order)}) found nothing for: {query}") return ([], []) # Fallback: empty results @@ -289,10 +289,10 @@ class DownloadOrchestrator: if scored: scored.sort(key=lambda x: x._match_confidence, reverse=True) filtered_results = scored - logger.info(f"🎵 Streaming validation: {len(scored)}/{len(tracks)} passed " + logger.info(f"Streaming validation: {len(scored)}/{len(tracks)} passed " f"(best: {scored[0]._match_confidence:.2f})") else: - logger.warning(f"⚠️ No streaming results passed validation for: {query}") + logger.warning(f"No streaming results passed validation for: {query}") return None elif is_streaming: filtered_results = tracks @@ -337,12 +337,12 @@ class DownloadOrchestrator: client = source_map[username] if not client: raise RuntimeError(f"{source_names[username]} download client not available (failed to initialize)") - logger.info(f"📥 Downloading from {source_names[username]}: {filename}") + logger.info(f"Downloading from {source_names[username]}: {filename}") return await client.download(username, filename, file_size) else: if not self.soulseek: raise RuntimeError("Soulseek client not available (failed to initialize)") - logger.info(f"📥 Downloading from Soulseek: {filename}") + logger.info(f"Downloading from Soulseek: {filename}") return await self.soulseek.download(username, filename, file_size) async def get_all_downloads(self) -> List[DownloadStatus]: diff --git a/core/itunes_client.py b/core/itunes_client.py index 0fbac47c..828088f6 100644 --- a/core/itunes_client.py +++ b/core/itunes_client.py @@ -1053,7 +1053,7 @@ class iTunesClient: logger.debug(f"Replacing clean version with explicit: {album.name} (verified {track_count} tracks)") seen_albums[normalized_name] = {'album': album, 'is_explicit': is_explicit} else: - logger.warning(f"⚠️ Skipping broken explicit album {album.name} (ID {album.id}): reports tracks but has 0") + logger.warning(f"Skipping broken explicit album {album.name} (ID {album.id}): reports tracks but has 0") except Exception as e: logger.warning(f"Failed to validate explicit album {album.name}: {e}, keeping clean version") else: @@ -1072,7 +1072,7 @@ class iTunesClient: logger.debug(f" Verified explicit album has {track_count} tracks") seen_albums[normalized_name] = {'album': album, 'is_explicit': is_explicit} else: - logger.warning(f"⚠️ Skipping broken explicit album {album.name} (ID {album.id}): reports tracks but has 0") + logger.warning(f"Skipping broken explicit album {album.name} (ID {album.id}): reports tracks but has 0") # Don't add to seen_albums so a clean version can be added later except Exception as e: logger.warning(f"Failed to validate explicit album {album.name}: {e}, skipping") diff --git a/core/jellyfin_client.py b/core/jellyfin_client.py index f46b24b7..da32a6c0 100644 --- a/core/jellyfin_client.py +++ b/core/jellyfin_client.py @@ -176,7 +176,7 @@ class JellyfinClient: self.music_library_id = None self._connection_attempted = False self.clear_cache() - logger.info("🔄 Jellyfin client config reset — will reconnect with new settings") + logger.info("Jellyfin client config reset — will reconnect with new settings") def ensure_connection(self) -> bool: """Ensure connection to Jellyfin server with lazy initialization.""" @@ -493,17 +493,17 @@ class JellyfinClient: # Check if we're in metadata-only mode and skip expensive operations if self._metadata_only_mode: - logger.info("🎯 Skipping cache population for metadata-only operation") + logger.info("Skipping cache population for metadata-only operation") self._cache_populated = True return - logger.info("🚀 Starting aggressive Jellyfin cache population to eliminate slow individual API calls...") + logger.info("Starting aggressive Jellyfin cache population to eliminate slow individual API calls...") if self._progress_callback: self._progress_callback("Fetching all tracks in bulk...") try: # SIMPLIFIED APPROACH: Fetch all tracks, then all albums separately (robust and fast) - logger.info("🎵 Fetching all tracks in bulk...") + logger.info("Fetching all tracks in bulk...") all_tracks = [] start_index = 0 limit = 10000 @@ -530,13 +530,13 @@ class JellyfinClient: if limit > 1000: limit = limit // 2 consecutive_failures = 0 # Reset — give the smaller batch a fair chance - logger.warning(f"⚠️ Track fetch failed - reducing batch size to {limit}") + logger.warning(f"Track fetch failed - reducing batch size to {limit}") continue elif consecutive_failures >= 2: - logger.warning("🚨 Multiple track fetch failures at minimum batch size - stopping") + logger.warning("Multiple track fetch failures at minimum batch size - stopping") break else: - logger.warning("⚠️ Track fetch failed at minimum batch size - retrying once") + logger.warning("Track fetch failed at minimum batch size - retrying once") continue consecutive_failures = 0 @@ -551,7 +551,7 @@ class JellyfinClient: start_index += limit progress_msg = f"Fetched {len(all_tracks)} tracks so far..." - logger.info(f" 🎵 {progress_msg} (batch size: {limit})") + logger.info(f" {progress_msg} (batch size: {limit})") if self._progress_callback: self._progress_callback(progress_msg) @@ -564,12 +564,12 @@ class JellyfinClient: self._track_cache[album_id] = [] self._track_cache[album_id].append(JellyfinTrack(track_data, self)) - logger.info(f"✅ Cached {len(all_tracks)} tracks for {len(self._track_cache)} albums") + logger.info(f"Cached {len(all_tracks)} tracks for {len(self._track_cache)} albums") if self._progress_callback: self._progress_callback(f"Cached {len(all_tracks)} tracks. Now fetching albums...") # STEP 2: Fetch all albums in bulk (same proven pattern) - logger.info("📀 Fetching all albums in bulk...") + logger.info("Fetching all albums in bulk...") all_albums = [] start_index = 0 limit = 10000 @@ -596,13 +596,13 @@ class JellyfinClient: if limit > 1000: limit = limit // 2 consecutive_failures = 0 # Reset — give the smaller batch a fair chance - logger.warning(f"⚠️ Album fetch failed - reducing batch size to {limit}") + logger.warning(f"Album fetch failed - reducing batch size to {limit}") continue elif consecutive_failures >= 2: - logger.warning("🚨 Multiple album fetch failures at minimum batch size - stopping") + logger.warning("Multiple album fetch failures at minimum batch size - stopping") break else: - logger.warning("⚠️ Album fetch failed at minimum batch size - retrying once") + logger.warning("Album fetch failed at minimum batch size - retrying once") continue consecutive_failures = 0 @@ -617,7 +617,7 @@ class JellyfinClient: start_index += limit progress_msg = f"Fetched {len(all_albums)} albums so far..." - logger.info(f" 📀 {progress_msg} (batch size: {limit})") + logger.info(f" {progress_msg} (batch size: {limit})") if self._progress_callback: self._progress_callback(progress_msg) @@ -632,10 +632,10 @@ class JellyfinClient: self._album_cache[artist_id] = [] self._album_cache[artist_id].append(JellyfinAlbum(album_data, self)) - logger.info(f"✅ Cached {len(all_albums)} albums for {len(self._album_cache)} artists") + logger.info(f"Cached {len(all_albums)} albums for {len(self._album_cache)} artists") self._cache_populated = True - logger.info("🎯 AGGRESSIVE CACHE COMPLETE! All subsequent album/track lookups will be INSTANT!") + logger.info("AGGRESSIVE CACHE COMPLETE! All subsequent album/track lookups will be INSTANT!") if self._progress_callback: self._progress_callback("Cache complete! Now processing artists...") @@ -648,7 +648,7 @@ class JellyfinClient: if not albums: return - logger.info(f"🎯 Starting targeted Jellyfin cache for {len(albums)} recent albums...") + logger.info(f"Starting targeted Jellyfin cache for {len(albums)} recent albums...") if self._progress_callback: self._progress_callback(f"Caching tracks for {len(albums)} recent albums...") @@ -688,11 +688,11 @@ class JellyfinClient: # Progress update every 50 albums if (i + 1) % 50 == 0 or i == len(album_ids) - 1: progress_msg = f"Cached {cached_tracks} tracks from {i + 1} albums..." - logger.info(f" 🎯 {progress_msg}") + logger.info(f" {progress_msg}") if self._progress_callback: self._progress_callback(progress_msg) - logger.info(f"✅ Targeted cache complete: {cached_tracks} tracks cached for {len(self._track_cache)} albums") + logger.info(f"Targeted cache complete: {cached_tracks} tracks cached for {len(self._track_cache)} albums") if self._progress_callback: self._progress_callback("Targeted cache complete! Now checking for new tracks...") @@ -1300,7 +1300,7 @@ class JellyfinClient: result = response.json() if result and 'Id' in result: - logger.info(f"✅ Created Jellyfin playlist '{name}' with {len(track_ids)} tracks") + logger.info(f"Created Jellyfin playlist '{name}' with {len(track_ids)} tracks") return True else: logger.error(f"Failed to create Jellyfin playlist '{name}': No playlist ID returned") @@ -1407,7 +1407,7 @@ class JellyfinClient: logger.error(f" Request params: Ids={add_params['Ids'][:200]}... (truncated)") # Continue with other batches even if one fails - logger.info(f"✅ Created large Jellyfin playlist '{name}' with {len(track_ids)} tracks in {total_batches} batches") + logger.info(f"Created large Jellyfin playlist '{name}' with {len(track_ids)} tracks in {total_batches} batches") return True except Exception as e: @@ -1453,7 +1453,7 @@ class JellyfinClient: try: success = self.create_playlist(target_name, source_tracks) if success: - logger.info(f"✅ Created backup playlist '{target_name}' with {len(source_tracks)} tracks") + logger.info(f"Created backup playlist '{target_name}' with {len(source_tracks)} tracks") return True else: logger.error(f"Failed to create backup playlist '{target_name}'") @@ -1541,12 +1541,12 @@ class JellyfinClient: if existing_playlist and create_backup: backup_name = f"{playlist_name} Backup" - logger.info(f"🛡️ Creating backup playlist '{backup_name}' before sync") + logger.info(f"Creating backup playlist '{backup_name}' before sync") if self.copy_playlist(playlist_name, backup_name): - logger.info(f"✅ Backup created successfully") + logger.info(f"Backup created successfully") else: - logger.warning(f"⚠️ Failed to create backup, continuing with sync") + logger.warning(f"Failed to create backup, continuing with sync") if existing_playlist: # Delete existing playlist using DELETE request @@ -1612,7 +1612,7 @@ class JellyfinClient: response = requests.post(url, headers=headers, params=params, timeout=10) response.raise_for_status() - logger.info(f"🎵 Triggered Jellyfin library scan for '{library_name}'") + logger.info(f"Triggered Jellyfin library scan for '{library_name}'") return True except Exception as e: @@ -1622,14 +1622,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("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("DEBUG: Could not get scheduled tasks") return False for task in response: @@ -1639,10 +1639,10 @@ 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(f"DEBUG: Found running scan task: {task.get('Name')} (State: {task_state})") return True - logger.debug("🔍 DEBUG: No active scan tasks detected") + logger.debug("DEBUG: No active scan tasks detected") return False except Exception as e: @@ -1802,9 +1802,9 @@ class JellyfinClient: try: self._metadata_only_mode = enabled if enabled: - logger.info("🎯 Metadata-only mode enabled - will skip expensive track caching") + logger.info("Metadata-only mode enabled - will skip expensive track caching") else: - logger.info("🎯 Metadata-only mode disabled") + logger.info("Metadata-only mode disabled") return True except Exception as e: logger.error(f"Error setting metadata-only mode: {e}") diff --git a/core/listenbrainz_client.py b/core/listenbrainz_client.py index 78f032e5..2ec6ed0e 100644 --- a/core/listenbrainz_client.py +++ b/core/listenbrainz_client.py @@ -72,10 +72,10 @@ class ListenBrainzClient: data = response.json() if data.get('valid'): self.username = data.get('user_name') - logger.info(f"✅ ListenBrainz authenticated as: {self.username}") + logger.info(f"ListenBrainz authenticated as: {self.username}") return True - logger.warning("❌ Invalid ListenBrainz token") + logger.warning("Invalid ListenBrainz token") return False except Exception as e: logger.error(f"Error validating ListenBrainz token: {e}") @@ -179,7 +179,7 @@ class ListenBrainzClient: if response and response.status_code == 200: data = response.json() playlists = data.get('playlists', []) - logger.info(f"📋 Fetched {len(playlists)} playlists created for {self.username}") + logger.info(f"Fetched {len(playlists)} playlists created for {self.username}") return playlists elif response and response.status_code == 404: logger.warning(f"User {self.username} not found") @@ -215,7 +215,7 @@ class ListenBrainzClient: if response and response.status_code == 200: data = response.json() playlists = data.get('playlists', []) - logger.info(f"📋 Fetched {len(playlists)} user playlists for {self.username}") + logger.info(f"Fetched {len(playlists)} user playlists for {self.username}") return playlists elif response and response.status_code == 404: logger.warning(f"User {self.username} not found") @@ -251,7 +251,7 @@ class ListenBrainzClient: if response and response.status_code == 200: data = response.json() playlists = data.get('playlists', []) - logger.info(f"📋 Fetched {len(playlists)} collaborative playlists for {self.username}") + logger.info(f"Fetched {len(playlists)} collaborative playlists for {self.username}") return playlists elif response and response.status_code == 404: logger.warning(f"User {self.username} not found") @@ -291,7 +291,7 @@ class ListenBrainzClient: data = response.json() playlist = data.get('playlist', {}) track_count = len(playlist.get('track', [])) - logger.info(f"📋 Fetched playlist '{playlist.get('title')}' with {track_count} tracks") + logger.info(f"Fetched playlist '{playlist.get('title')}' with {track_count} tracks") return playlist elif response and response.status_code == 404: logger.warning(f"Playlist {playlist_mbid} not found") @@ -333,7 +333,7 @@ class ListenBrainzClient: if response.status_code == 200: data = response.json() playlists = data.get('playlists', []) - logger.info(f"🔍 Found {len(playlists)} playlists matching '{query}'") + logger.info(f"Found {len(playlists)} playlists matching '{query}'") return playlists else: logger.error(f"Failed to search playlists: {response.status_code}") diff --git a/core/listenbrainz_manager.py b/core/listenbrainz_manager.py index e0636f6c..e36ee65b 100644 --- a/core/listenbrainz_manager.py +++ b/core/listenbrainz_manager.py @@ -79,7 +79,7 @@ class ListenBrainzManager: "error": "Not authenticated" } - logger.info("🔄 Starting ListenBrainz playlists update...") + logger.info("Starting ListenBrainz playlists update...") summary = { "created_for": {"updated": 0, "skipped": 0, "new": 0}, @@ -97,7 +97,7 @@ class ListenBrainzManager: for playlist_type, fetch_func in playlist_types: try: playlists = fetch_func() - logger.info(f"📋 Fetched {len(playlists)} {playlist_type} playlists") + logger.info(f"Fetched {len(playlists)} {playlist_type} playlists") for playlist in playlists: result = self._update_playlist(playlist, playlist_type) @@ -114,7 +114,7 @@ class ListenBrainzManager: # Cleanup old playlists (keep only 4 most recent per type) self._cleanup_old_playlists() - logger.info(f"✅ ListenBrainz update complete: {summary}") + logger.info(f"ListenBrainz update complete: {summary}") return { "success": True, "summary": summary @@ -165,11 +165,11 @@ class ListenBrainzManager: # Skip if track count hasn't changed (playlist content likely the same) if db_track_count == track_count: - logger.debug(f"✓ Playlist '{title}' unchanged, skipping") + logger.debug(f"Playlist '{title}' unchanged, skipping") conn.close() return "skipped" - logger.info(f"🔄 Playlist '{title}' changed ({db_track_count} → {track_count} tracks), updating...") + logger.info(f"Playlist '{title}' changed ({db_track_count} → {track_count} tracks), updating...") # Delete old tracks cursor.execute("DELETE FROM listenbrainz_tracks WHERE playlist_id = ?", (db_id,)) @@ -185,7 +185,7 @@ class ListenBrainzManager: result_type = "updated" else: - logger.info(f"➕ New playlist '{title}', adding to database...") + logger.info(f"New playlist '{title}', adding to database...") # Insert new playlist cursor.execute(""" @@ -218,7 +218,7 @@ class ListenBrainzManager: """ Cache tracks for a playlist, including fetching cover art URLs in parallel """ - logger.info(f"🎵 Caching {len(tracks)} tracks with cover art...") + logger.info(f"Caching {len(tracks)} tracks with cover art...") # First pass: extract track data track_data_list = [] @@ -324,7 +324,7 @@ class ListenBrainzManager: logger.debug(f"Error fetching cover for track {idx}: {e}") covers_found = sum(1 for t in track_data_list if t.get('album_cover_url')) - logger.info(f"✅ Fetched {covers_found}/{len(track_data_list)} cover art URLs") + logger.info(f"Fetched {covers_found}/{len(track_data_list)} cover art URLs") def _cleanup_old_playlists(self): """Remove old playlists, keeping only the 25 most recent per type""" @@ -354,7 +354,7 @@ class ListenBrainzManager: # Delete old playlists cursor.execute(f"DELETE FROM listenbrainz_playlists WHERE id IN ({placeholders})", old_playlist_ids) - logger.info(f"🗑️ Removed {len(old_playlist_ids)} old {playlist_type} playlists") + logger.info(f"Removed {len(old_playlist_ids)} old {playlist_type} playlists") except Exception as e: logger.error(f"Error cleaning up {playlist_type} playlists: {e}") diff --git a/core/lyrics_client.py b/core/lyrics_client.py index 052e2329..e7efebd8 100644 --- a/core/lyrics_client.py +++ b/core/lyrics_client.py @@ -113,14 +113,14 @@ class LyricsClient: f.write(synced) # Embed synced lyrics in audio tags self._embed_lyrics(audio_file_path, synced) - logger.info(f"✅ Created synced LRC + embedded: {os.path.basename(lrc_path)}") + logger.info(f"Created synced LRC + embedded: {os.path.basename(lrc_path)}") else: # Plain lyrics only → write as .txt (not .lrc, which requires timestamps) with open(txt_path, 'w', encoding='utf-8') as f: f.write(plain) # Still embed plain lyrics in audio tags (players can display unsynced lyrics) self._embed_lyrics(audio_file_path, plain) - logger.info(f"✅ Created plain lyrics .txt + embedded: {os.path.basename(txt_path)}") + logger.info(f"Created plain lyrics .txt + embedded: {os.path.basename(txt_path)}") return True except Exception as e: diff --git a/core/matching_engine.py b/core/matching_engine.py index 9fd4f65f..83ed1c09 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}')") + print(f"Heuristic album detection: '{original_title}' → '{cleaned_title}' (removed: '{potential_album_part}')") return cleaned_title, True return track_title, False @@ -750,7 +750,7 @@ class MusicMatchingEngine: f"vs '{slskd_track.filename[:60]}...' | " f"Title: {title_score:.2f} (ratio: {title_ratio:.2f}, boundary: {has_word_boundary}), " f"Artist: {artist_score:.2f}, Duration: {duration_score:.2f}{album_tag}, " - f"Final: {final_confidence:.2f} {'✅ PASS' if final_confidence > 0.63 else '❌ FAIL'}" + f"Final: {final_confidence:.2f} {'PASS' if final_confidence > 0.63 else 'FAIL'}" ) # Ensure the final score doesn't exceed 1.0 @@ -982,11 +982,11 @@ 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") + print(f"DEBUG: 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]}...") elif confident_results: - print(f"✅ DEBUG: {len(confident_results)} results passed confidence threshold 0.58") + print(f"DEBUG: {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]}...") diff --git a/core/media_scan_manager.py b/core/media_scan_manager.py index 2b2e674d..28f083b3 100644 --- a/core/media_scan_manager.py +++ b/core/media_scan_manager.py @@ -122,15 +122,15 @@ class MediaScanManager: if self._scan_in_progress: # Server is currently scanning - mark that we need another scan later self._downloads_during_scan = True - logger.info(f"📡 Media scan in progress - queueing follow-up scan ({reason})") + logger.info(f"Media scan in progress - queueing follow-up scan ({reason})") return # Cancel any existing timer and start a new one if self._timer: self._timer.cancel() - logger.debug(f"⏳ Resetting scan timer ({reason})") + logger.debug(f"Resetting scan timer ({reason})") else: - logger.info(f"⏳ Media scan queued - will execute in {self.delay}s ({reason})") + logger.info(f"Media scan queued - will execute in {self.delay}s ({reason})") # Start the debounce timer self._timer = threading.Timer(self.delay, self._execute_scan) @@ -176,21 +176,21 @@ class MediaScanManager: # Get the active media client media_client, server_type = self._get_active_media_client() if not media_client: - logger.error("❌ No active media client available for library scan") + logger.error("No active media client available for library scan") self._reset_scan_state() return - logger.info(f"🎵 Starting {server_type.upper()} library scan...") + logger.info(f"Starting {server_type.upper()} library scan...") try: success = media_client.trigger_library_scan() if success: - logger.info(f"✅ {server_type.upper()} library scan initiated successfully") + logger.info(f"{server_type.upper()} library scan initiated successfully") # Start new periodic update system instead of completion detection self._start_periodic_updates() else: - logger.error(f"❌ Failed to initiate {server_type.upper()} library scan") + logger.error(f"Failed to initiate {server_type.upper()} library scan") self._reset_scan_state() except Exception as e: @@ -207,7 +207,7 @@ class MediaScanManager: self._is_doing_periodic_updates = True - logger.info(f"🕒 Starting periodic database updates - will check/update every {self._periodic_update_interval//60} minutes") + logger.info(f"Starting periodic database updates - will check/update every {self._periodic_update_interval//60} minutes") # Schedule first periodic update after 5 minutes self._periodic_update_timer = threading.Timer(self._periodic_update_interval, self._do_periodic_update) @@ -242,20 +242,20 @@ class MediaScanManager: is_scanning = media_client.is_library_scanning("Music") elapsed_time = time.time() - self._scan_start_time if self._scan_start_time else 0 - logger.info(f"🕒 PERIODIC UPDATE: After {elapsed_time//60:.0f} minutes - {server_type.upper()} scanning: {is_scanning}") + logger.info(f"PERIODIC UPDATE: After {elapsed_time//60:.0f} minutes - {server_type.upper()} scanning: {is_scanning}") if is_scanning: # Still scanning - trigger database update and continue periodic updates - logger.info(f"🔄 {server_type.upper()} still scanning - triggering database update") + logger.info(f"{server_type.upper()} still scanning - triggering database update") self._call_completion_callbacks() # Schedule next periodic update - logger.info(f"🕒 Scheduling next periodic update in {self._periodic_update_interval//60} minutes") + logger.info(f"Scheduling next periodic update in {self._periodic_update_interval//60} minutes") self._periodic_update_timer = threading.Timer(self._periodic_update_interval, self._do_periodic_update) self._periodic_update_timer.start() else: # Scanning stopped - final update and cleanup - logger.info(f"✅ {server_type.upper()} scanning completed - doing final database update") + logger.info(f"{server_type.upper()} scanning completed - doing final database update") self._call_completion_callbacks() self._stop_periodic_updates() @@ -273,7 +273,7 @@ class MediaScanManager: self._periodic_update_timer.cancel() self._periodic_update_timer = None - logger.info("🕒 Stopped periodic database updates") + logger.info("Stopped periodic database updates") self._scan_completed() except Exception as e: @@ -292,17 +292,17 @@ class MediaScanManager: logger.debug("Scan completion callback called but scan was not in progress") return - logger.info("📡 Media library scan completed") + logger.info("Media library scan completed") # Call registered completion callbacks self._call_completion_callbacks() # Check if we need a follow-up scan if downloads_during_scan: - logger.info("🔄 Downloads occurred during scan - triggering follow-up scan") + logger.info("Downloads occurred during scan - triggering follow-up scan") self.request_scan("Follow-up scan for downloads during previous scan") else: - logger.info("✅ No downloads during scan - scan cycle complete") + logger.info("No downloads during scan - scan cycle complete") def _call_completion_callbacks(self): """Call all registered scan completion callbacks""" @@ -344,7 +344,7 @@ class MediaScanManager: logger.warning("Force scan requested but scan already in progress") return - logger.info("🚀 Force scan requested - executing immediately") + logger.info("Force scan requested - executing immediately") self._execute_scan() def get_status(self) -> dict: diff --git a/core/metadata_service.py b/core/metadata_service.py index 829fd210..29498687 100644 --- a/core/metadata_service.py +++ b/core/metadata_service.py @@ -212,8 +212,8 @@ class MetadataService: def _log_initialization(self): """Log initialization status""" - spotify_status = "✅ Authenticated" if self.spotify.is_spotify_authenticated() else "❌ Not authenticated" - fallback_status = "✅ Available" if self.itunes.is_authenticated() else "❌ Not available" + spotify_status = "Authenticated" if self.spotify.is_spotify_authenticated() else "Not authenticated" + fallback_status = "Available" if self.itunes.is_authenticated() else "Not available" logger.info(f"MetadataService initialized - Spotify: {spotify_status}, {self._fallback_source.capitalize()}: {fallback_status}") logger.info(f"Preferred provider: {self.preferred_provider}") diff --git a/core/musicbrainz_worker.py b/core/musicbrainz_worker.py index 09a0e4a5..ccdd7108 100644 --- a/core/musicbrainz_worker.py +++ b/core/musicbrainz_worker.py @@ -288,11 +288,11 @@ class MusicBrainzWorker: if result and result.get('mbid'): self.mb_service.update_artist_mbid(item_id, result['mbid'], 'matched') self.stats['matched'] += 1 - logger.info(f"✅ Matched artist '{item_name}' → MBID: {result['mbid']}") + logger.info(f"Matched artist '{item_name}' → MBID: {result['mbid']}") else: self.mb_service.update_artist_mbid(item_id, None, 'not_found') self.stats['not_found'] += 1 - logger.debug(f"❌ No match for artist '{item_name}'") + logger.debug(f"No match for artist '{item_name}'") elif item_type == 'album': artist_name = item.get('artist') @@ -300,11 +300,11 @@ class MusicBrainzWorker: if result and result.get('mbid'): self.mb_service.update_album_mbid(item_id, result['mbid'], 'matched') self.stats['matched'] += 1 - logger.info(f"✅ Matched album '{item_name}' → MBID: {result['mbid']}") + logger.info(f"Matched album '{item_name}' → MBID: {result['mbid']}") else: self.mb_service.update_album_mbid(item_id, None, 'not_found') self.stats['not_found'] += 1 - logger.debug(f"❌ No match for album '{item_name}'") + logger.debug(f"No match for album '{item_name}'") elif item_type == 'track': artist_name = item.get('artist') @@ -312,11 +312,11 @@ class MusicBrainzWorker: if result and result.get('mbid'): self.mb_service.update_track_mbid(item_id, result['mbid'], 'matched') self.stats['matched'] += 1 - logger.info(f"✅ Matched track '{item_name}' → MBID: {result['mbid']}") + logger.info(f"Matched track '{item_name}' → MBID: {result['mbid']}") else: self.mb_service.update_track_mbid(item_id, None, 'not_found') self.stats['not_found'] += 1 - logger.debug(f"❌ No match for track '{item_name}'") + logger.debug(f"No match for track '{item_name}'") except Exception as e: logger.error(f"Error processing {item['type']} #{item['id']}: {e}") diff --git a/core/navidrome_client.py b/core/navidrome_client.py index 81b49f67..e3cf036c 100644 --- a/core/navidrome_client.py +++ b/core/navidrome_client.py @@ -173,7 +173,7 @@ class NavidromeClient: self._artist_cache.clear() self._album_cache.clear() self._track_cache.clear() - logger.info("🔄 Navidrome client config reset — will reconnect with new settings") + logger.info("Navidrome client config reset — will reconnect with new settings") def get_music_folders(self) -> list: """Get available music folders from Navidrome.""" @@ -833,7 +833,7 @@ class NavidromeClient: response = self._make_request('createPlaylist', params) if response and response.get('status') == 'ok': - logger.info(f"✅ {'Updated' if playlist_id else 'Created'} Navidrome playlist '{name}' with {len(track_ids)} tracks") + logger.info(f"{'Updated' if playlist_id else 'Created'} Navidrome playlist '{name}' with {len(track_ids)} tracks") return True else: logger.error(f"Failed to {'update' if playlist_id else 'create'} Navidrome playlist '{name}'") @@ -877,7 +877,7 @@ class NavidromeClient: try: success = self.create_playlist(target_name, source_tracks) if success: - logger.info(f"✅ Created backup playlist '{target_name}' with {len(source_tracks)} tracks") + logger.info(f"Created backup playlist '{target_name}' with {len(source_tracks)} tracks") return True else: logger.error(f"Failed to create backup playlist '{target_name}'") @@ -938,13 +938,13 @@ class NavidromeClient: # If we have existing playlists and want to backup, use the first one found if existing_playlists and create_backup: backup_name = f"{playlist_name} Backup" - logger.info(f"🛡️ Creating backup playlist '{backup_name}' before sync") + logger.info(f"Creating backup playlist '{backup_name}' before sync") # We only need to backup once, even if duplicates exist if self.copy_playlist(playlist_name, backup_name): - logger.info(f"✅ Backup created successfully") + logger.info(f"Backup created successfully") else: - logger.warning(f"⚠️ Failed to create backup, continuing with sync") + logger.warning(f"Failed to create backup, continuing with sync") # STRATEGY: Update the first match, delete the rest if existing_playlists: diff --git a/core/plex_client.py b/core/plex_client.py index b31d2ae8..0fdea25f 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -427,7 +427,7 @@ class PlexClient: # Create new playlist with copied tracks try: self.server.createPlaylist(target_name, items=valid_tracks) - logger.info(f"✅ Created backup playlist '{target_name}' with {len(valid_tracks)} tracks") + logger.info(f"Created backup playlist '{target_name}' with {len(valid_tracks)} tracks") return True except Exception as create_error: logger.error(f"Failed to create backup playlist: {create_error}") @@ -435,7 +435,7 @@ class PlexClient: try: new_playlist = self.server.createPlaylist(target_name) new_playlist.addItems(valid_tracks) - logger.info(f"✅ Created backup playlist '{target_name}' with {len(valid_tracks)} tracks (alternative method)") + logger.info(f"Created backup playlist '{target_name}' with {len(valid_tracks)} tracks (alternative method)") return True except Exception as alt_error: logger.error(f"Alternative backup creation also failed: {alt_error}") @@ -461,12 +461,12 @@ class PlexClient: if create_backup: backup_name = f"{playlist_name} Backup" - logger.info(f"🛡️ Creating backup playlist '{backup_name}' before sync") + logger.info(f"Creating backup playlist '{backup_name}' before sync") if self.copy_playlist(playlist_name, backup_name): - logger.info(f"✅ Backup created successfully") + logger.info(f"Backup created successfully") else: - logger.warning(f"⚠️ Failed to create backup, continuing with sync") + logger.warning(f"Failed to create backup, continuing with sync") # Delete original and recreate existing_playlist.delete() @@ -931,7 +931,7 @@ class PlexClient: try: library = self.server.library.section(library_name) library.update() # Non-blocking scan request - logger.info(f"🎵 Triggered Plex library scan for '{library_name}'") + logger.info(f"Triggered Plex library scan for '{library_name}'") return True except Exception as e: logger.error(f"Failed to trigger library scan for '{library_name}': {e}") @@ -940,7 +940,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(f"DEBUG: Not connected to Plex, cannot check scan status") return False try: @@ -949,31 +949,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(f"DEBUG: Library.refreshing = {refreshing}") if refreshing: - logger.debug(f"🔍 DEBUG: Library is refreshing") + logger.debug(f"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(f"DEBUG: Found {len(activities)} server 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(f"DEBUG: Activity - type: {activity_type}, title: {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(f"DEBUG: Found matching scan activity: {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(f"DEBUG: No scan activity detected") return False except Exception as e: diff --git a/core/plex_scan_manager.py b/core/plex_scan_manager.py index 32d1b4a2..8c694284 100644 --- a/core/plex_scan_manager.py +++ b/core/plex_scan_manager.py @@ -54,15 +54,15 @@ class PlexScanManager: if self._scan_in_progress: # Plex is currently scanning - mark that we need another scan later self._downloads_during_scan = True - logger.info(f"📡 Plex scan in progress - queueing follow-up scan ({reason})") + logger.info(f"Plex scan in progress - queueing follow-up scan ({reason})") return # Cancel any existing timer and start a new one if self._timer: self._timer.cancel() - logger.debug(f"⏳ Resetting scan timer ({reason})") + logger.debug(f"Resetting scan timer ({reason})") else: - logger.info(f"⏳ Plex scan queued - will execute in {self.delay}s ({reason})") + logger.info(f"Plex scan queued - will execute in {self.delay}s ({reason})") # Start the debounce timer self._timer = threading.Timer(self.delay, self._execute_scan) @@ -105,17 +105,17 @@ class PlexScanManager: self._timer = None self._scan_start_time = time.time() - logger.info("🎵 Starting Plex library scan...") + logger.info("Starting Plex library scan...") try: success = self.plex_client.trigger_library_scan() if success: - logger.info("✅ Plex library scan initiated successfully") + logger.info("Plex library scan initiated successfully") # Start new periodic update system instead of completion detection self._start_periodic_updates() else: - logger.error("❌ Failed to initiate Plex library scan") + logger.error("Failed to initiate Plex library scan") self._reset_scan_state() except Exception as e: @@ -132,7 +132,7 @@ class PlexScanManager: self._is_doing_periodic_updates = True - logger.info(f"🕒 Starting periodic database updates - will check/update every {self._periodic_update_interval//60} minutes") + logger.info(f"Starting periodic database updates - will check/update every {self._periodic_update_interval//60} minutes") # Schedule first periodic update after 5 minutes self._periodic_update_timer = threading.Timer(self._periodic_update_interval, self._do_periodic_update) @@ -160,20 +160,20 @@ class PlexScanManager: is_scanning = self.plex_client.is_library_scanning("Music") elapsed_time = time.time() - self._scan_start_time if self._scan_start_time else 0 - logger.info(f"🕒 PERIODIC UPDATE: After {elapsed_time//60:.0f} minutes - Plex scanning: {is_scanning}") + logger.info(f"PERIODIC UPDATE: After {elapsed_time//60:.0f} minutes - Plex scanning: {is_scanning}") if is_scanning: # Still scanning - trigger database update and continue periodic updates - logger.info("🔄 Plex still scanning - triggering database update") + logger.info("Plex still scanning - triggering database update") self._call_completion_callbacks() # Schedule next periodic update - logger.info(f"🕒 Scheduling next periodic update in {self._periodic_update_interval//60} minutes") + logger.info(f"Scheduling next periodic update in {self._periodic_update_interval//60} minutes") self._periodic_update_timer = threading.Timer(self._periodic_update_interval, self._do_periodic_update) self._periodic_update_timer.start() else: # Scanning stopped - final update and cleanup - logger.info("✅ Plex scanning completed - doing final database update") + logger.info("Plex scanning completed - doing final database update") self._call_completion_callbacks() self._stop_periodic_updates() @@ -191,7 +191,7 @@ class PlexScanManager: self._periodic_update_timer.cancel() self._periodic_update_timer = None - logger.info("🕒 Stopped periodic database updates") + logger.info("Stopped periodic database updates") self._scan_completed() except Exception as e: @@ -222,7 +222,7 @@ class PlexScanManager: else: # Scan completed! elapsed_time = time.time() - self._scan_start_time if self._scan_start_time else 0 - logger.info(f"🎵 Plex library scan detected as completed (took {elapsed_time:.1f} seconds)") + logger.info(f"Plex library scan detected as completed (took {elapsed_time:.1f} seconds)") self._scan_completed() except Exception as e: @@ -243,17 +243,17 @@ class PlexScanManager: logger.debug("Scan completion callback called but scan was not in progress") return - logger.info("📡 Plex library scan completed") + logger.info("Plex library scan completed") # Call registered completion callbacks self._call_completion_callbacks() # Check if we need a follow-up scan if downloads_during_scan: - logger.info("🔄 Downloads occurred during scan - triggering follow-up scan") + logger.info("Downloads occurred during scan - triggering follow-up scan") self.request_scan("Follow-up scan for downloads during previous scan") else: - logger.info("✅ No downloads during scan - scan cycle complete") + logger.info("No downloads during scan - scan cycle complete") def _call_completion_callbacks(self): """Call all registered scan completion callbacks""" @@ -295,7 +295,7 @@ class PlexScanManager: logger.warning("Force scan requested but scan already in progress") return - logger.info("🚀 Force scan requested - executing immediately") + logger.info("Force scan requested - executing immediately") self._execute_scan() def get_status(self) -> dict: diff --git a/core/seasonal_discovery.py b/core/seasonal_discovery.py index 67859607..6431218c 100644 --- a/core/seasonal_discovery.py +++ b/core/seasonal_discovery.py @@ -28,7 +28,7 @@ SEASONAL_CONFIG = { "keywords": ["christmas", "xmas", "holiday", "santa", "jingle", "winter wonderland", "sleigh", "noel", "carol"], "active_months": [11, 12], # November-December "playlist_size": 50, - "icon": "🎄" + "icon": "🎅" }, "valentines": { "name": "Love Songs", @@ -36,7 +36,7 @@ SEASONAL_CONFIG = { "keywords": ["love", "valentine", "romance", "heart", "romantic", "darling"], "active_months": [2], # February "playlist_size": 50, - "icon": "❤️" + "icon": "💝" }, "summer": { "name": "Summer Vibes", diff --git a/core/soulseek_client.py b/core/soulseek_client.py index c548140c..91d2c619 100644 --- a/core/soulseek_client.py +++ b/core/soulseek_client.py @@ -1029,10 +1029,10 @@ class SoulseekClient: logger.debug(f"{action} download (attempt {i+1}/3) with endpoint: {endpoint}") response = await self._make_request('DELETE', endpoint) if response is not None: - logger.info(f"✅ Successfully cancelled download using endpoint format {i+1}") + logger.info(f"Successfully cancelled download using endpoint format {i+1}") return True else: - logger.debug(f"❌ Endpoint format {i+1} failed: {endpoint}") + logger.debug(f"Endpoint format {i+1} failed: {endpoint}") # Fallback: if download_id looks like a filename (contains path separators), # list all transfers, find by filename, and cancel with the real transfer ID @@ -1049,12 +1049,12 @@ class SoulseekClient: logger.debug(f"Found matching transfer with real ID, trying: {fallback_endpoint}") response = await self._make_request('DELETE', fallback_endpoint) if response is not None: - logger.info(f"✅ Successfully cancelled download via filename fallback") + logger.info(f"Successfully cancelled download via filename fallback") return True except Exception as fallback_error: logger.debug(f"Filename fallback failed: {fallback_error}") - logger.error(f"❌ All cancel endpoint formats failed for download_id: {download_id}") + logger.error(f"All cancel endpoint formats failed for download_id: {download_id}") return False except Exception as e: @@ -1725,7 +1725,7 @@ class SoulseekClient: async with session.get(swagger_url, headers=headers) as response: if response.status == 200: swagger_data = await response.json() - logger.info("✓ Found Swagger documentation") + logger.info("Found Swagger documentation") # Look for download/transfer related endpoints paths = swagger_data.get('paths', {}) diff --git a/core/spotify_client.py b/core/spotify_client.py index bea544c5..ffc71209 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -99,7 +99,7 @@ def _set_global_rate_limit(retry_after_seconds, endpoint_name, has_real_header=F _rate_limit_endpoint = endpoint_name _rate_limit_set_at = now logger.warning( - f"⚠️ GLOBAL RATE LIMIT ACTIVATED: {retry_after_seconds}s ban " + f"GLOBAL RATE LIMIT ACTIVATED: {retry_after_seconds}s ban " f"(expires {time.strftime('%H:%M:%S', time.localtime(new_until))}) " f"triggered by {endpoint_name}" ) @@ -227,7 +227,7 @@ def _detect_and_set_rate_limit(exception, endpoint_name="unknown"): logger.info(f"Rate limit detected on {endpoint_name} — Retry-After header: {delay}s") except (ValueError, TypeError): delay = _BASE_UNKNOWN_BAN - logger.warning(f"⚠️ Rate limit detected on {endpoint_name} — unparseable Retry-After: {retry_after}") + logger.warning(f"Rate limit detected on {endpoint_name} — unparseable Retry-After: {retry_after}") else: # No Retry-After header available if "max retries" in error_str.lower(): @@ -237,7 +237,7 @@ def _detect_and_set_rate_limit(exception, endpoint_name="unknown"): delay = _BASE_MAX_RETRIES_BAN # 4 hours else: delay = _BASE_UNKNOWN_BAN # 30 min - logger.warning(f"⚠️ Rate limit detected on {endpoint_name} — no Retry-After header, using {delay}s default") + logger.warning(f"Rate limit detected on {endpoint_name} — no Retry-After header, using {delay}s default") _set_global_rate_limit(delay, endpoint_name, has_real_header=has_real_header) return True @@ -312,7 +312,7 @@ def rate_limited(func): delay = 3.0 * (2 ** attempt) # 3, 6, 12, 24, 48 if attempt < max_retries: - logger.warning(f"⚠️ Spotify rate limit hit, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries}): {func.__name__}") + logger.warning(f"Spotify rate limit hit, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries}): {func.__name__}") time.sleep(delay) continue else: @@ -323,7 +323,7 @@ def rate_limited(func): elif is_server_error and attempt < max_retries: delay = 2.0 * (2 ** attempt) # 2, 4, 8, 16, 32 - logger.warning(f"⚠️ Spotify server error, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries}): {func.__name__}") + logger.warning(f"Spotify server error, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries}): {func.__name__}") time.sleep(delay) continue @@ -534,7 +534,7 @@ class SpotifyClient: config = config_manager.get_spotify_config() if not config.get('client_id') or not config.get('client_secret'): - logger.warning("⚠️ Spotify credentials not configured") + logger.warning("Spotify credentials not configured") return try: @@ -630,7 +630,7 @@ class SpotifyClient: # Minimum 30 min for auth probe 429s — these indicate persistent throttling ban_duration = max(delay, _BASE_UNKNOWN_BAN) _set_global_rate_limit(ban_duration, 'is_spotify_authenticated', has_real_header=has_real_header) - logger.warning(f"⚠️ Auth probe rate limited — activating {ban_duration}s global ban") + logger.warning(f"Auth probe rate limited — activating {ban_duration}s global ban") result = True else: logger.debug(f"Spotify authentication check failed: {e}") @@ -656,7 +656,7 @@ class SpotifyClient: os.remove(cache_path) logger.info("Deleted Spotify cache file") except Exception as e: - logger.warning(f"⚠️ Failed to delete Spotify cache: {e}") + logger.warning(f"Failed to delete Spotify cache: {e}") logger.info("Spotify client disconnected") @@ -1075,7 +1075,7 @@ class SpotifyClient: return artists except Exception as e: if '403' in str(e) or 'Forbidden' in str(e): - logger.warning("⚠️ Spotify user-follow-read scope not granted — re-authorize to see followed artists") + logger.warning("Spotify user-follow-read scope not granted — re-authorize to see followed artists") return [] _detect_and_set_rate_limit(e, 'get_followed_artists') logger.error(f"Error fetching followed artists: {e}") diff --git a/core/tidal_client.py b/core/tidal_client.py index 6185e197..22c3507c 100644 --- a/core/tidal_client.py +++ b/core/tidal_client.py @@ -472,9 +472,9 @@ class TidalClient: result = self._exchange_code_for_tokens() if result: - logger.info("✅ Token exchange successful") + logger.info("Token exchange successful") else: - logger.error("❌ Token exchange failed") + logger.error("Token exchange failed") return result except Exception as e: diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index e69157c5..a841a71d 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -128,7 +128,7 @@ def clean_track_name_for_search(track_name): # Log cleaning if significant changes were made if cleaned_name != track_name: - logger.debug(f"🧹 Intelligent track cleaning: '{track_name}' -> '{cleaned_name}'") + logger.debug(f"Intelligent track cleaning: '{track_name}' -> '{cleaned_name}'") return cleaned_name @@ -406,7 +406,7 @@ class WatchlistScanner: def _disable_spotify_for_run(self, reason: str): """Disable Spotify for rest of current run, once.""" if not self._spotify_disabled_for_run: - logger.warning(f"⚠️ Spotify disabled for rest of run: {reason}") + logger.warning(f"Spotify disabled for rest of run: {reason}") self._spotify_disabled_for_run = True self._spotify_disabled_reason = reason @@ -640,7 +640,7 @@ class WatchlistScanner: try: self._backfill_missing_ids(all_watchlist_artists, provider) except Exception as backfill_error: - logger.warning(f"⚠️ Error during {provider} ID backfilling: {backfill_error}") + logger.warning(f"Error during {provider} ID backfilling: {backfill_error}") # Continue with scan even if backfilling fails scan_results = [] @@ -655,9 +655,9 @@ class WatchlistScanner: self._disable_spotify_for_run("global Spotify rate limit active") if result.success: - logger.info(f"✅ Scanned {artist.artist_name}: {result.new_tracks_found} new tracks found") + logger.info(f"Scanned {artist.artist_name}: {result.new_tracks_found} new tracks found") else: - logger.warning(f"❌ Failed to scan {artist.artist_name}: {result.error_message}") + logger.warning(f"Failed to scan {artist.artist_name}: {result.error_message}") # Rate limiting: Add delay between artists to avoid hitting Spotify API limits # This is critical to prevent getting banned for 6+ hours @@ -701,7 +701,7 @@ class WatchlistScanner: self._disable_spotify_for_run("global Spotify rate limit active") self.sync_spotify_library_cache() except Exception as lib_err: - logger.warning(f"⚠️ Error syncing Spotify library cache: {lib_err}") + logger.warning(f"Error syncing Spotify library cache: {lib_err}") return scan_results @@ -1054,10 +1054,10 @@ class WatchlistScanner: artists_to_match = [a for a in artists if not getattr(a, id_attr, None)] if not artists_to_match: - logger.info(f"✅ All artists already have {provider} IDs") + logger.info(f"All artists already have {provider} IDs") return - logger.info(f"🔄 Backfilling {len(artists_to_match)} artists with {provider} IDs...") + logger.info(f"Backfilling {len(artists_to_match)} artists with {provider} IDs...") match_fn = { 'spotify': self._match_to_spotify, @@ -1086,7 +1086,7 @@ class WatchlistScanner: update_fn(artist.id, new_id) setattr(artist, id_attr, new_id) matched_count += 1 - logger.info(f"✅ Matched '{artist.artist_name}' to {provider}: {new_id}") + logger.info(f"Matched '{artist.artist_name}' to {provider}: {new_id}") else: unmatched_names.append(artist.artist_name) @@ -1097,9 +1097,9 @@ class WatchlistScanner: unmatched_names.append(artist.artist_name) continue - logger.info(f"✅ Backfilled {matched_count}/{len(artists_to_match)} artists with {provider} IDs") + logger.info(f"Backfilled {matched_count}/{len(artists_to_match)} artists with {provider} IDs") if unmatched_names: - logger.warning(f"⚠️ Could not confidently match {len(unmatched_names)} artists: {', '.join(unmatched_names[:10])}" + logger.warning(f"Could not confidently match {len(unmatched_names)} artists: {', '.join(unmatched_names[:10])}" f"{'...' if len(unmatched_names) > 10 else ''} — use Watchlist Settings to link manually") @staticmethod @@ -1511,11 +1511,11 @@ class WatchlistScanner: db_track, confidence = self.database.check_track_exists(query_title, artist_name, confidence_threshold=0.7, server_source=active_server, album=album_name) if db_track and confidence >= 0.7: - logger.debug(f"✔️ Track found in library: '{original_title}' by '{artist_name}' (confidence: {confidence:.2f})") + logger.debug(f"Track found in library: '{original_title}' by '{artist_name}' (confidence: {confidence:.2f})") return False # Track exists in library # No match found with any variation or artist - logger.info(f"❌ Track missing from library: '{original_title}' by '{artists_to_search[0] if artists_to_search else 'Unknown'}' - adding to wishlist") + logger.info(f"Track missing from library: '{original_title}' by '{artists_to_search[0] if artists_to_search else 'Unknown'}' - adding to wishlist") return True # Track is missing except Exception as e: @@ -3212,7 +3212,7 @@ class WatchlistScanner: logger.debug("Spotify not authenticated, skipping library cache sync") return - logger.info("📚 Syncing Spotify library cache...") + logger.info("Syncing Spotify library cache...") try: last_sync = self.database.get_metadata('spotify_library_last_sync') @@ -3256,7 +3256,7 @@ class WatchlistScanner: # Update last sync timestamp self.database.set_metadata('spotify_library_last_sync', datetime.now().isoformat()) - logger.info(f"✅ Spotify library cache sync complete — {len(albums)} albums processed") + logger.info(f"Spotify library cache sync complete — {len(albums)} albums processed") except Exception as e: logger.error(f"Error syncing Spotify library cache: {e}") diff --git a/core/web_scan_manager.py b/core/web_scan_manager.py index 55eb17cf..343d36b4 100644 --- a/core/web_scan_manager.py +++ b/core/web_scan_manager.py @@ -91,7 +91,7 @@ class WebScanManager: if self._scan_in_progress: # Server is currently scanning - mark that we need another scan later self._downloads_during_scan = True - logger.info(f"📡 Web scan in progress - queueing follow-up scan ({reason})") + logger.info(f"Web scan in progress - queueing follow-up scan ({reason})") return { "status": "queued", "message": "Scan already in progress, queued for later", @@ -101,9 +101,9 @@ class WebScanManager: # Cancel any existing timer and start a new one if self._timer: self._timer.cancel() - logger.debug(f"⏳ Resetting web scan timer ({reason})") + logger.debug(f"Resetting web scan timer ({reason})") else: - logger.info(f"⏳ Web scan queued - will execute in {self.delay}s ({reason})") + logger.info(f"Web scan queued - will execute in {self.delay}s ({reason})") # Start the debounce timer self._timer = threading.Timer(self.delay, self._execute_scan) @@ -182,12 +182,12 @@ class WebScanManager: # Get the active media client media_client, server_type = self._get_active_media_client() if not media_client: - logger.error("❌ No active media client available for web library scan") + logger.error("No active media client available for web library scan") self._reset_scan_state() return self._current_server_type = server_type - logger.info(f"🎵 Starting {server_type.upper()} library scan via web interface...") + logger.info(f"Starting {server_type.upper()} library scan via web interface...") try: # Update progress @@ -200,7 +200,7 @@ class WebScanManager: success = media_client.trigger_library_scan() if success: - logger.info(f"✅ {server_type.upper()} library scan initiated successfully via web") + logger.info(f"{server_type.upper()} library scan initiated successfully via web") with self._lock: self._scan_progress = { "status": "active", @@ -210,7 +210,7 @@ class WebScanManager: # Start periodic completion checking self._start_periodic_completion_check() else: - logger.error(f"❌ Failed to initiate {server_type.upper()} library scan via web") + logger.error(f"Failed to initiate {server_type.upper()} library scan via web") with self._lock: self._scan_progress = { "status": "failed", @@ -219,7 +219,7 @@ class WebScanManager: self._reset_scan_state() except Exception as e: - logger.error(f"❌ Error during {server_type.upper()} library scan via web: {e}") + logger.error(f"Error during {server_type.upper()} library scan via web: {e}") with self._lock: self._scan_progress = { "status": "error", @@ -265,7 +265,7 @@ class WebScanManager: def _handle_scan_completion(self): """Handle scan completion and trigger callbacks""" - logger.info(f"🏁 Web {self._current_server_type.upper()} library scan completed") + logger.info(f"Web {self._current_server_type.upper()} library scan completed") # Call completion callbacks callbacks_to_call = [] @@ -274,7 +274,7 @@ class WebScanManager: for callback in callbacks_to_call: try: - logger.info(f"🔄 Calling web scan completion callback: {callback.__name__}") + logger.info(f"Calling web scan completion callback: {callback.__name__}") callback() except Exception as e: logger.error(f"Error in web scan completion callback {callback.__name__}: {e}") @@ -285,7 +285,7 @@ class WebScanManager: # Check if we need another scan due to downloads during this scan with self._lock: if self._downloads_during_scan: - logger.info("🔄 Web scan follow-up needed for downloads during scan") + logger.info("Web scan follow-up needed for downloads during scan") self.request_scan("Follow-up scan for downloads during previous scan") def _reset_scan_state(self): diff --git a/core/youtube_client.py b/core/youtube_client.py index 0f0e6fe0..83f745e5 100644 --- a/core/youtube_client.py +++ b/core/youtube_client.py @@ -107,7 +107,7 @@ class YouTubeClient: self.download_path = Path(download_path) self.download_path.mkdir(parents=True, exist_ok=True) - logger.info(f"📁 YouTube client using download path: {self.download_path}") + logger.info(f"YouTube client using download path: {self.download_path}") # Callback for shutdown check (avoids circular imports) self.shutdown_check = None @@ -123,11 +123,11 @@ class YouTubeClient: # Initialize production matching engine for parity with Soulseek self.matching_engine = MusicMatchingEngine() - logger.info("✅ Initialized production MusicMatchingEngine") + logger.info("Initialized production MusicMatchingEngine") # Check for ffmpeg (REQUIRED for MP3 conversion) if not self._check_ffmpeg(): - logger.error("❌ ffmpeg is required but not found") + logger.error("ffmpeg is required but not found") logger.error("The client will attempt to auto-download ffmpeg on first use") # Download queue management (mirrors Soulseek's download tracking) @@ -201,7 +201,7 @@ class YouTubeClient: self.download_opts['cookiesfrombrowser'] = (cookies_browser,) elif 'cookiesfrombrowser' in self.download_opts: del self.download_opts['cookiesfrombrowser'] - logger.info(f"🔄 YouTube settings reloaded (delay={self._download_delay}s, cookies={'enabled' if cookies_browser else 'disabled'})") + logger.info(f"YouTube settings reloaded (delay={self._download_delay}s, cookies={'enabled' if cookies_browser else 'disabled'})") async def check_connection(self) -> bool: """ @@ -356,7 +356,7 @@ class YouTubeClient: # Check if ffmpeg is in system PATH if shutil.which('ffmpeg'): - logger.info("✅ Found ffmpeg in system PATH") + logger.info("Found ffmpeg in system PATH") return True # Auto-download ffmpeg to tools folder if not found @@ -373,7 +373,7 @@ class YouTubeClient: # If we already have both locally, use them if ffmpeg_path.exists() and ffprobe_path.exists(): - logger.info(f"✅ Found ffmpeg and ffprobe in tools folder") + logger.info(f"Found ffmpeg and ffprobe in tools folder") # Add to PATH so yt-dlp can find them tools_dir_str = str(tools_dir.absolute()) os.environ['PATH'] = tools_dir_str + os.pathsep + os.environ.get('PATH', '') @@ -451,10 +451,10 @@ class YouTubeClient: ffprobe_zip.unlink() # Clean up zip else: - logger.error(f"❌ Unsupported platform: {system}") + logger.error(f"Unsupported platform: {system}") return False - logger.info(f"✅ Downloaded ffmpeg to: {ffmpeg_path}") + logger.info(f"Downloaded ffmpeg to: {ffmpeg_path}") # Add to PATH tools_dir_str = str(tools_dir.absolute()) @@ -463,7 +463,7 @@ class YouTubeClient: return True except Exception as e: - logger.error(f"❌ Failed to download ffmpeg: {e}") + logger.error(f"Failed to download ffmpeg: {e}") logger.error(f" Please install manually:") logger.error(f" Windows: scoop install ffmpeg") logger.error(f" Linux: sudo apt install ffmpeg") @@ -568,7 +568,7 @@ class YouTubeClient: this returns YouTubeSearchResult objects with video-specific metadata (thumbnails, view counts, channel names) for UI display. """ - logger.info(f"🎬 Searching YouTube videos for: {query}") + logger.info(f"Searching YouTube videos for: {query}") try: loop = asyncio.get_event_loop() @@ -643,7 +643,7 @@ class YouTubeClient: Returns: Tuple of (track_results, album_results). Album results will always be empty for YouTube. """ - logger.info(f"🔍 Searching YouTube for: {query}") + logger.info(f"Searching YouTube for: {query}") try: # Run yt-dlp in executor to avoid blocking event loop @@ -693,13 +693,13 @@ class YouTubeClient: track_result = self._youtube_to_track_result(entry, best_audio) track_results.append(track_result) - logger.info(f"✅ Found {len(track_results)} YouTube tracks") + logger.info(f"Found {len(track_results)} YouTube tracks") # Return tuple: (tracks, albums) - YouTube doesn't have albums, so return empty list return (track_results, []) except Exception as e: - logger.error(f"❌ YouTube search failed: {e}") + logger.error(f"YouTube search failed: {e}") import traceback traceback.print_exc() return ([], []) @@ -846,7 +846,7 @@ class YouTubeClient: # Sort by confidence (best first) matches.sort(key=lambda r: r.confidence, reverse=True) - logger.info(f"✅ Found {len(matches)} matches above {min_confidence} confidence") + logger.info(f"Found {len(matches)} matches above {min_confidence} confidence") return matches async def download(self, username: str, filename: str, file_size: int = 0) -> Optional[str]: @@ -867,13 +867,13 @@ class YouTubeClient: try: # Parse filename to extract video_id if '||' not in filename: - logger.error(f"❌ Invalid filename format: {filename}") + logger.error(f"Invalid filename format: {filename}") return None video_id, title = filename.split('||', 1) youtube_url = f"https://www.youtube.com/watch?v={video_id}" - logger.info(f"📥 Starting YouTube download: {title}") + logger.info(f"Starting YouTube download: {title}") logger.info(f" URL: {youtube_url}") # Create unique download ID @@ -905,11 +905,11 @@ class YouTubeClient: ) download_thread.start() - logger.info(f"✅ YouTube download {download_id} started in background") + logger.info(f"YouTube download {download_id} started in background") return download_id except Exception as e: - logger.error(f"❌ Failed to start YouTube download: {e}") + logger.error(f"Failed to start YouTube download: {e}") import traceback traceback.print_exc() return None @@ -926,7 +926,7 @@ class YouTubeClient: elapsed = time.time() - self._last_download_time if self._last_download_time > 0 and elapsed < self._download_delay: wait_time = self._download_delay - elapsed - logger.info(f"⏳ Rate limiting: waiting {wait_time:.1f}s before next YouTube download") + logger.info(f"Rate limiting: waiting {wait_time:.1f}s before next YouTube download") time.sleep(wait_time) # Update state to downloading @@ -958,17 +958,17 @@ class YouTubeClient: self.active_downloads[download_id]['file_path'] = file_path # DO NOT update filename - keep original_filename for context matching - logger.info(f"✅ YouTube download {download_id} completed: {file_path}") + logger.info(f"YouTube download {download_id} completed: {file_path}") else: # Mark as errored with self._download_lock: if download_id in self.active_downloads: self.active_downloads[download_id]['state'] = 'Errored' - logger.error(f"❌ YouTube download {download_id} failed") + logger.error(f"YouTube download {download_id} failed") except Exception as e: - logger.error(f"❌ YouTube download thread failed for {download_id}: {e}") + logger.error(f"YouTube download thread failed for {download_id}: {e}") import traceback traceback.print_exc() @@ -997,7 +997,7 @@ class YouTubeClient: for attempt in range(max_retries): # Check for server shutdown using callback if self.shutdown_check and self.shutdown_check(): - logger.info(f"🛑 Server shutting down, aborting download attempt {attempt + 1}") + logger.info(f"Server shutting down, aborting download attempt {attempt + 1}") return None try: @@ -1012,15 +1012,15 @@ class YouTubeClient: if attempt == 1: # Drop browser cookies — authenticated sessions sometimes get restricted formats if 'cookiesfrombrowser' in download_opts: - logger.info(f"🔄 Retry {attempt + 1}/{max_retries} without browser cookies") + logger.info(f"Retry {attempt + 1}/{max_retries} without browser cookies") download_opts.pop('cookiesfrombrowser', None) else: - logger.info(f"🔄 Retry {attempt + 1}/{max_retries} with web_creator client") + logger.info(f"Retry {attempt + 1}/{max_retries} with web_creator client") download_opts['extractor_args'] = { 'youtube': { 'player_client': ['web_creator'] } } elif attempt >= 2: - logger.info(f"🔄 Retry {attempt + 1}/{max_retries} with 'best' format (video fallback)") + logger.info(f"Retry {attempt + 1}/{max_retries} with 'best' format (video fallback)") download_opts['format'] = 'best' download_opts.pop('cookiesfrombrowser', None) download_opts.pop('extractor_args', None) @@ -1036,19 +1036,19 @@ class YouTubeClient: if filename.exists(): return str(filename) else: - logger.error(f"❌ Download completed but file not found: {filename}") + logger.error(f"Download completed but file not found: {filename}") if attempt < max_retries - 1: continue # Retry return None except Exception as e: error_msg = str(e) - logger.error(f"❌ Download attempt {attempt + 1} failed: {error_msg}") + logger.error(f"Download attempt {attempt + 1} failed: {error_msg}") # Check if it's a 403 error if '403' in error_msg or 'Forbidden' in error_msg: if attempt < max_retries - 1: - logger.info(f"⏳ Waiting 2 seconds before retry...") + logger.info(f"Waiting 2 seconds before retry...") import time time.sleep(2) continue # Retry on 403 @@ -1065,7 +1065,7 @@ class YouTubeClient: return None # All retries failed except Exception as e: - logger.error(f"❌ Download failed: {e}") + logger.error(f"Download failed: {e}") import traceback traceback.print_exc() return None @@ -1204,7 +1204,7 @@ class YouTubeClient: # Remove them for download_id in ids_to_remove: del self.active_downloads[download_id] - logger.debug(f"🗑️ Cleared finished download {download_id}") + logger.debug(f"Cleared finished download {download_id}") return True except Exception as e: @@ -1229,22 +1229,22 @@ class YouTubeClient: try: with self._download_lock: if download_id not in self.active_downloads: - logger.warning(f"⚠️ Download {download_id} not found") + logger.warning(f"Download {download_id} not found") return False # Update state to cancelled self.active_downloads[download_id]['state'] = 'Cancelled' - logger.info(f"⚠️ Marked YouTube download {download_id} as cancelled") + logger.info(f"Marked YouTube download {download_id} as cancelled") # Remove from active downloads if requested if remove: del self.active_downloads[download_id] - logger.info(f"🗑️ Removed YouTube download {download_id} from queue") + logger.info(f"Removed YouTube download {download_id} from queue") return True except Exception as e: - logger.error(f"❌ Failed to cancel download {download_id}: {e}") + logger.error(f"Failed to cancel download {download_id}: {e}") return False def _enhance_metadata(self, filepath: str, spotify_track: Optional[SpotifyTrack], yt_result: YouTubeSearchResult, track_number: int = 1, disc_number: int = 1, release_year: str = None, artist_genres: list = None): @@ -1258,7 +1258,7 @@ class YouTubeClient: from mutagen.id3 import ID3NoHeaderError import requests - logger.info(f"🏷️ Enhancing metadata for: {Path(filepath).name}") + logger.info(f"Enhancing metadata for: {Path(filepath).name}") # Load MP3 file audio = MP3(filepath) @@ -1267,11 +1267,11 @@ class YouTubeClient: if audio.tags is not None: # Delete ALL existing frames audio.tags.clear() - logger.debug(f" 🧹 Cleared all existing tag frames") + logger.debug(f" Cleared all existing tag frames") else: # No tags exist, add them audio.add_tags() - logger.debug(f" ➕ Added new tag structure") + logger.debug(f" Added new tag structure") if spotify_track: # Use Spotify metadata @@ -1295,7 +1295,7 @@ class YouTubeClient: except: pass - logger.debug(f" 📝 Setting metadata tags...") + logger.debug(f" Setting metadata tags...") # Set ID3 tags (using setall to ensure they're set) audio.tags.setall('TIT2', [TIT2(encoding=3, text=title)]) @@ -1314,21 +1314,21 @@ class YouTubeClient: # Combine up to 3 genres (matches production logic) genre = ', '.join(artist_genres[:3]) audio.tags.setall('TCON', [TCON(encoding=3, text=genre)]) - logger.debug(f" ✓ Genre: {genre}") + logger.debug(f" Genre: {genre}") audio.tags.setall('COMM', [COMM(encoding=3, lang='eng', desc='', text=f'Downloaded via SoulSync (YouTube)\nSource: {yt_result.url}\nConfidence: {yt_result.confidence:.2f}')]) - logger.debug(f" ✓ Artist: {artist}") - logger.debug(f" ✓ Album Artist: {album_artist}") - logger.debug(f" ✓ Title: {title}") - logger.debug(f" ✓ Album: {album}") - logger.debug(f" ✓ Track #: {track_number}") - logger.debug(f" ✓ Disc #: {disc_number}") - logger.debug(f" ✓ Year: {year}") + logger.debug(f" Artist: {artist}") + logger.debug(f" Album Artist: {album_artist}") + logger.debug(f" Title: {title}") + logger.debug(f" Album: {album}") + logger.debug(f" Track #: {track_number}") + logger.debug(f" Disc #: {disc_number}") + logger.debug(f" Year: {year}") # Fetch and embed album art from Spotify (via search) - logger.debug(f" 🎨 Fetching album art from Spotify...") + logger.debug(f" Fetching album art from Spotify...") album_art_url = self._get_spotify_album_art(spotify_track) if album_art_url: @@ -1354,25 +1354,25 @@ class YouTubeClient: data=response.content )) - logger.debug(f" ✓ Album art embedded ({len(response.content) // 1024} KB)") + logger.debug(f" Album art embedded ({len(response.content) // 1024} KB)") except Exception as art_error: - logger.warning(f" ⚠️ Could not embed album art: {art_error}") + logger.warning(f" Could not embed album art: {art_error}") else: - logger.warning(f" ⚠️ No album art found on Spotify") + logger.warning(f" No album art found on Spotify") # Save all tags audio.save() - logger.info(f"✅ Metadata enhanced successfully") + logger.info(f"Metadata enhanced successfully") # Return album art URL for cover.jpg creation return album_art_url except ImportError: - logger.warning("⚠️ mutagen not installed - skipping enhanced metadata tagging") + logger.warning("mutagen not installed - skipping enhanced metadata tagging") logger.warning(" Install with: pip install mutagen") return None except Exception as e: - logger.warning(f"⚠️ Could not enhance metadata: {e}") + logger.warning(f"Could not enhance metadata: {e}") return None def _get_spotify_album_art(self, spotify_track: SpotifyTrack) -> Optional[str]: @@ -1409,7 +1409,7 @@ class YouTubeClient: logger.debug(f" ℹ️ cover.jpg already exists, skipping") return - logger.debug(f" 📥 Downloading cover.jpg...") + logger.debug(f" Downloading cover.jpg...") response = requests.get(album_art_url, timeout=10) response.raise_for_status() @@ -1417,10 +1417,10 @@ class YouTubeClient: # Save to file cover_path.write_bytes(response.content) - logger.debug(f" ✅ Saved cover.jpg ({len(response.content) // 1024} KB)") + logger.debug(f" Saved cover.jpg ({len(response.content) // 1024} KB)") except Exception as e: - logger.warning(f" ⚠️ Could not save cover.jpg: {e}") + logger.warning(f" Could not save cover.jpg: {e}") def _create_lyrics_file(self, audio_file_path: str, spotify_track: SpotifyTrack): """ @@ -1431,10 +1431,10 @@ class YouTubeClient: from core.lyrics_client import lyrics_client if not lyrics_client.api: - logger.debug(f" 🎵 LRClib API not available - skipping lyrics") + logger.debug(f" LRClib API not available - skipping lyrics") return - logger.debug(f" 🎵 Fetching lyrics from LRClib...") + logger.debug(f" Fetching lyrics from LRClib...") # Get track metadata artist_name = spotify_track.artists[0] if spotify_track.artists else "Unknown Artist" @@ -1452,14 +1452,14 @@ class YouTubeClient: ) if success: - logger.debug(f" ✅ Created .lrc lyrics file") + logger.debug(f" Created .lrc lyrics file") else: - logger.debug(f" 🎵 No lyrics found on LRClib") + logger.debug(f" No lyrics found on LRClib") except ImportError: - logger.debug(f" ⚠️ lyrics_client not available - skipping lyrics") + logger.debug(f" lyrics_client not available - skipping lyrics") except Exception as e: - logger.warning(f" ⚠️ Could not create lyrics file: {e}") + logger.warning(f" Could not create lyrics file: {e}") def search_and_download_best(self, spotify_track: SpotifyTrack, min_confidence: float = 0.58) -> Optional[str]: """ @@ -1473,7 +1473,7 @@ class YouTubeClient: Returns: Path to downloaded file, or None if failed """ - logger.info(f"🎯 Starting YouTube download flow for: {spotify_track.name} by {spotify_track.artists[0]}") + logger.info(f"Starting YouTube download flow for: {spotify_track.name} by {spotify_track.artists[0]}") # Generate search query query = f"{spotify_track.artists[0]} {spotify_track.name}" @@ -1482,19 +1482,19 @@ class YouTubeClient: results = self.search(query, max_results=10) if not results: - logger.error(f"❌ No YouTube results found for query: {query}") + logger.error(f"No YouTube results found for query: {query}") return None # Find best matches matches = self.find_best_matches(spotify_track, results, min_confidence=min_confidence) if not matches: - logger.error(f"❌ No matches above {min_confidence} confidence threshold") + logger.error(f"No matches above {min_confidence} confidence threshold") return None # Try downloading best match best_match = matches[0] - logger.info(f"🎯 Best match: {best_match.title} (confidence: {best_match.confidence:.2f})") + logger.info(f"Best match: {best_match.title} (confidence: {best_match.confidence:.2f})") downloaded_file = self.download(best_match, spotify_track) diff --git a/database/music_database.py b/database/music_database.py index 2c4ab73d..4a567acd 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -1524,7 +1524,7 @@ class MusicDatabase: cursor.execute("ALTER TABLE artists ADD COLUMN musicbrainz_match_status TEXT") columns_added = True if columns_added: - logger.info("✅ Added MusicBrainz columns to artists table") + logger.info("Added MusicBrainz columns to artists table") # --- Albums --- cursor.execute("PRAGMA table_info(albums)") @@ -1542,7 +1542,7 @@ class MusicDatabase: added_albums = True if added_albums: columns_added = True - logger.info("✅ Added MusicBrainz columns to albums table") + logger.info("Added MusicBrainz columns to albums table") # --- Tracks --- cursor.execute("PRAGMA table_info(tracks)") @@ -1560,7 +1560,7 @@ class MusicDatabase: added_tracks = True if added_tracks: columns_added = True - logger.info("✅ Added MusicBrainz columns to tracks table") + logger.info("Added MusicBrainz columns to tracks table") # Create MusicBrainz cache table for storing API results cursor.execute(""" @@ -1593,7 +1593,7 @@ class MusicDatabase: cursor.execute("CREATE INDEX IF NOT EXISTS idx_mb_cache_failed ON musicbrainz_cache (entity_type, last_updated) WHERE musicbrainz_id IS NULL") if columns_added: - logger.info("🎉 MusicBrainz migration completed successfully") + logger.info("MusicBrainz migration completed successfully") except Exception as e: logger.error(f"Error in MusicBrainz migration: {e}") @@ -2902,7 +2902,7 @@ class MusicDatabase: cleared = cursor.rowcount cursor.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('soulid_v2_migration', '1')") if cleared > 0: - logger.info(f"🔄 SoulID v2 migration: cleared {cleared} artist soul_ids for regeneration") + logger.info(f"SoulID v2 migration: cleared {cleared} artist soul_ids for regeneration") except Exception as e: logger.error(f"Error adding soul_id columns: {e}") @@ -3932,7 +3932,7 @@ class MusicDatabase: DELETE FROM artists WHERE id NOT IN (SELECT DISTINCT artist_id FROM tracks WHERE artist_id IS NOT NULL) """) - logger.info(f"🧹 Removed {orphaned_artists_count} orphaned artists") + logger.info(f"Removed {orphaned_artists_count} orphaned artists") # Delete orphaned albums if orphaned_albums_count > 0: @@ -3940,7 +3940,7 @@ class MusicDatabase: DELETE FROM albums WHERE id NOT IN (SELECT DISTINCT album_id FROM tracks WHERE album_id IS NOT NULL) """) - logger.info(f"🧹 Removed {orphaned_albums_count} orphaned albums") + logger.info(f"Removed {orphaned_albums_count} orphaned albums") conn.commit() @@ -3973,7 +3973,7 @@ class MusicDatabase: duplicate_groups = cursor.fetchall() if not duplicate_groups: - logger.debug("🧹 No duplicate artists found") + logger.debug("No duplicate artists found") return {'artists_merged': 0, 'albums_migrated': 0} total_merged = 0 @@ -3993,7 +3993,7 @@ class MusicDatabase: server_source = group['server_source'] ids = group['ids'].split(',') - logger.info(f"🔄 Merging duplicate artist '{artist_name}' ({server_source}): IDs {ids}") + logger.info(f"Merging duplicate artist '{artist_name}' ({server_source}): IDs {ids}") # Pick the keeper: the one with the most enrichment data best_id = ids[0] @@ -4061,7 +4061,7 @@ class MusicDatabase: conn.commit() if total_merged > 0: - logger.info(f"🧹 Duplicate merge complete: {total_merged} duplicates merged, {total_albums_migrated} albums migrated") + logger.info(f"Duplicate merge complete: {total_merged} duplicates merged, {total_albums_migrated} albums migrated") return {'artists_merged': total_merged, 'albums_migrated': total_albums_migrated} @@ -4292,7 +4292,7 @@ class MusicDatabase: if existing_by_name: old_id = existing_by_name['id'] # ratingKey changed — migrate old artist to new ID, preserving enrichment data - logger.info(f"🔄 Artist ratingKey migrated: '{name}' ({old_id} → {artist_id})") + logger.info(f"Artist ratingKey migrated: '{name}' ({old_id} → {artist_id})") # Step 1: Insert new artist record, copying enrichment data from old enrichment_cols = [ @@ -4463,7 +4463,7 @@ class MusicDatabase: if existing_by_title: old_id = existing_by_title['id'] # ratingKey changed — migrate old album to new ID, preserving enrichment data - logger.info(f"🔄 Album ratingKey migrated: '{title}' ({old_id} → {album_id})") + logger.info(f"Album ratingKey migrated: '{title}' ({old_id} → {album_id})") enrichment_cols = [ 'musicbrainz_release_id', 'musicbrainz_last_attempted', 'musicbrainz_match_status', @@ -4857,13 +4857,13 @@ class MusicDatabase: basic_results = self._search_tracks_basic(cursor, title, artist, limit, server_source) if basic_results: - logger.debug(f"🔍 Basic search found {len(basic_results)} results") + logger.debug(f"Basic search found {len(basic_results)} results") return basic_results # STRATEGY 2: Broader fuzzy search - splits into individual words with OR matching fuzzy_results = self._search_tracks_fuzzy_fallback(cursor, title, artist, limit, server_source) if fuzzy_results: - logger.debug(f"🔍 Fuzzy fallback search found {len(fuzzy_results)} results") + logger.debug(f"Fuzzy fallback search found {len(fuzzy_results)} results") return fuzzy_results @@ -5108,7 +5108,7 @@ class MusicDatabase: # Generate title variations for better matching (similar to album approach) title_variations = self._generate_track_title_variations(title) - logger.debug(f"🔍 Enhanced track matching for '{title}' by '{artist}': trying {len(title_variations)} variations") + logger.debug(f"Enhanced track matching for '{title}' by '{artist}': trying {len(title_variations)} variations") for i, var in enumerate(title_variations): logger.debug(f" {i+1}. '{var}'") @@ -5126,12 +5126,12 @@ class MusicDatabase: if not potential_matches: continue - logger.debug(f"🎵 Found {len(potential_matches)} tracks for variation '{title_variation}'") + logger.debug(f"Found {len(potential_matches)} tracks for variation '{title_variation}'") # Score each potential match for track in potential_matches: confidence = self._calculate_track_confidence(title, artist, track) - logger.debug(f" 🎯 '{track.title}' confidence: {confidence:.3f}") + logger.debug(f" '{track.title}' confidence: {confidence:.3f}") if confidence > best_confidence: best_confidence = confidence @@ -5139,13 +5139,13 @@ class MusicDatabase: # Return match only if it meets threshold if best_match and best_confidence >= confidence_threshold: - logger.debug(f"✅ Enhanced track match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") + logger.debug(f"Enhanced track match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") return best_match, best_confidence # Album-aware fallback: find album by title (any artist), check tracks on it # Handles multi-artist albums filed under a different artist in the library if album and best_confidence < confidence_threshold: - logger.debug(f"⚠️ Artist-specific search failed, trying album-aware fallback: '{title}' on '{album}'") + logger.debug(f"Artist-specific search failed, trying album-aware fallback: '{title}' on '{album}'") try: album_candidates = self.search_albums(title=album, artist="", limit=10, server_source=server_source) for album_candidate in album_candidates: @@ -5185,12 +5185,12 @@ class MusicDatabase: best_match = db_track if best_match and best_confidence >= 0.7: - logger.debug(f"✅ Album-aware fallback matched: '{title}' on '{album}' -> '{best_match.title}' by '{best_match.artist_name}' (title_sim: {best_confidence:.3f})") + logger.debug(f"Album-aware fallback matched: '{title}' on '{album}' -> '{best_match.title}' by '{best_match.artist_name}' (title_sim: {best_confidence:.3f})") return best_match, best_confidence except Exception as album_fallback_err: logger.debug(f"Album-aware fallback error: {album_fallback_err}") - logger.debug(f"❌ No confident track match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})") + logger.debug(f"No confident track match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})") return None, best_confidence except Exception as e: @@ -5441,7 +5441,7 @@ class MusicDatabase: # Generate album title variations for edition matching title_variations = self._generate_album_title_variations(title) - logger.debug(f"🔍 Edition matching for '{title}' by '{artist}': trying {len(title_variations)} variations") + logger.debug(f"Edition matching for '{title}' by '{artist}': trying {len(title_variations)} variations") for i, var in enumerate(title_variations): logger.debug(f" {i+1}. '{var}'") @@ -5462,7 +5462,7 @@ class MusicDatabase: existing_ids.add(album.id) if albums: - logger.debug(f"📀 Found {len(albums)} albums for variation '{variation}'") + logger.debug(f"Found {len(albums)} albums for variation '{variation}'") if not albums: continue @@ -5470,7 +5470,7 @@ class MusicDatabase: # Score each potential match with Smart Edition Matching for album in albums: confidence = self._calculate_album_confidence(title, artist, album, expected_track_count) - logger.debug(f" 🎯 '{album.title}' confidence: {confidence:.3f}") + logger.debug(f" '{album.title}' confidence: {confidence:.3f}") if confidence > best_confidence: best_confidence = confidence @@ -5478,13 +5478,13 @@ class MusicDatabase: # Return match only if it meets threshold if best_match and best_confidence >= confidence_threshold: - logger.debug(f"✅ Edition match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") + logger.debug(f"Edition match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") return best_match, best_confidence # Fallback: Check ALL albums by this artist (resolves SQL accent sensitivity issues #101) # If we haven't found a match yet, fetch broader list from artist and double check if best_confidence < confidence_threshold: - logger.debug(f"⚠️ specific title search failed, trying broad artist search fallback for '{artist}'") + logger.debug(f"specific title search failed, trying broad artist search fallback for '{artist}'") try: # Get ALL albums by this artist (limit 100 to be safe) # This bypasses SQL 'LIKE' limitations for diacritics (e.g. 'ă' vs 'a') @@ -5508,18 +5508,18 @@ class MusicDatabase: if confidence > best_confidence: best_confidence = confidence best_match = album - logger.debug(f" 🎯 Fallback match: '{album.title}' confidence: {confidence:.3f}") + logger.debug(f" Fallback match: '{album.title}' confidence: {confidence:.3f}") except Exception as fallback_error: logger.warning(f"Fallback artist search failed: {fallback_error}") if best_match and best_confidence >= confidence_threshold: - logger.debug(f"✅ Fallback match succeeded: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") + logger.debug(f"Fallback match succeeded: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") return best_match, best_confidence # Multi-artist fallback: search by title only (any artist) # Handles collaborative albums filed under a different artist in the library if best_confidence < confidence_threshold: - logger.debug(f"⚠️ Artist-specific search failed, trying title-only fallback for '{title}'") + logger.debug(f"Artist-specific search failed, trying title-only fallback for '{title}'") try: title_only_albums = self.search_albums(title=title, artist="", limit=20, server_source=server_source) for album in title_only_albums: @@ -5528,15 +5528,15 @@ class MusicDatabase: if confidence > best_confidence: best_confidence = confidence best_match = album - logger.debug(f" 🎯 Title-only match: '{album.title}' (confidence: {confidence:.3f})") + logger.debug(f" Title-only match: '{album.title}' (confidence: {confidence:.3f})") except Exception as title_error: logger.warning(f"Title-only fallback search failed: {title_error}") if best_match and best_confidence >= confidence_threshold: - logger.debug(f"✅ Title-only match succeeded: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") + logger.debug(f"Title-only match succeeded: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") return best_match, best_confidence - logger.debug(f"❌ No confident edition match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})") + logger.debug(f"No confident edition match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})") return None, best_confidence except Exception as e: @@ -5662,7 +5662,7 @@ class MusicDatabase: # Log when normalized matching helps (only if it's the best score and better than others) if normalized_title_similarity == best_title_similarity and normalized_title_similarity > max(title_similarity, clean_title_similarity): - logger.debug(f" 🌍 Diacritic normalization improved match: '{search_title}' -> '{db_album.title}' (normalized: {normalized_title_similarity:.3f} vs raw: {title_similarity:.3f})") + logger.debug(f" Diacritic normalization improved match: '{search_title}' -> '{db_album.title}' (normalized: {normalized_title_similarity:.3f} vs raw: {title_similarity:.3f})") # Require minimum title similarity to prevent a perfect artist match from # carrying a bad title match over the threshold (e.g. "divisions" vs "silos") @@ -5684,12 +5684,12 @@ class MusicDatabase: # Found same/better edition (e.g., Deluxe when searching for Standard) edition_bonus = min(0.15, (db_album.track_count - expected_track_count) / expected_track_count * 0.1) confidence += edition_bonus - logger.debug(f" 📀 Edition upgrade bonus: +{edition_bonus:.3f} ({db_album.track_count} >= {expected_track_count} tracks)") + logger.debug(f" Edition upgrade bonus: +{edition_bonus:.3f} ({db_album.track_count} >= {expected_track_count} tracks)") elif db_album.track_count < expected_track_count * 0.8: # Found significantly smaller edition, apply penalty edition_penalty = 0.1 confidence -= edition_penalty - logger.debug(f" 📀 Edition downgrade penalty: -{edition_penalty:.3f} ({db_album.track_count} << {expected_track_count} tracks)") + logger.debug(f" Edition downgrade penalty: -{edition_penalty:.3f} ({db_album.track_count} << {expected_track_count} tracks)") return min(confidence, 1.0) # Cap at 1.0 @@ -5800,7 +5800,7 @@ class MusicDatabase: # Debug logging for Unicode normalization if search_title != search_title_norm or search_artist != search_artist_norm or \ db_track.title != db_title_norm or db_track.artist_name != db_artist_norm: - logger.debug(f"🔤 Unicode normalization:") + logger.debug(f"Unicode normalization:") logger.debug(f" Search: '{search_title}' → '{search_title_norm}' | '{search_artist}' → '{search_artist_norm}'") logger.debug(f" Database: '{db_track.title}' → '{db_title_norm}' | '{db_track.artist_name}' → '{db_artist_norm}'") diff --git a/services/sync_service.py b/services/sync_service.py index 745734cd..5a49895c 100644 --- a/services/sync_service.py +++ b/services/sync_service.py @@ -271,9 +271,9 @@ class PlaylistSyncService: for i, track in enumerate(media_tracks): if track and hasattr(track, 'ratingKey'): valid_tracks.append(track) - logger.debug(f"✔️ Track {i+1} valid for playlist: '{track.title}' (ratingKey: {track.ratingKey})") + logger.debug(f"Track {i+1} valid for playlist: '{track.title}' (ratingKey: {track.ratingKey})") else: - logger.warning(f"❌ Track {i+1} invalid for playlist: {track} (type: {type(track)}, has ratingKey: {hasattr(track, 'ratingKey') if track else 'N/A'})") + logger.warning(f"Track {i+1} invalid for playlist: {track} (type: {type(track)}, has ratingKey: {hasattr(track, 'ratingKey') if track else 'N/A'})") logger.info(f"Playlist validation: {len(valid_tracks)}/{len(media_tracks)} tracks are valid {server_type.title()} objects with ratingKeys") @@ -312,7 +312,7 @@ class PlaylistSyncService: # Auto-add unmatched tracks to wishlist (skip in Wing It mode) wishlist_added_count = 0 if unmatched_tracks and getattr(self, '_skip_wishlist', False): - logger.info(f"⚡ [Wing It] Skipping wishlist for {len(unmatched_tracks)} unmatched tracks") + logger.info(f"[Wing It] Skipping wishlist for {len(unmatched_tracks)} unmatched tracks") unmatched_tracks = [] # Clear so the loop below doesn't run if unmatched_tracks: try: @@ -484,10 +484,10 @@ class PlaylistSyncService: actual_track = None if actual_track: - logger.debug(f"⚡ Sync cache hit: '{original_title}' → server track {server_track_id}") + logger.debug(f"Sync cache hit: '{original_title}' → server track {server_track_id}") return actual_track, cached['confidence'] - logger.debug(f"🔄 Sync cache stale for '{original_title}' — track {server_track_id} gone") + logger.debug(f"Sync cache stale for '{original_title}' — track {server_track_id} gone") except Exception as cache_err: logger.debug(f"Sync cache lookup error: {cache_err}") # --- End cache fast-path --- @@ -511,7 +511,7 @@ class PlaylistSyncService: db_track, confidence = db.check_track_exists(original_title, artist_name, confidence_threshold=0.7, server_source=active_server) if db_track and confidence >= 0.7: - logger.debug(f"✔️ Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}") + logger.debug(f"Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}") # Save to sync match cache for next time if spotify_id: @@ -536,7 +536,7 @@ class PlaylistSyncService: self.id = db_track.id actual_track = JellyfinTrackFromDB(db_track) - logger.debug(f"✔️ Created Jellyfin track object for '{db_track.title}' (ID: {actual_track.ratingKey})") + logger.debug(f"Created Jellyfin track object for '{db_track.title}' (ID: {actual_track.ratingKey})") return actual_track, confidence elif server_type == "navidrome": # For Navidrome, create a track object from database info (similar to Jellyfin) @@ -547,7 +547,7 @@ class PlaylistSyncService: self.id = db_track.id actual_track = NavidromeTrackFromDB(db_track) - logger.debug(f"✔️ Created Navidrome track object for '{db_track.title}' (ID: {actual_track.ratingKey})") + logger.debug(f"Created Navidrome track object for '{db_track.title}' (ID: {actual_track.ratingKey})") return actual_track, confidence else: # For Plex, use the original fetchItem approach @@ -556,16 +556,16 @@ class PlaylistSyncService: track_id = int(db_track.id) actual_plex_track = media_client.server.fetchItem(track_id) if actual_plex_track and hasattr(actual_plex_track, 'ratingKey'): - logger.debug(f"✔️ Successfully fetched actual Plex track for '{db_track.title}' (ratingKey: {actual_plex_track.ratingKey})") + logger.debug(f"Successfully fetched actual Plex track for '{db_track.title}' (ratingKey: {actual_plex_track.ratingKey})") return actual_plex_track, confidence else: - logger.warning(f"❌ Fetched Plex track for '{db_track.title}' lacks ratingKey attribute") + logger.warning(f"Fetched Plex track for '{db_track.title}' lacks ratingKey attribute") except ValueError: - logger.warning(f"❌ Invalid Plex track ID format for '{db_track.title}' (ID: {db_track.id}) - skipping this track") + logger.warning(f"Invalid Plex track ID format for '{db_track.title}' (ID: {db_track.id}) - skipping this track") continue except Exception as fetch_error: - logger.error(f"❌ Failed to fetch actual {server_type} track for '{db_track.title}' (ID: {db_track.id}): {fetch_error}") + logger.error(f"Failed to fetch actual {server_type} track for '{db_track.title}' (ID: {db_track.id}): {fetch_error}") # Continue to try other artists rather than fail completely continue @@ -573,7 +573,7 @@ class PlaylistSyncService: logger.error(f"Error checking track existence for '{original_title}' by '{artist_name}': {db_error}") continue - logger.debug(f"❌ No database match found for '{original_title}' by any of the artists {spotify_track.artists}") + logger.debug(f"No database match found for '{original_title}' by any of the artists {spotify_track.artists}") return None, 0.0 except Exception as e: diff --git a/tests/conftest.py b/tests/conftest.py index b52f91a1..a9976c6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -188,7 +188,7 @@ _DEFAULT_DISCOVERY_STATES = { 'discovery_progress': 30, 'spotify_matches': 3, 'spotify_total': 10, 'discovery_results': [ {'index': 0, 'yt_track': 'Song B', 'yt_artist': 'Artist B', - 'status': '✅ Found', 'status_class': 'found', + 'status': 'Found', 'status_class': 'found', 'spotify_track': 'Song B', 'spotify_artist': 'Artist B', 'spotify_album': 'Album B'}, ], diff --git a/tests/test_phase2_dashboard.py b/tests/test_phase2_dashboard.py index 81751246..dc5ae65f 100644 --- a/tests/test_phase2_dashboard.py +++ b/tests/test_phase2_dashboard.py @@ -103,7 +103,7 @@ class TestActivityFeed: client = socketio.test_client(app) # Add some activities first add_item = shared_state['add_activity_item'] - add_item('🎵', 'Download Complete', 'Artist - Song', show_toast=False) + add_item('', 'Download Complete', 'Artist - Song', show_toast=False) build = shared_state['build_activity_feed_payload'] socketio.emit('dashboard:activity', build()) @@ -123,7 +123,7 @@ class TestActivityFeed: # Add an activity add_item = shared_state['add_activity_item'] - add_item('🎵', 'Test Activity', 'Test subtitle', show_toast=False) + add_item('', 'Test Activity', 'Test subtitle', show_toast=False) http_data = flask_client.get('/api/activity/feed').get_json() build = shared_state['build_activity_feed_payload'] @@ -158,7 +158,7 @@ class TestToasts: client.get_received() # clear add_item = shared_state['add_activity_item'] - add_item('✅', 'Download Complete', 'Artist - Song', show_toast=True) + add_item('', 'Download Complete', 'Artist - Song', show_toast=True) received = client.get_received() toast_events = [e for e in received if e['name'] == 'dashboard:toast'] @@ -174,7 +174,7 @@ class TestToasts: client.get_received() # clear add_item = shared_state['add_activity_item'] - add_item('📊', 'Background Task', 'Silent update', show_toast=False) + add_item('', 'Background Task', 'Silent update', show_toast=False) received = client.get_received() toast_events = [e for e in received if e['name'] == 'dashboard:toast'] @@ -187,7 +187,7 @@ class TestToasts: client.get_received() # clear add_item = shared_state['add_activity_item'] - add_item('✅', 'Test Title', 'Test Subtitle', 'Now', show_toast=True) + add_item('', 'Test Title', 'Test Subtitle', 'Now', show_toast=True) received = client.get_received() toast_events = [e for e in received if e['name'] == 'dashboard:toast'] @@ -205,7 +205,7 @@ class TestToasts: """GET /api/activity/toasts returns 200 with expected structure.""" # Add a toast-worthy activity first add_item = shared_state['add_activity_item'] - add_item('✅', 'Test', 'Sub', show_toast=True) + add_item('', 'Test', 'Sub', show_toast=True) resp = flask_client.get('/api/activity/toasts') assert resp.status_code == 200 diff --git a/tools/test_youtube_download.py b/tools/test_youtube_download.py index 585c9e8c..59bbdbd6 100644 --- a/tools/test_youtube_download.py +++ b/tools/test_youtube_download.py @@ -32,7 +32,7 @@ if sys.platform == 'win32': try: import yt_dlp except ImportError: - print("❌ yt-dlp not installed. Install with: pip install yt-dlp") + print("yt-dlp not installed. Install with: pip install yt-dlp") sys.exit(1) # Add parent directory to path to import from core @@ -122,12 +122,12 @@ class YouTubeClient: # Initialize production matching engine for parity with Soulseek self.matching_engine = MusicMatchingEngine() - logger.info("✅ Initialized production MusicMatchingEngine") + logger.info("Initialized production MusicMatchingEngine") # Check for ffmpeg (REQUIRED for MP3 conversion) if not self._check_ffmpeg(): print("\n" + "="*80) - print("❌ ERROR: ffmpeg is required but not found in PATH") + print("ERROR: ffmpeg is required but not found in PATH") print("="*80) print("\nInstall ffmpeg:") print(" Windows: scoop install ffmpeg") @@ -195,7 +195,7 @@ class YouTubeClient: # Check if ffmpeg is in system PATH if shutil.which('ffmpeg'): - logger.info("✅ Found ffmpeg in system PATH") + logger.info("Found ffmpeg in system PATH") return True # Auto-download ffmpeg to tools folder if not found @@ -211,7 +211,7 @@ class YouTubeClient: # If we already have both locally, use them if ffmpeg_path.exists() and ffprobe_path.exists(): - logger.info(f"✅ Found ffmpeg and ffprobe in tools folder") + logger.info(f"Found ffmpeg and ffprobe in tools folder") # Add to PATH so yt-dlp can find them import os tools_dir_str = str(tools_dir.absolute()) @@ -290,10 +290,10 @@ class YouTubeClient: ffprobe_zip.unlink() # Clean up zip else: - logger.error(f"❌ Unsupported platform: {system}") + logger.error(f"Unsupported platform: {system}") return False - logger.info(f"✅ Downloaded ffmpeg to: {ffmpeg_path}") + logger.info(f"Downloaded ffmpeg to: {ffmpeg_path}") # Add to PATH import os @@ -303,7 +303,7 @@ class YouTubeClient: return True except Exception as e: - logger.error(f"❌ Failed to download ffmpeg: {e}") + logger.error(f"Failed to download ffmpeg: {e}") logger.error(f" Please install manually:") logger.error(f" Windows: scoop install ffmpeg") logger.error(f" Linux: sudo apt install ffmpeg") @@ -331,7 +331,7 @@ class YouTubeClient: # Format speed safely speed_kb = speed / 1024 if speed else 0 - logger.info(f"📥 Progress: {progress:.1f}% | Speed: {speed_kb:.1f} KB/s | ETA: {eta}s") + logger.info(f"Progress: {progress:.1f}% | Speed: {speed_kb:.1f} KB/s | ETA: {eta}s") elif d['status'] == 'finished': self.current_download_progress = { @@ -339,14 +339,14 @@ class YouTubeClient: 'progress': 100.0, 'filename': d.get('filename', '') } - logger.info(f"✅ Download finished: {d.get('filename', '')}") + logger.info(f"Download finished: {d.get('filename', '')}") elif d['status'] == 'error': self.current_download_progress = { 'status': 'error', 'error': str(d.get('error', 'Unknown error')) } - logger.error(f"❌ Download error: {d.get('error', '')}") + logger.error(f"Download error: {d.get('error', '')}") def search(self, query: str, max_results: int = 10) -> List[YouTubeSearchResult]: """ @@ -359,7 +359,7 @@ class YouTubeClient: Returns: List of YouTubeSearchResult objects """ - logger.info(f"🔍 Searching YouTube for: '{query}'") + logger.info(f"Searching YouTube for: '{query}'") try: # Use YouTube Music for better music search results @@ -406,11 +406,11 @@ class YouTubeClient: logger.warning(f"Could not get detailed info for {entry['id']}: {e}") continue - logger.info(f"✅ Found {len(results)} YouTube results") + logger.info(f"Found {len(results)} YouTube results") return results except Exception as e: - logger.error(f"❌ YouTube search error: {e}") + logger.error(f"YouTube search error: {e}") return [] def _get_best_audio_format(self, formats: List[Dict]) -> Optional[Dict]: @@ -555,7 +555,7 @@ class YouTubeClient: # Sort by confidence (best first) matches.sort(key=lambda r: r.confidence, reverse=True) - logger.info(f"✅ Found {len(matches)} matches above {min_confidence} confidence") + logger.info(f"Found {len(matches)} matches above {min_confidence} confidence") return matches def download(self, yt_result: YouTubeSearchResult, spotify_track: Optional[SpotifyTrack] = None) -> Optional[str]: @@ -569,7 +569,7 @@ class YouTubeClient: Returns: Path to downloaded file, or None if failed """ - logger.info(f"📥 Starting download: {yt_result.title}") + logger.info(f"Starting download: {yt_result.title}") logger.info(f" Quality: {yt_result.available_quality}") logger.info(f" URL: {yt_result.url}") @@ -617,9 +617,9 @@ class YouTubeClient: except: pass - logger.info(f" 📀 Spotify track #{track_number} on album: {spotify_track.album} ({release_year})") + logger.info(f" Spotify track #{track_number} on album: {spotify_track.album} ({release_year})") except Exception as e: - logger.warning(f" ⚠️ Could not fetch Spotify track details: {e}") + logger.warning(f" Could not fetch Spotify track details: {e}") # If we have Spotify metadata, use production file organization if spotify_track: @@ -644,8 +644,8 @@ class YouTubeClient: # Override output template with production folder structure download_opts['outtmpl'] = str(album_folder / f'{final_filename}.%(ext)s') - logger.info(f" 📁 Album folder: {album_artist}/{album_artist} - {album}/") - logger.info(f" 📝 Filename: {final_filename}.mp3") + logger.info(f" Album folder: {album_artist}/{album_artist} - {album}/") + logger.info(f" Filename: {final_filename}.mp3") # Add metadata postprocessor with Spotify info download_opts['postprocessor_args'] = { @@ -669,7 +669,7 @@ class YouTubeClient: filename = Path(ydl.prepare_filename(info)).with_suffix('.mp3') if filename.exists(): - logger.info(f"✅ Download successful: {filename}") + logger.info(f"Download successful: {filename}") # Post-download: Enhance metadata with mutagen album_art_url = self._enhance_metadata(str(filename), spotify_track, yt_result, track_number, disc_number, release_year, artist_genres) @@ -684,11 +684,11 @@ class YouTubeClient: return str(filename) else: - logger.error(f"❌ Download completed but file not found: {filename}") + logger.error(f"Download completed but file not found: {filename}") return None except Exception as e: - logger.error(f"❌ Download failed: {e}") + logger.error(f"Download failed: {e}") import traceback traceback.print_exc() return None @@ -704,7 +704,7 @@ class YouTubeClient: from mutagen.id3 import ID3NoHeaderError import requests - logger.info(f"🏷️ Enhancing metadata for: {Path(filepath).name}") + logger.info(f"Enhancing metadata for: {Path(filepath).name}") # Load MP3 file audio = MP3(filepath) @@ -713,11 +713,11 @@ class YouTubeClient: if audio.tags is not None: # Delete ALL existing frames audio.tags.clear() - logger.info(f" 🧹 Cleared all existing tag frames") + logger.info(f" Cleared all existing tag frames") else: # No tags exist, add them audio.add_tags() - logger.info(f" ➕ Added new tag structure") + logger.info(f" Added new tag structure") if spotify_track: # Use Spotify metadata @@ -741,7 +741,7 @@ class YouTubeClient: except: pass - logger.info(f" 📝 Setting metadata tags...") + logger.info(f" Setting metadata tags...") # Set ID3 tags (using setall to ensure they're set) audio.tags.setall('TIT2', [TIT2(encoding=3, text=title)]) @@ -760,21 +760,21 @@ class YouTubeClient: # Combine up to 3 genres (matches production logic) genre = ', '.join(artist_genres[:3]) audio.tags.setall('TCON', [TCON(encoding=3, text=genre)]) - logger.info(f" ✓ Genre: {genre}") + logger.info(f" Genre: {genre}") audio.tags.setall('COMM', [COMM(encoding=3, lang='eng', desc='', text=f'Downloaded via SoulSync (YouTube)\nSource: {yt_result.url}\nConfidence: {yt_result.confidence:.2f}')]) - logger.info(f" ✓ Artist: {artist}") - logger.info(f" ✓ Album Artist: {album_artist}") - logger.info(f" ✓ Title: {title}") - logger.info(f" ✓ Album: {album}") - logger.info(f" ✓ Track #: {track_number}") - logger.info(f" ✓ Disc #: {disc_number}") - logger.info(f" ✓ Year: {year}") + logger.info(f" Artist: {artist}") + logger.info(f" Album Artist: {album_artist}") + logger.info(f" Title: {title}") + logger.info(f" Album: {album}") + logger.info(f" Track #: {track_number}") + logger.info(f" Disc #: {disc_number}") + logger.info(f" Year: {year}") # Fetch and embed album art from Spotify (via search) - logger.info(f" 🎨 Fetching album art from Spotify...") + logger.info(f" Fetching album art from Spotify...") album_art_url = self._get_spotify_album_art(spotify_track) if album_art_url: @@ -800,25 +800,25 @@ class YouTubeClient: data=response.content )) - logger.info(f" ✓ Album art embedded ({len(response.content) // 1024} KB)") + logger.info(f" Album art embedded ({len(response.content) // 1024} KB)") except Exception as art_error: - logger.warning(f" ⚠️ Could not embed album art: {art_error}") + logger.warning(f" Could not embed album art: {art_error}") else: - logger.warning(f" ⚠️ No album art found on Spotify") + logger.warning(f" No album art found on Spotify") # Save all tags audio.save() - logger.info(f"✅ Metadata enhanced successfully") + logger.info(f"Metadata enhanced successfully") # Return album art URL for cover.jpg creation return album_art_url except ImportError: - logger.warning("⚠️ mutagen not installed - skipping enhanced metadata tagging") + logger.warning("mutagen not installed - skipping enhanced metadata tagging") logger.warning(" Install with: pip install mutagen") return None except Exception as e: - logger.warning(f"⚠️ Could not enhance metadata: {e}") + logger.warning(f"Could not enhance metadata: {e}") import traceback traceback.print_exc() return None @@ -833,83 +833,83 @@ class YouTubeClient: sys.path.insert(0, str(Path(__file__).parent.parent)) from core.spotify_client import SpotifyClient - logger.info(f" 🔍 Getting Spotify client...") + logger.info(f" Getting Spotify client...") # Get authenticated Spotify client spotify_client = SpotifyClient() if not spotify_client: - logger.warning(f" ⚠️ Spotify client not available") + logger.warning(f" Spotify client not available") return None if not spotify_client.is_authenticated(): - logger.warning(f" ⚠️ Spotify client not authenticated") + logger.warning(f" Spotify client not authenticated") return None - logger.info(f" ✓ Spotify client authenticated") + logger.info(f" Spotify client authenticated") # Use the track ID if available (real Spotify IDs) if spotify_track.id and not spotify_track.id.startswith('test'): - logger.info(f" 🔍 Fetching track info for ID: {spotify_track.id}") + logger.info(f" Fetching track info for ID: {spotify_track.id}") try: # Get track info from Spotify API track_info = spotify_client.sp.track(spotify_track.id) if track_info: - logger.info(f" ✓ Got track info from Spotify") + logger.info(f" Got track info from Spotify") if 'album' in track_info: album_images = track_info['album'].get('images', []) - logger.info(f" 📸 Found {len(album_images)} album images") + logger.info(f" Found {len(album_images)} album images") if album_images: # Get highest quality image (first in list) album_art_url = album_images[0]['url'] - logger.info(f" ✓ Album art URL: {album_art_url[:50]}...") + logger.info(f" Album art URL: {album_art_url[:50]}...") return album_art_url else: - logger.warning(f" ⚠️ No album data in track info") + logger.warning(f" No album data in track info") else: - logger.warning(f" ⚠️ Track info is empty") + logger.warning(f" Track info is empty") except Exception as e: - logger.warning(f" ❌ Error fetching via track ID: {e}") + logger.warning(f" Error fetching via track ID: {e}") import traceback traceback.print_exc() # Fallback: Search for the track query = f"track:{spotify_track.name} artist:{spotify_track.artists[0]}" - logger.info(f" 🔍 Searching Spotify: {query}") + logger.info(f" Searching Spotify: {query}") try: search_results = spotify_client.sp.search(q=query, type='track', limit=1) if search_results and 'tracks' in search_results: tracks = search_results['tracks'].get('items', []) - logger.info(f" 📋 Search returned {len(tracks)} tracks") + logger.info(f" Search returned {len(tracks)} tracks") if tracks: album_images = tracks[0].get('album', {}).get('images', []) if album_images: # Get highest quality image (first in list) album_art_url = album_images[0]['url'] - logger.info(f" ✓ Found via search: {album_art_url[:50]}...") + logger.info(f" Found via search: {album_art_url[:50]}...") return album_art_url except Exception as search_error: - logger.warning(f" ❌ Search error: {search_error}") + logger.warning(f" Search error: {search_error}") import traceback traceback.print_exc() - logger.warning(f" ⚠️ No album art found on Spotify") + logger.warning(f" No album art found on Spotify") return None except ImportError as e: - logger.warning(f" ❌ Could not import Spotify client: {e}") + logger.warning(f" Could not import Spotify client: {e}") import traceback traceback.print_exc() return None except Exception as e: - logger.warning(f" ❌ Error fetching album art: {e}") + logger.warning(f" Error fetching album art: {e}") import traceback traceback.print_exc() return None @@ -925,10 +925,10 @@ class YouTubeClient: # Skip if already exists if cover_path.exists(): - logger.info(f" 📷 cover.jpg already exists") + logger.info(f" cover.jpg already exists") return - logger.info(f" 📷 Downloading cover.jpg to album folder...") + logger.info(f" Downloading cover.jpg to album folder...") # Download album art response = requests.get(album_art_url, timeout=10) @@ -937,10 +937,10 @@ class YouTubeClient: # Save to file cover_path.write_bytes(response.content) - logger.info(f" ✅ Saved cover.jpg ({len(response.content) // 1024} KB)") + logger.info(f" Saved cover.jpg ({len(response.content) // 1024} KB)") except Exception as e: - logger.warning(f" ⚠️ Could not save cover.jpg: {e}") + logger.warning(f" Could not save cover.jpg: {e}") def _create_lyrics_file(self, audio_file_path: str, spotify_track: SpotifyTrack): """ @@ -952,10 +952,10 @@ class YouTubeClient: from core.lyrics_client import lyrics_client if not lyrics_client.api: - logger.debug(f" 🎵 LRClib API not available - skipping lyrics") + logger.debug(f" LRClib API not available - skipping lyrics") return - logger.info(f" 🎵 Fetching lyrics from LRClib...") + logger.info(f" Fetching lyrics from LRClib...") # Get track metadata artist_name = spotify_track.artists[0] if spotify_track.artists else "Unknown Artist" @@ -973,14 +973,14 @@ class YouTubeClient: ) if success: - logger.info(f" ✅ Created .lrc lyrics file") + logger.info(f" Created .lrc lyrics file") else: - logger.info(f" 🎵 No lyrics found on LRClib") + logger.info(f" No lyrics found on LRClib") except ImportError: - logger.debug(f" ⚠️ lyrics_client not available - skipping lyrics") + logger.debug(f" lyrics_client not available - skipping lyrics") except Exception as e: - logger.warning(f" ⚠️ Could not create lyrics file: {e}") + logger.warning(f" Could not create lyrics file: {e}") def search_and_download_best(self, spotify_track: SpotifyTrack, min_confidence: float = 0.58) -> Optional[str]: """ @@ -994,7 +994,7 @@ class YouTubeClient: Returns: Path to downloaded file, or None if failed """ - logger.info(f"🎯 Starting YouTube download flow for: {spotify_track.name} by {spotify_track.artists[0]}") + logger.info(f"Starting YouTube download flow for: {spotify_track.name} by {spotify_track.artists[0]}") # Generate search query query = f"{spotify_track.artists[0]} {spotify_track.name}" @@ -1003,19 +1003,19 @@ class YouTubeClient: results = self.search(query, max_results=10) if not results: - logger.error(f"❌ No YouTube results found for query: {query}") + logger.error(f"No YouTube results found for query: {query}") return None # Find best matches matches = self.find_best_matches(spotify_track, results, min_confidence=min_confidence) if not matches: - logger.error(f"❌ No matches above {min_confidence} confidence threshold") + logger.error(f"No matches above {min_confidence} confidence threshold") return None # Try downloading best match best_match = matches[0] - logger.info(f"🎯 Best match: {best_match.title} (confidence: {best_match.confidence:.2f})") + logger.info(f"Best match: {best_match.title} (confidence: {best_match.confidence:.2f})") downloaded_file = self.download(best_match, spotify_track) @@ -1030,7 +1030,7 @@ def test_youtube_download(): """Test the YouTube download flow with a curated playlist""" print("=" * 80) - print("🎵 YouTube Download Test - Curated Playlist (5 Tracks)") + print("YouTube Download Test - Curated Playlist (5 Tracks)") print("=" * 80) print() @@ -1082,7 +1082,7 @@ def test_youtube_download(): ), ] - print("🎧 Curated Playlist - Testing Diverse Genres:") + print("Curated Playlist - Testing Diverse Genres:") print() for i, track in enumerate(test_playlist, 1): print(f" {i}. {track.artists[0]:20s} - {track.name}") @@ -1133,7 +1133,7 @@ def test_youtube_download(): 'error': None }) - print(f"\n✅ SUCCESS - Downloaded in {track_duration:.1f}s") + print(f"\nSUCCESS - Downloaded in {track_duration:.1f}s") print(f" File: {Path(downloaded_file).name}") print(f" Size: {file_size:.2f} MB") else: @@ -1148,13 +1148,13 @@ def test_youtube_download(): 'error': 'Download failed or no matches found' }) - print(f"\n❌ FAILED - No suitable match found") + print(f"\nFAILED - No suitable match found") except Exception as e: track_end_time = datetime.now() track_duration = (track_end_time - track_start_time).total_seconds() - print(f"\n❌ ERROR: {e}") + print(f"\nERROR: {e}") import traceback traceback.print_exc() @@ -1177,7 +1177,7 @@ def test_youtube_download(): # ======================================================================== print("\n\n" + "=" * 80) - print("📊 FINAL SUMMARY REPORT") + print("FINAL SUMMARY REPORT") print("=" * 80) print() @@ -1185,7 +1185,7 @@ def test_youtube_download(): failed = [r for r in results if not r['success']] print(f"⏱️ Total Time: {total_duration:.1f}s ({total_duration/60:.1f} minutes)") - print(f"✅ Success Rate: {len(successful)}/{len(results)} ({len(successful)/len(results)*100:.1f}%)") + print(f"Success Rate: {len(successful)}/{len(results)} ({len(successful)/len(results)*100:.1f}%)") print() if successful: @@ -1193,7 +1193,7 @@ def test_youtube_download(): avg_time = sum(r['duration_seconds'] for r in successful) / len(successful) print("─" * 80) - print("✅ SUCCESSFUL DOWNLOADS:") + print("SUCCESSFUL DOWNLOADS:") print("─" * 80) for i, result in enumerate(successful, 1): print(f"\n{i}. {result['artist']} - {result['track']}") @@ -1203,13 +1203,13 @@ def test_youtube_download(): print(f" Time: {result['duration_seconds']:.1f}s") print() - print(f"📦 Total Downloaded: {total_size:.2f} MB") - print(f"⚡ Average Download Time: {avg_time:.1f}s per track") + print(f"Total Downloaded: {total_size:.2f} MB") + print(f"Average Download Time: {avg_time:.1f}s per track") print() if failed: print("─" * 80) - print("❌ FAILED DOWNLOADS:") + print("FAILED DOWNLOADS:") print("─" * 80) for i, result in enumerate(failed, 1): print(f"\n{i}. {result['artist']} - {result['track']}") @@ -1219,7 +1219,7 @@ def test_youtube_download(): # File list for easy access if successful: print("─" * 80) - print("📁 DOWNLOAD LOCATION:") + print("DOWNLOAD LOCATION:") print("─" * 80) print(f"\n{yt_client.download_path.absolute()}\n") print("Files:") @@ -1228,13 +1228,13 @@ def test_youtube_download(): print() print("=" * 80) - print("🎉 Test Complete!") + print("Test Complete!") print("=" * 80) print() # Quality check reminder if successful: - print("📝 Next Steps:") + print("Next Steps:") print(" 1. Listen to downloaded files to verify quality") print(" 2. Check metadata tags in your music player") print(" 3. Compare with Soulseek downloads if available") diff --git a/ui/components/database_updater_widget.py b/ui/components/database_updater_widget.py index 63f0a51d..ad8598fd 100644 --- a/ui/components/database_updater_widget.py +++ b/ui/components/database_updater_widget.py @@ -52,7 +52,7 @@ class DatabaseUpdaterWidget(QFrame): info_label.setWordWrap(True) # Recommendation label - self.recommendation_label = QLabel("💡 Tip: Run a Full Refresh every 1-2 weeks to ensure database accuracy") + self.recommendation_label = QLabel("Tip: Run a Full Refresh every 1-2 weeks to ensure database accuracy") self.recommendation_label.setFont(QFont("Arial", 9)) self.recommendation_label.setStyleSheet("color: #ffaa00; margin-bottom: 8px; padding: 6px 8px; background: #332200; border-radius: 4px;") self.recommendation_label.setWordWrap(True) @@ -389,8 +389,8 @@ class DatabaseUpdaterWidget(QFrame): def _update_recommendation_urgency(self, urgent: bool = False): """Update the recommendation label styling based on urgency""" if urgent: - self.recommendation_label.setText("⚠️ Recommended: Run a Full Refresh - it's been over 2 weeks!") + self.recommendation_label.setText("Recommended: Run a Full Refresh - it's been over 2 weeks!") self.recommendation_label.setStyleSheet("color: #ffffff; margin-bottom: 8px; padding: 6px 8px; background: #cc3300; border-radius: 4px;") else: - self.recommendation_label.setText("💡 Tip: Run a Full Refresh every 1-2 weeks to ensure database accuracy") + self.recommendation_label.setText("Tip: Run a Full Refresh every 1-2 weeks to ensure database accuracy") self.recommendation_label.setStyleSheet("color: #ffaa00; margin-bottom: 8px; padding: 6px 8px; background: #332200; border-radius: 4px;") \ No newline at end of file diff --git a/ui/components/toast_manager.py b/ui/components/toast_manager.py index 8b636fba..0cd824bf 100644 --- a/ui/components/toast_manager.py +++ b/ui/components/toast_manager.py @@ -62,17 +62,17 @@ class Toast(QWidget): def apply_styling(self): """Apply styling based on toast type""" if self.toast_type == ToastType.SUCCESS: - icon = "✅" + icon = "" accent_color = "#1db954" # Spotify green bg_color = "rgba(29, 185, 84, 0.15)" border_color = "rgba(29, 185, 84, 0.3)" elif self.toast_type == ToastType.ERROR: - icon = "❌" + icon = "" accent_color = "#f04747" bg_color = "rgba(240, 71, 71, 0.15)" border_color = "rgba(240, 71, 71, 0.3)" elif self.toast_type == ToastType.WARNING: - icon = "⚠️" + icon = "" accent_color = "#ffa500" bg_color = "rgba(255, 165, 0, 0.15)" border_color = "rgba(255, 165, 0, 0.3)" diff --git a/ui/components/version_info_modal.py b/ui/components/version_info_modal.py index 91c17a47..c62b45e3 100644 --- a/ui/components/version_info_modal.py +++ b/ui/components/version_info_modal.py @@ -114,7 +114,7 @@ class VersionInfoModal(QDialog): # WebUI Transformation webui_section = self.create_feature_section( - "🌐 Complete WebUI Transformation", + "Complete WebUI Transformation", "SoulSync has been completely rebuilt from the ground up as a modern web application, moving from desktop GUI to web-based interface", [ "• Full transition from PyQt6 desktop application to responsive web interface", @@ -131,7 +131,7 @@ class VersionInfoModal(QDialog): # Docker Support docker_section = self.create_feature_section( - "🐳 Docker Container Support", + "Docker Container Support", "Complete containerization with Docker for easy deployment and scalability", [ "• Pre-built Docker images available for instant deployment", @@ -147,7 +147,7 @@ class VersionInfoModal(QDialog): # Enhanced Music Management music_section = self.create_feature_section( - "🎵 Enhanced Music Management", + "Enhanced Music Management", "All beloved features preserved and enhanced with new web-based capabilities", [ "• Complete Spotify, Tidal, and YouTube Music playlist synchronization", @@ -163,7 +163,7 @@ class VersionInfoModal(QDialog): # Performance & Reliability performance_section = self.create_feature_section( - "🚀 Performance & Reliability", + "Performance & Reliability", "Significant improvements in speed, stability, and resource efficiency", [ "• Asynchronous processing for improved responsiveness", @@ -234,7 +234,7 @@ class VersionInfoModal(QDialog): # Usage note if provided if usage_note: - usage_label = QLabel(f"💡 {usage_note}") + usage_label = QLabel(f"{usage_note}") usage_label.setFont(QFont("SF Pro Text", 10)) usage_label.setStyleSheet(""" color: #1ed760; diff --git a/ui/components/watchlist_status_modal.py b/ui/components/watchlist_status_modal.py index 46bc7c2b..da52f330 100644 --- a/ui/components/watchlist_status_modal.py +++ b/ui/components/watchlist_status_modal.py @@ -483,7 +483,7 @@ class WatchlistStatusModal(QDialog): # Search bar self.search_bar = QLineEdit() - self.search_bar.setPlaceholderText("🔍 Search all artists...") + self.search_bar.setPlaceholderText("Search all artists...") self.search_bar.setFixedHeight(32) self.search_bar.setStyleSheet(""" QLineEdit { @@ -675,7 +675,7 @@ class WatchlistStatusModal(QDialog): def get_artist_status_icon(self, artist): """Determine the appropriate status icon and color for an artist based on scan history""" if not artist.last_scan_timestamp: - return "⚪", "#888888" # Not scanned yet (gray circle) + return "", "#888888" # Not scanned yet (gray circle) try: from datetime import datetime, timezone @@ -693,13 +693,13 @@ class WatchlistStatusModal(QDialog): # If scanned within the last 24 hours, show as up to date # If older, show as potentially stale but still scanned if hours_ago <= 24: - return "✓", "#4caf50" # Recently up to date (bright green) + return "", "#4caf50" # Recently up to date (bright green) else: - return "✓", "#888888" # Scanned but older (gray checkmark) + return "", "#888888" # Scanned but older (gray checkmark) except Exception: # Fallback if datetime parsing fails - return "✓", "#4caf50" # Default to up to date + return "", "#4caf50" # Default to up to date def create_artist_card(self, artist: WatchlistArtist) -> QFrame: """Create a professional artist card widget""" @@ -773,7 +773,7 @@ class WatchlistStatusModal(QDialog): info_layout.addWidget(sync_label) # Delete button with modern styling - delete_button = QPushButton("✕") + delete_button = QPushButton("") delete_button.setFixedSize(28, 28) delete_button.setFont(QFont("Arial", 12, QFont.Weight.Bold)) delete_button.setStyleSheet(""" @@ -870,7 +870,7 @@ class WatchlistStatusModal(QDialog): if item and item.widget(): card = item.widget() if hasattr(card, 'status_indicator'): - card.status_indicator.setText("⚪") # Not scanned yet + card.status_indicator.setText("") # Not scanned yet card.status_indicator.setStyleSheet("color: #888888; border: none; background: transparent;") # Use shared scan worker so it persists across modal close/open @@ -945,7 +945,7 @@ class WatchlistStatusModal(QDialog): # Find artist by name (we don't have ID in signal) for artist in self.current_artists: if artist.artist_name == artist_name: - card.status_indicator.setText("🔍") # Scanning + card.status_indicator.setText("") # Scanning card.status_indicator.setStyleSheet("color: #ffc107; border: none; background: transparent;") break @@ -1018,13 +1018,13 @@ class WatchlistStatusModal(QDialog): if artist.artist_name == artist_name: if success: if new_tracks > 0: - card.status_indicator.setText("⚡") # New tracks found + card.status_indicator.setText("") # New tracks found card.status_indicator.setStyleSheet("color: #1db954; border: none; background: transparent;") else: - card.status_indicator.setText("✓") # Up to date + card.status_indicator.setText("") # Up to date card.status_indicator.setStyleSheet("color: #4caf50; border: none; background: transparent;") else: - card.status_indicator.setText("❌") # Error + card.status_indicator.setText("") # Error card.status_indicator.setStyleSheet("color: #f44336; border: none; background: transparent;") break @@ -1083,7 +1083,7 @@ class WatchlistStatusModal(QDialog): if item and item.widget(): card = item.widget() if hasattr(card, 'status_indicator'): - card.status_indicator.setText("⚪") # Not scanned yet + card.status_indicator.setText("") # Not scanned yet card.status_indicator.setStyleSheet("color: #888888; border: none; background: transparent;") def on_background_scan_completed(self, total_artists: int, total_new_tracks: int, total_added_to_wishlist: int): diff --git a/ui/pages/artists.py b/ui/pages/artists.py index abb53942..e0c6da33 100644 --- a/ui/pages/artists.py +++ b/ui/pages/artists.py @@ -76,7 +76,7 @@ class DownloadCompletionWorker(QRunnable): def run(self): """Process download completion in background thread""" try: - print(f"🧵 Background worker processing download...") + print(f"Background worker processing download...") # Add a small delay to ensure file is fully written import time @@ -89,7 +89,7 @@ class DownloadCompletionWorker(QRunnable): self.signals.completed.emit(self.download_item, organized_path or self.absolute_file_path) except Exception as e: - print(f"❌ Error in background worker: {e}") + print(f"Error in background worker: {e}") import traceback traceback.print_exc() # Emit error signal @@ -181,12 +181,12 @@ class AlbumFetchWorker(QThread): def run(self): try: - print(f"🎵 Fetching all releases (albums & singles) for artist: {self.artist.name} (ID: {self.artist.id})") + print(f"Fetching all releases (albums & singles) for artist: {self.artist.name} (ID: {self.artist.id})") # Always fetch both albums and singles from the Spotify API albums = self.spotify_client.get_artist_albums(self.artist.id, album_type='album,single', limit=50) - print(f"📀 Found {len(albums)} total releases for {self.artist.name}") + print(f"Found {len(albums)} total releases for {self.artist.name}") # Remove duplicates based on name (case insensitive) seen_names = set() @@ -200,12 +200,12 @@ class AlbumFetchWorker(QThread): # Sort by release date (newest first) unique_albums.sort(key=lambda x: x.release_date if x.release_date else '', reverse=True) - print(f"✅ Returning {len(unique_albums)} unique releases") + print(f"Returning {len(unique_albums)} unique releases") self.albums_found.emit(unique_albums, self.artist) except Exception as e: error_msg = f"Failed to fetch albums for {self.artist.name}: {str(e)}" - print(f"❌ {error_msg}") + print(f"{error_msg}") self.fetch_failed.emit(error_msg) class AlbumSearchWorker(QThread): @@ -328,7 +328,7 @@ class AlbumStatusProcessingWorker(QRunnable): if 'id' in file_data: transfers_by_id[file_data['id']] = file_data - print(f"🔍 Album status worker found {len(all_transfers)} total transfers") + print(f"Album status worker found {len(all_transfers)} total transfers") # Process each download item results = [] @@ -339,7 +339,7 @@ class AlbumStatusProcessingWorker(QRunnable): file_path = item_data.get('file_path', '') widget_id = item_data.get('widget_id') - print(f"🔍 Processing album download: ID={download_id}, file={os.path.basename(file_path)}") + print(f"Processing album download: ID={download_id}, file={os.path.basename(file_path)}") matching_transfer = None @@ -347,7 +347,7 @@ class AlbumStatusProcessingWorker(QRunnable): if download_id and download_id in transfers_by_id and download_id not in used_transfer_ids: matching_transfer = transfers_by_id[download_id] used_transfer_ids.add(download_id) - print(f" ✅ ID match found for {download_id}") + print(f" ID match found for {download_id}") # Fallback matching: by filename elif file_path: @@ -363,7 +363,7 @@ class AlbumStatusProcessingWorker(QRunnable): if transfer_basename == expected_basename: matching_transfer = transfer used_transfer_ids.add(transfer_id) - print(f" 🎯 Filename match: {expected_basename}") + print(f" Filename match: {expected_basename}") # Update download_id if it was missing if not download_id: download_id = transfer_id @@ -408,7 +408,7 @@ class AlbumStatusProcessingWorker(QRunnable): 'speed': matching_transfer.get('averageSpeed', 0) } - print(f" 📊 Status: {new_status} ({progress:.1f}%)") + print(f" Status: {new_status} ({progress:.1f}%)") else: # Download not found in API - increment missing count api_missing_count = item_data.get('api_missing_count', 0) + 1 @@ -416,11 +416,11 @@ class AlbumStatusProcessingWorker(QRunnable): if api_missing_count >= 3: # Grace period exceeded - mark as failed new_status = 'failed' - print(f" ❌ Download missing from API (failed after 3 checks)") + print(f" Download missing from API (failed after 3 checks)") else: # Still in grace period new_status = 'missing' - print(f" ⚠️ Download missing from API (attempt {api_missing_count}/3)") + print(f" Download missing from API (attempt {api_missing_count}/3)") result = { 'widget_id': widget_id, @@ -432,7 +432,7 @@ class AlbumStatusProcessingWorker(QRunnable): results.append(result) - print(f"🎯 Album status worker completed: {len(results)} results") + print(f"Album status worker completed: {len(results)} results") self.signals.completed.emit(results) finally: @@ -463,7 +463,7 @@ class SinglesEPsLibraryWorker(QThread): def run(self): try: - print("🔍 Starting track-level matching for singles and EPs...") + print("Starting track-level matching for singles and EPs...") release_statuses = {} # release_name -> AlbumOwnershipStatus # Get database instance @@ -472,13 +472,13 @@ class SinglesEPsLibraryWorker(QThread): if self._stop_requested: return - print(f"🎵 Checking {len(self.releases)} singles/EPs against database...") + print(f"Checking {len(self.releases)} singles/EPs against database...") for i, release in enumerate(self.releases): if self._stop_requested: return - print(f"🎵 Checking release {i+1}/{len(self.releases)}: {release.name} ({release.total_tracks} tracks)") + print(f"Checking release {i+1}/{len(self.releases)}: {release.name} ({release.total_tracks} tracks)") if release.total_tracks == 1: # SINGLE: Use track-level matching @@ -492,12 +492,12 @@ class SinglesEPsLibraryWorker(QThread): # Emit individual match for real-time UI update self.release_matched.emit(release.name, status) - print(f"🎯 Singles/EPs check complete: {len(release_statuses)} releases processed") + print(f"Singles/EPs check complete: {len(release_statuses)} releases processed") self.check_completed.emit(release_statuses) except Exception as e: error_msg = f"Error checking singles/EPs library: {e}" - print(f"❌ {error_msg}") + print(f"{error_msg}") import traceback traceback.print_exc() self.check_failed.emit(error_msg) @@ -535,14 +535,14 @@ class SinglesEPsLibraryWorker(QThread): track_name = single_release.name artist_name = single_release.artists[0] if single_release.artists else "" except Exception as e: - print(f" 🔍 Debug single track fetch error: {e}") + print(f" Debug single track fetch error: {e}") if album_data and 'tracks' in album_data: - print(f" 🔍 Debug: tracks data type = {type(album_data['tracks'])}") + print(f" Debug: tracks data type = {type(album_data['tracks'])}") # Fallback if Spotify call fails track_name = single_release.name artist_name = single_release.artists[0] if single_release.artists else "" - print(f" 🔍 Searching for single track: '{track_name}' by '{artist_name}'") + print(f" Searching for single track: '{track_name}' by '{artist_name}'") # Search for the track anywhere in the library (active server only) from config.settings import config_manager @@ -550,7 +550,7 @@ class SinglesEPsLibraryWorker(QThread): db_track, confidence = db.check_track_exists(track_name, artist_name, confidence_threshold=0.7, server_source=active_server) if db_track and confidence >= 0.7: - print(f" ✅ Single found: '{track_name}' in album '{db_track.album_title}' (confidence: {confidence:.2f})") + print(f" Single found: '{track_name}' in album '{db_track.album_title}' (confidence: {confidence:.2f})") # For singles, if we find the track, it's "complete" return AlbumOwnershipStatus( @@ -563,7 +563,7 @@ class SinglesEPsLibraryWorker(QThread): completion_ratio=1.0 ) else: - print(f" ❌ Single not found: '{track_name}'") + print(f" Single not found: '{track_name}'") return AlbumOwnershipStatus( album_name=single_release.name, is_owned=False, @@ -575,7 +575,7 @@ class SinglesEPsLibraryWorker(QThread): ) except Exception as e: - print(f" ❌ Error checking single '{single_release.name}': {e}") + print(f" Error checking single '{single_release.name}': {e}") return AlbumOwnershipStatus( album_name=single_release.name, is_owned=False, @@ -594,7 +594,7 @@ class SinglesEPsLibraryWorker(QThread): spotify_client = SpotifyClient() if not spotify_client.is_authenticated(): - print(f" ⚠️ Spotify not available, cannot check EP tracks for '{ep_release.name}'") + print(f" Spotify not available, cannot check EP tracks for '{ep_release.name}'") return AlbumOwnershipStatus( album_name=ep_release.name, is_owned=False, @@ -620,11 +620,11 @@ class SinglesEPsLibraryWorker(QThread): raise Exception(f"Unexpected tracks data format: {type(tracks_data)}") except Exception as e: - print(f" ⚠️ Could not fetch EP tracks for '{ep_release.name}': {e}") + print(f" Could not fetch EP tracks for '{ep_release.name}': {e}") if album_data and 'tracks' in album_data: - print(f" 🔍 Debug: tracks data type = {type(album_data['tracks'])}") + print(f" Debug: tracks data type = {type(album_data['tracks'])}") if hasattr(album_data['tracks'], '__len__') and len(album_data['tracks']) > 0: - print(f" 🔍 Debug: first item type = {type(album_data['tracks'][0])}") + print(f" Debug: first item type = {type(album_data['tracks'][0])}") return AlbumOwnershipStatus( album_name=ep_release.name, is_owned=False, @@ -635,7 +635,7 @@ class SinglesEPsLibraryWorker(QThread): completion_ratio=0.0 ) - print(f" 🔍 Checking {len(tracks)} tracks in EP '{ep_release.name}'") + print(f" Checking {len(tracks)} tracks in EP '{ep_release.name}'") owned_tracks = 0 expected_tracks = len(tracks) @@ -654,16 +654,16 @@ class SinglesEPsLibraryWorker(QThread): if db_track and confidence >= 0.7: owned_tracks += 1 - print(f" ✅ Track found: '{track_name}'") + print(f" Track found: '{track_name}'") else: - print(f" ❌ Track missing: '{track_name}'") + print(f" Track missing: '{track_name}'") completion_ratio = owned_tracks / max(expected_tracks, 1) is_complete = completion_ratio >= 0.9 is_nearly_complete = completion_ratio >= 0.8 and completion_ratio < 0.9 is_owned = owned_tracks > 0 - print(f" 📊 EP '{ep_release.name}': {owned_tracks}/{expected_tracks} tracks ({int(completion_ratio * 100)}%)") + print(f" EP '{ep_release.name}': {owned_tracks}/{expected_tracks} tracks ({int(completion_ratio * 100)}%)") return AlbumOwnershipStatus( album_name=ep_release.name, @@ -676,7 +676,7 @@ class SinglesEPsLibraryWorker(QThread): ) except Exception as e: - print(f" ❌ Error checking EP '{ep_release.name}': {e}") + print(f" Error checking EP '{ep_release.name}': {e}") return AlbumOwnershipStatus( album_name=ep_release.name, is_owned=False, @@ -705,7 +705,7 @@ class DatabaseLibraryWorker(QThread): def run(self): try: - print("🔍 Starting robust database album matching with completeness checking...") + print("Starting robust database album matching with completeness checking...") album_statuses = {} # album_name -> AlbumOwnershipStatus # Get database instance @@ -715,22 +715,22 @@ class DatabaseLibraryWorker(QThread): try: from config.settings import config_manager active_server = config_manager.get_active_media_server() - print(f"🔍 Checking albums against {active_server.upper()} library only") + print(f"Checking albums against {active_server.upper()} library only") except Exception as e: - print(f"⚠️ Could not get active server, defaulting to 'plex': {e}") + print(f"Could not get active server, defaulting to 'plex': {e}") active_server = 'plex' if self._stop_requested: return - print(f"📚 Checking {len(self.albums)} Spotify albums against local database...") + print(f"Checking {len(self.albums)} Spotify albums against local database...") # Use robust matching for each album for i, spotify_album in enumerate(self.albums): if self._stop_requested: return - print(f"🎵 Checking album {i+1}/{len(self.albums)}: {spotify_album.name}") + print(f"Checking album {i+1}/{len(self.albums)}: {spotify_album.name}") # Create multiple search variations album_variations = [] @@ -768,7 +768,7 @@ class DatabaseLibraryWorker(QThread): return # Search database for this combination with completeness info - print(f" 🔍 Searching database: album='{album_name}', artist='{artist_clean}'") + print(f" Searching database: album='{album_name}', artist='{artist_clean}'") db_album, confidence, owned_tracks, expected_tracks, is_complete, *_ = db.check_album_exists_with_completeness( album_name, artist_clean, expected_track_count, confidence_threshold=0.7, server_source=active_server ) @@ -779,7 +779,7 @@ class DatabaseLibraryWorker(QThread): best_owned_tracks = owned_tracks best_expected_tracks = expected_tracks best_is_complete = is_complete - print(f" 📀 Found database match with confidence {confidence:.2f} ({owned_tracks}/{expected_tracks} tracks)") + print(f" Found database match with confidence {confidence:.2f} ({owned_tracks}/{expected_tracks} tracks)") # If we have a very confident match, we can stop searching for this album if confidence >= 0.95: @@ -787,7 +787,7 @@ class DatabaseLibraryWorker(QThread): # Backup search with original uncleaned artist name if not db_album and artist and artist != artist_clean: - print(f" 🔄 Backup search with original artist: album='{album_name}', artist='{artist}'") + print(f" Backup search with original artist: album='{album_name}', artist='{artist}'") db_album_backup, confidence_backup, owned_backup, expected_backup, complete_backup, *_ = db.check_album_exists_with_completeness( album_name, artist, expected_track_count, confidence_threshold=0.7, server_source=active_server ) @@ -798,13 +798,13 @@ class DatabaseLibraryWorker(QThread): best_owned_tracks = owned_backup best_expected_tracks = expected_backup best_is_complete = complete_backup - print(f" 📀 Found backup match with confidence {confidence_backup:.2f} ({owned_backup}/{expected_backup} tracks)") + print(f" Found backup match with confidence {confidence_backup:.2f} ({owned_backup}/{expected_backup} tracks)") # Additional fallback: remove commas if not db_album_backup and ',' in artist: artist_no_comma = artist.replace(',', '').strip() artist_no_comma = ' '.join(artist_no_comma.split()) - print(f" 🔄 Comma-removal fallback: album='{album_name}', artist='{artist_no_comma}'") + print(f" Comma-removal fallback: album='{album_name}', artist='{artist_no_comma}'") db_album_comma, confidence_comma, owned_comma, expected_comma, complete_comma, *_ = db.check_album_exists_with_completeness( album_name, artist_no_comma, expected_track_count, confidence_threshold=0.7, server_source=active_server ) @@ -815,7 +815,7 @@ class DatabaseLibraryWorker(QThread): best_owned_tracks = owned_comma best_expected_tracks = expected_comma best_is_complete = complete_comma - print(f" 📀 Found comma-removal match with confidence {confidence_comma:.2f} ({owned_comma}/{expected_comma} tracks)") + print(f" Found comma-removal match with confidence {confidence_comma:.2f} ({owned_comma}/{expected_comma} tracks)") # If we found a very confident match, stop searching other artists if best_confidence >= 0.95: @@ -838,11 +838,11 @@ class DatabaseLibraryWorker(QThread): # Log detailed result if best_is_complete: - print(f"✅ Complete album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") + print(f"Complete album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") elif is_nearly_complete: - print(f"🔵 Nearly complete album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") + print(f"Nearly complete album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") else: - print(f"⚠️ Partial album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") + print(f"Partial album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") # Emit individual match for real-time UI update self.album_matched.emit(spotify_album.name, status) @@ -860,9 +860,9 @@ class DatabaseLibraryWorker(QThread): album_statuses[spotify_album.name] = status if best_album: - print(f"❌ No confident match for '{spotify_album.name}' (best: {best_confidence:.2f})") + print(f"No confident match for '{spotify_album.name}' (best: {best_confidence:.2f})") else: - print(f"❌ No database candidates found for '{spotify_album.name}'") + print(f"No database candidates found for '{spotify_album.name}'") # Count results for summary complete_count = sum(1 for status in album_statuses.values() if status.is_complete) @@ -870,14 +870,14 @@ class DatabaseLibraryWorker(QThread): partial_count = sum(1 for status in album_statuses.values() if status.is_owned and not status.is_complete and not status.is_nearly_complete) missing_count = sum(1 for status in album_statuses.values() if not status.is_owned) - print(f"🎯 Final result: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {len(self.albums)} albums") - print(f"🚀 Emitting detailed album statuses") + print(f"Final result: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {len(self.albums)} albums") + print(f"Emitting detailed album statuses") self.library_checked.emit(album_statuses) except Exception as e: if not self._stop_requested: error_msg = f"Error checking database library: {e}" - print(f"❌ {error_msg}") + print(f"{error_msg}") self.check_failed.emit(error_msg) @@ -1232,7 +1232,7 @@ class ArtistResultCard(QFrame): font-size: 48px; } """) - self.image_label.setText("🎵") + self.image_label.setText("") image_layout.addWidget(self.image_label) @@ -1258,7 +1258,7 @@ class ArtistResultCard(QFrame): # Watchlist eye indicator (positioned absolutely in top-right corner) self.watchlist_indicator = QLabel(self) - self.watchlist_indicator.setText("👁️") + self.watchlist_indicator.setText("") self.watchlist_indicator.setFont(QFont("Arial", 14)) self.watchlist_indicator.setFixedSize(24, 24) self.watchlist_indicator.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -1348,7 +1348,7 @@ class ArtistResultCard(QFrame): super().mousePressEvent(event) except RuntimeError as e: # Qt object has been deleted, ignore the event silently - print(f"⚠️ ArtistCard object deleted during mouse event: {e}") + print(f"ArtistCard object deleted during mouse event: {e}") pass class AlbumCard(QFrame): @@ -1413,7 +1413,7 @@ class AlbumCard(QFrame): font-size: 32px; } """) - self.image_label.setText("💿") + self.image_label.setText("") image_layout.addWidget(self.image_label) @@ -1533,7 +1533,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.overlay.setText("✓ Complete\nVerify tracks") + self.overlay.setText("Complete\nVerify tracks") self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) elif self.ownership_status and self.ownership_status.is_nearly_complete: # Nearly complete album (80-89%) - blue overlay @@ -1563,7 +1563,7 @@ class AlbumCard(QFrame): """) percentage = int(self.ownership_status.completion_ratio * 100) missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks - self.overlay.setText(f"⚠ Partial\n({percentage}%)\nGet {missing_tracks} missing") + self.overlay.setText(f"Partial\n({percentage}%)\nGet {missing_tracks} missing") self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) else: # Legacy complete album - green checkmark overlay @@ -1576,7 +1576,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.overlay.setText("✓ Complete\nVerify tracks") + self.overlay.setText("Complete\nVerify tracks") self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) else: # Missing album - download overlay @@ -1589,7 +1589,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.overlay.setText("📥 Missing\n(0%)\nDownload") + self.overlay.setText("Missing\n(0%)\nDownload") self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) def update_status_indicator(self): @@ -1606,7 +1606,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.status_indicator.setText("✓") + self.status_indicator.setText("") self.status_indicator.setToolTip(f"Complete album - {self.ownership_status.owned_tracks}/{self.ownership_status.expected_tracks} tracks ({int(self.ownership_status.completion_ratio * 100)}%)") elif self.ownership_status and self.ownership_status.is_nearly_complete: # Nearly complete album (80-89%) - blue half-circle @@ -1634,7 +1634,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.status_indicator.setText("⚠") + self.status_indicator.setText("") percentage = int(self.ownership_status.completion_ratio * 100) self.status_indicator.setToolTip(f"Partial album - {self.ownership_status.owned_tracks}/{self.ownership_status.expected_tracks} tracks ({percentage}%)") else: @@ -1648,7 +1648,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.status_indicator.setText("✓") + self.status_indicator.setText("") self.status_indicator.setToolTip("Album owned in library") else: # Missing album - red download icon @@ -1661,7 +1661,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.status_indicator.setText("📥") + self.status_indicator.setText("") self.status_indicator.setToolTip("Album available for download") def update_ownership(self, ownership_info): @@ -1677,9 +1677,9 @@ class AlbumCard(QFrame): if self.is_owned != is_owned: # Only log if status actually changed if self.ownership_status: - print(f"🔄 '{self.album.name}' ownership: {self.is_owned} -> {is_owned} (complete: {self.ownership_status.is_complete})") + print(f"'{self.album.name}' ownership: {self.is_owned} -> {is_owned} (complete: {self.ownership_status.is_complete})") else: - print(f"🔄 '{self.album.name}' ownership: {self.is_owned} -> {is_owned}") + print(f"'{self.album.name}' ownership: {self.is_owned} -> {is_owned}") self.is_owned = is_owned @@ -1701,7 +1701,7 @@ class AlbumCard(QFrame): try: if hasattr(self, 'progress_overlay') and self.progress_overlay and not self.progress_overlay.isNull(): - self.progress_overlay.setText("⏳\nPreparing...") + self.progress_overlay.setText("\nPreparing...") self.progress_overlay.show() except (RuntimeError, AttributeError): # Object has been deleted or is invalid, skip @@ -1719,7 +1719,7 @@ class AlbumCard(QFrame): font-weight: bold; } """) - self.status_indicator.setText("⏳") + self.status_indicator.setText("") self.status_indicator.setToolTip("Album downloading...") except (RuntimeError, AttributeError): # Object has been deleted or is invalid, skip @@ -1728,7 +1728,7 @@ class AlbumCard(QFrame): def update_download_progress(self, completed_tracks: int, total_tracks: int, percentage: int): """Update download progress display""" try: - progress_text = f"📥 Downloading\n{completed_tracks}/{total_tracks} tracks\n{percentage}%" + progress_text = f"Downloading\n{completed_tracks}/{total_tracks} tracks\n{percentage}%" if hasattr(self, 'progress_overlay') and self.progress_overlay: self.progress_overlay.setText(progress_text) self.progress_overlay.show() @@ -1762,7 +1762,7 @@ class AlbumCard(QFrame): # Show completion message briefly if overlay still exists if hasattr(self, 'progress_overlay') and self.progress_overlay is not None: try: - self.progress_overlay.setText("✅\nCompleted!") + self.progress_overlay.setText("\nCompleted!") self.progress_overlay.setStyleSheet(""" QLabel { background: rgba(29, 185, 84, 0.9); @@ -1782,7 +1782,7 @@ class AlbumCard(QFrame): pass except Exception as e: - print(f"⚠️ Error in set_download_completed: {e}") + print(f"Error in set_download_completed: {e}") # Still try to update ownership even if overlay fails try: self.update_ownership(True) @@ -1804,12 +1804,12 @@ class AlbumCard(QFrame): # Don't allow downloads if already downloading if (event.button() == Qt.MouseButton.LeftButton and not self.progress_overlay.isVisible()): - print(f"🖱️ Album card clicked: {self.album.name} (owned: {self.is_owned})") + print(f"Album card clicked: {self.album.name} (owned: {self.is_owned})") self.download_requested.emit(self.album) super().mousePressEvent(event) except RuntimeError as e: # Qt object has been deleted, ignore the event silently - print(f"⚠️ AlbumCard object deleted during mouse event: {e}") + print(f"AlbumCard object deleted during mouse event: {e}") pass class DownloadMissingAlbumTracksModal(QDialog): @@ -1841,7 +1841,7 @@ class DownloadMissingAlbumTracksModal(QDialog): self.permanently_failed_tracks = [] self.cancelled_tracks = set() # Track indices of cancelled tracks - print(f"📊 Total album tracks: {self.total_tracks}") + print(f"Total album tracks: {self.total_tracks}") # Track analysis results self.analysis_results = [] @@ -1862,9 +1862,9 @@ class DownloadMissingAlbumTracksModal(QDialog): self.active_downloads = [] - print("🎨 Setting up album modal UI...") + print("Setting up album modal UI...") self.setup_ui() - print("✅ Album modal initialization complete") + print("Album modal initialization complete") def generate_smart_search_queries(self, artist_name, track_name): """Generate smart search query variations with album-in-title detection""" @@ -1912,7 +1912,7 @@ class DownloadMissingAlbumTracksModal(QDialog): unique_queries.append(query) seen.add(query.lower()) - print(f"🧠 Generated {len(unique_queries)} smart queries for '{track_name}' (enhanced with album detection)") + print(f"Generated {len(unique_queries)} smart queries for '{track_name}' (enhanced with album detection)") for i, query in enumerate(unique_queries): print(f" {i+1}. '{query}'") @@ -1983,10 +1983,10 @@ class DownloadMissingAlbumTracksModal(QDialog): dashboard_layout = QHBoxLayout() dashboard_layout.setSpacing(20) - self.total_card = self.create_compact_counter_card("📀 Total", str(self.total_tracks), "#1db954") - self.matched_card = self.create_compact_counter_card("✅ Found", "0", "#4CAF50") + self.total_card = self.create_compact_counter_card("Total", str(self.total_tracks), "#1db954") + self.matched_card = self.create_compact_counter_card("Found", "0", "#4CAF50") self.download_card = self.create_compact_counter_card("⬇️ Missing", "0", "#ff6b6b") - self.downloaded_card = self.create_compact_counter_card("✅ Downloaded", "0", "#4CAF50") + self.downloaded_card = self.create_compact_counter_card("Downloaded", "0", "#4CAF50") dashboard_layout.addWidget(self.total_card) dashboard_layout.addWidget(self.matched_card) @@ -2051,7 +2051,7 @@ class DownloadMissingAlbumTracksModal(QDialog): analysis_container = QVBoxLayout() analysis_container.setSpacing(4) - analysis_label = QLabel("🔍 Plex Analysis") + analysis_label = QLabel("Plex Analysis") analysis_label.setFont(QFont("Arial", 11, QFont.Weight.Bold)) analysis_label.setStyleSheet("color: #cccccc;") @@ -2109,7 +2109,7 @@ class DownloadMissingAlbumTracksModal(QDialog): layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) - header_label = QLabel("📋 Album Track Analysis") + header_label = QLabel("Album Track Analysis") header_label.setFont(QFont("Arial", 13, QFont.Weight.Bold)) header_label.setStyleSheet("color: #ffffff; padding: 5px;") @@ -2158,7 +2158,7 @@ class DownloadMissingAlbumTracksModal(QDialog): if self.is_valid_track(track): valid_tracks.append(track) else: - print(f"⚠️ Skipping invalid track: name='{getattr(track, 'name', 'None')}', artists={getattr(track, 'artists', 'None')}, duration={getattr(track, 'duration_ms', 'None')}") + print(f"Skipping invalid track: name='{getattr(track, 'name', 'None')}', artists={getattr(track, 'artists', 'None')}, duration={getattr(track, 'duration_ms', 'None')}") # Update album tracks to only include valid ones self.album.tracks = valid_tracks @@ -2177,7 +2177,7 @@ class DownloadMissingAlbumTracksModal(QDialog): duration_item = QTableWidgetItem(duration) duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.track_table.setItem(i, 2, duration_item) - matched_item = QTableWidgetItem("⏳ Pending") + matched_item = QTableWidgetItem("Pending") matched_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.track_table.setItem(i, 3, matched_item) status_item = QTableWidgetItem("—") @@ -2287,10 +2287,10 @@ class DownloadMissingAlbumTracksModal(QDialog): cancel_button = layout.itemAt(0).widget() if cancel_button: cancel_button.setEnabled(False) - cancel_button.setText("✓") + cancel_button.setText("") # Update status to cancelled - self.track_table.setItem(row, 4, QTableWidgetItem("🚫 Cancelled")) + self.track_table.setItem(row, 4, QTableWidgetItem("Cancelled")) # Add to cancelled tracks set if not hasattr(self, 'cancelled_tracks'): @@ -2298,7 +2298,7 @@ class DownloadMissingAlbumTracksModal(QDialog): self.cancelled_tracks.add(row) track = self.album.tracks[row] - print(f"🚫 Track cancelled: {track.name} (row {row})") + print(f"Track cancelled: {track.name} (row {row})") # If downloads are active, also handle active download cancellation download_index = None @@ -2308,7 +2308,7 @@ class DownloadMissingAlbumTracksModal(QDialog): for download in self.active_downloads: if download.get('table_index') == row: download_index = download.get('download_index', row) - print(f"🚫 Found active download {download_index} for cancelled track") + print(f"Found active download {download_index} for cancelled track") break # Check parallel_search_tracking for download index @@ -2316,12 +2316,12 @@ class DownloadMissingAlbumTracksModal(QDialog): for idx, track_info in self.parallel_search_tracking.items(): if track_info.get('table_index') == row: download_index = idx - print(f"🚫 Found parallel tracking {download_index} for cancelled track") + print(f"Found parallel tracking {download_index} for cancelled track") break # If we found an active download, trigger completion to free up the worker if download_index is not None and hasattr(self, 'on_parallel_track_completed'): - print(f"🚫 Triggering completion for active download {download_index}") + print(f"Triggering completion for active download {download_index}") self.on_parallel_track_completed(download_index, success=False) def create_buttons(self): @@ -2331,7 +2331,7 @@ class DownloadMissingAlbumTracksModal(QDialog): layout.setSpacing(15) layout.setContentsMargins(0, 10, 0, 0) - self.correct_failed_btn = QPushButton("🔧 Correct Failed Matches") + self.correct_failed_btn = QPushButton("Correct Failed Matches") self.correct_failed_btn.setFixedWidth(220) self.correct_failed_btn.setStyleSheet(""" QPushButton { background-color: #ffc107; color: #000000; border-radius: 20px; font-weight: bold; } @@ -2384,7 +2384,7 @@ class DownloadMissingAlbumTracksModal(QDialog): self.album_card.set_download_in_progress() except (RuntimeError, AttributeError): # Album card object has been deleted, skip UI update - print("⚠️ Album card object deleted, skipping progress update") + print("Album card object deleted, skipping progress update") pass self.begin_search_btn.hide() @@ -2407,18 +2407,18 @@ class DownloadMissingAlbumTracksModal(QDialog): QThreadPool.globalInstance().start(worker) def on_analysis_started(self, total_tracks): - print(f"🔍 Album analysis started for {total_tracks} tracks") + print(f"Album analysis started for {total_tracks} tracks") def on_track_analyzed(self, track_index, result): """Handle individual track analysis completion with live UI updates""" self.analysis_progress.setValue(track_index) row_index = track_index - 1 if result.exists_in_plex: - matched_text = f"✅ Found ({result.confidence:.1f})" + matched_text = f"Found ({result.confidence:.1f})" self.matched_tracks_count += 1 self.matched_count_label.setText(str(self.matched_tracks_count)) else: - matched_text = "❌ Missing" + matched_text = "Missing" self.tracks_to_download_count += 1 self.download_count_label.setText(str(self.tracks_to_download_count)) # Add cancel button for missing tracks only @@ -2430,7 +2430,7 @@ class DownloadMissingAlbumTracksModal(QDialog): self.analysis_complete = True self.analysis_results = results self.missing_tracks = [r for r in results if not r.exists_in_plex] - print(f"✅ Album analysis complete: {len(self.missing_tracks)} to download") + print(f"Album analysis complete: {len(self.missing_tracks)} to download") if self.missing_tracks: self.start_download_progress() else: @@ -2439,13 +2439,13 @@ class DownloadMissingAlbumTracksModal(QDialog): try: self.process_finished.emit() except RuntimeError as e: - print(f"⚠️ Modal object deleted during analysis complete signal: {e}") + print(f"Modal object deleted during analysis complete signal: {e}") QMessageBox.information(self, "Analysis Complete", "All album tracks already exist in Plex! No downloads needed.") # Close with accept since all tracks are already available (success case) self.accept() def on_analysis_failed(self, error_message): - print(f"❌ Album analysis failed: {error_message}") + print(f"Album analysis failed: {error_message}") QMessageBox.critical(self, "Analysis Failed", f"Failed to analyze album tracks: {error_message}") self.cancel_btn.hide() self.begin_search_btn.show() @@ -2476,12 +2476,12 @@ class DownloadMissingAlbumTracksModal(QDialog): # Skip if track was cancelled if hasattr(self, 'cancelled_tracks') and track_index in self.cancelled_tracks: - print(f"🚫 Skipping cancelled track at index {track_index}: {track.name}") + print(f"Skipping cancelled track at index {track_index}: {track.name}") self.download_queue_index += 1 self.completed_downloads += 1 continue - self.track_table.setItem(track_index, 4, QTableWidgetItem("🔍 Searching...")) + self.track_table.setItem(track_index, 4, QTableWidgetItem("Searching...")) self.search_and_download_track_parallel(track, self.download_queue_index, track_index) self.active_parallel_downloads += 1 self.download_queue_index += 1 @@ -2549,7 +2549,7 @@ class DownloadMissingAlbumTracksModal(QDialog): # Reset state if this track was previously marked as completed (for retries) if track_info.get('completed', False): - print(f"🔄 Resetting state for manually retried track (index: {download_index}).") + print(f"Resetting state for manually retried track (index: {download_index}).") track_info['completed'] = False if self.failed_downloads > 0: @@ -2596,7 +2596,7 @@ class DownloadMissingAlbumTracksModal(QDialog): def cancel_existing_download(self, download_item): """Cancel an existing download item""" if download_item and hasattr(download_item, 'cancel_download'): - print(f"🚫 Cancelling existing queued download: '{download_item.title}' by {download_item.artist}") + print(f"Cancelling existing queued download: '{download_item.title}' by {download_item.artist}") download_item.cancel_download() return True return False @@ -2607,7 +2607,7 @@ class DownloadMissingAlbumTracksModal(QDialog): # Check for existing download and cancel if found existing_download = self.find_existing_download_for_track(spotify_based_result) if existing_download: - print(f"⚠️ Found existing download for '{spotify_based_result.title}', canceling before retry...") + print(f"Found existing download for '{spotify_based_result.title}', canceling before retry...") self.cancel_existing_download(existing_download) artist = type('Artist', (), {'name': spotify_based_result.artist})() @@ -2706,7 +2706,7 @@ class DownloadMissingAlbumTracksModal(QDialog): download_info['downloading_start_time'] = time.time() # 90-second timeout for being stuck at 0% elif time.time() - download_info['downloading_start_time'] > 90: - print(f"⚠️ Download for '{download_info['slskd_result'].filename}' is stuck at 0%. Cancelling and retrying.") + print(f"Download for '{download_info['slskd_result'].filename}' is stuck at 0%. Cancelling and retrying.") # Cancel the old download before retry self.cancel_download_before_retry(download_info) if download_info in self.active_downloads: @@ -2723,7 +2723,7 @@ class DownloadMissingAlbumTracksModal(QDialog): if 'queued_start_time' not in download_info: download_info['queued_start_time'] = time.time() elif time.time() - download_info['queued_start_time'] > 90: # 90-second timeout - print(f"⚠️ Download for '{download_info['slskd_result'].filename}' is stuck in queue. Cancelling and retrying.") + print(f"Download for '{download_info['slskd_result'].filename}' is stuck in queue. Cancelling and retrying.") # Cancel the old download before retry self.cancel_download_before_retry(download_info) if download_info in self.active_downloads: @@ -2737,7 +2737,7 @@ class DownloadMissingAlbumTracksModal(QDialog): try: slskd_result = download_info.get('slskd_result') if not slskd_result: - print("⚠️ No slskd_result found in download_info for cancellation") + print("No slskd_result found in download_info for cancellation") return # Extract download details for cancellation @@ -2745,7 +2745,7 @@ class DownloadMissingAlbumTracksModal(QDialog): username = getattr(slskd_result, 'username', None) if download_id and username: - print(f"🚫 Cancelling timed-out album download: {download_id} from {username}") + print(f"Cancelling timed-out album download: {download_id} from {username}") # Use asyncio to call the async cancel method import asyncio @@ -2756,16 +2756,16 @@ class DownloadMissingAlbumTracksModal(QDialog): self.soulseek_client.cancel_download(download_id, username, remove=False) ) if success: - print(f"✅ Successfully cancelled album download {download_id}") + print(f"Successfully cancelled album download {download_id}") else: - print(f"⚠️ Failed to cancel album download {download_id}") + print(f"Failed to cancel album download {download_id}") finally: loop.close() else: - print(f"⚠️ Missing download_id ({download_id}) or username ({username}) for album cancellation") + print(f"Missing download_id ({download_id}) or username ({username}) for album cancellation") except Exception as e: - print(f"❌ Error cancelling album download: {e}") + print(f"Error cancelling album download: {e}") def retry_parallel_download_with_fallback(self, failed_download_info): """Retries a failed download by selecting the next-best cached candidate""" @@ -2791,8 +2791,8 @@ class DownloadMissingAlbumTracksModal(QDialog): self.on_parallel_track_failed(download_index, "No alternative sources in cache") return - print(f"🔄 Retrying album download {download_index + 1} with next candidate: {next_candidate.filename}") - self.track_table.setItem(failed_download_info['table_index'], 4, QTableWidgetItem(f"🔄 Retrying ({track_info['retry_count']})...")) + print(f"Retrying album download {download_index + 1} with next candidate: {next_candidate.filename}") + self.track_table.setItem(failed_download_info['table_index'], 4, QTableWidgetItem(f"Retrying ({track_info['retry_count']})...")) self.start_validated_download_parallel( next_candidate, track_info['spotify_track'], track_info['track_index'], @@ -2802,14 +2802,14 @@ class DownloadMissingAlbumTracksModal(QDialog): def on_parallel_track_completed(self, download_index, success): """Handle completion of a parallel track download""" if not hasattr(self, 'parallel_search_tracking'): - print(f"⚠️ parallel_search_tracking not initialized yet, skipping completion for download {download_index}") + print(f"parallel_search_tracking not initialized yet, skipping completion for download {download_index}") return track_info = self.parallel_search_tracking.get(download_index) if not track_info or track_info.get('completed', False): return track_info['completed'] = True if success: - self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("✅ Downloaded")) + self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("Downloaded")) # Hide cancel button since track is now downloaded self.hide_cancel_button_for_row(track_info['table_index']) self.downloaded_tracks_count += 1 @@ -2819,10 +2819,10 @@ class DownloadMissingAlbumTracksModal(QDialog): # Check if track was cancelled (don't overwrite cancelled status) table_index = track_info['table_index'] current_status = self.track_table.item(table_index, 4) - if current_status and "🚫 Cancelled" in current_status.text(): - print(f"🔧 Track {download_index} was cancelled - preserving cancelled status") + if current_status and "Cancelled" in current_status.text(): + print(f"Track {download_index} was cancelled - preserving cancelled status") else: - self.track_table.setItem(table_index, 4, QTableWidgetItem("❌ Failed")) + self.track_table.setItem(table_index, 4, QTableWidgetItem("Failed")) if track_info not in self.permanently_failed_tracks: self.permanently_failed_tracks.append(track_info) self.update_failed_matches_button() @@ -2838,14 +2838,14 @@ class DownloadMissingAlbumTracksModal(QDialog): def on_parallel_track_failed(self, download_index, reason): """Handle failure of a parallel track download""" - print(f"❌ Album parallel download {download_index + 1} failed: {reason}") + print(f"Album parallel download {download_index + 1} failed: {reason}") self.on_parallel_track_completed(download_index, False) def update_failed_matches_button(self): """Shows, hides, and updates the counter on the 'Correct Failed Matches' button""" count = len(self.permanently_failed_tracks) if count > 0: - self.correct_failed_btn.setText(f"🔧 Correct {count} Failed Match{'es' if count > 1 else ''}") + self.correct_failed_btn.setText(f"Correct {count} Failed Match{'es' if count > 1 else ''}") self.correct_failed_btn.show() else: self.correct_failed_btn.hide() @@ -2860,14 +2860,14 @@ class DownloadMissingAlbumTracksModal(QDialog): def on_all_downloads_complete(self): """Handle completion of all downloads""" self.download_in_progress = False - print("🎉 All album downloads completed!") + print("All album downloads completed!") self.cancel_btn.hide() # Emit process_finished signal to unlock UI try: self.process_finished.emit() except RuntimeError as e: - print(f"⚠️ Modal object deleted during downloads complete signal: {e}") + print(f"Modal object deleted during downloads complete signal: {e}") # Request Plex library scan if we have successful downloads if self.successful_downloads > 0 and hasattr(self, 'parent_artists_page') and self.parent_artists_page.scan_manager: @@ -2893,8 +2893,8 @@ class DownloadMissingAlbumTracksModal(QDialog): status_item = self.track_table.item(cancelled_row, 4) current_status = status_item.text() if status_item else "" - if "✅ Downloaded" in current_status: - print(f"🚫 Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist addition") + if "Downloaded" in current_status: + print(f"Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist addition") else: cancelled_track_info = { 'download_index': cancelled_row, @@ -2908,9 +2908,9 @@ class DownloadMissingAlbumTracksModal(QDialog): # Check if not already in permanently_failed_tracks if not any(t.get('table_index') == cancelled_row for t in self.permanently_failed_tracks): self.permanently_failed_tracks.append(cancelled_track_info) - print(f"🚫 Added cancelled missing track {cancelled_track.name} to failed list for wishlist") + print(f"Added cancelled missing track {cancelled_track.name} to failed list for wishlist") else: - print(f"🚫 Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist addition") + print(f"Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist addition") # Add permanently failed tracks to wishlist before showing completion message failed_count = len(self.permanently_failed_tracks) @@ -2981,7 +2981,7 @@ class DownloadMissingAlbumTracksModal(QDialog): final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing album tracks!\n\n" if wishlist_added_count > 0: - final_message += f"✨ Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n" + final_message += f"Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n" final_message += "You can also manually correct failed downloads or check the wishlist on the dashboard." @@ -3012,10 +3012,10 @@ class DownloadMissingAlbumTracksModal(QDialog): initial_candidates = self.matching_engine.find_best_slskd_matches_enhanced(spotify_track, results) if not initial_candidates: - print(f"⚠️ No initial candidates found for '{spotify_track.name}' from query '{query}'.") + print(f"No initial candidates found for '{spotify_track.name}' from query '{query}'.") return [] - print(f"✅ Found {len(initial_candidates)} initial candidates for '{spotify_track.name}'. Now verifying artist...") + print(f"Found {len(initial_candidates)} initial candidates for '{spotify_track.name}'. Now verifying artist...") # Perform strict artist verification on the initial candidates verified_candidates = [] @@ -3033,10 +3033,10 @@ class DownloadMissingAlbumTracksModal(QDialog): # Check if the cleaned artist's name is in the cleaned folder path if normalized_spotify_artist in normalized_slskd_path: - print(f"✔️ Artist '{spotify_artist_name}' VERIFIED in path: '{slskd_full_path}'") + print(f"Artist '{spotify_artist_name}' VERIFIED in path: '{slskd_full_path}'") verified_candidates.append(candidate) else: - print(f"❌ Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.") + print(f"Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.") if verified_candidates: # Apply quality profile filtering before returning @@ -3047,14 +3047,14 @@ class DownloadMissingAlbumTracksModal(QDialog): if quality_filtered: verified_candidates = quality_filtered - print(f"🎯 Applied quality profile filtering: {len(verified_candidates)} candidates remain") + print(f"Applied quality profile filtering: {len(verified_candidates)} candidates remain") else: - print(f"⚠️ Quality profile filtering removed all candidates, keeping originals") + print(f"Quality profile filtering removed all candidates, keeping originals") best_confidence = verified_candidates[0].confidence best_version = getattr(verified_candidates[0], 'version_type', 'unknown') best_quality = getattr(verified_candidates[0], 'quality', 'unknown') - print(f"✅ Found {len(verified_candidates)} VERIFIED matches for '{spotify_track.name}'. Best: {best_confidence:.2f} ({best_version}, {best_quality.upper()})") + print(f"Found {len(verified_candidates)} VERIFIED matches for '{spotify_track.name}'. Best: {best_confidence:.2f} ({best_version}, {best_quality.upper()})") # Log version breakdown for debugging version_counts = {} @@ -3064,10 +3064,10 @@ class DownloadMissingAlbumTracksModal(QDialog): penalty = getattr(candidate, 'version_penalty', 0.0) quality = getattr(candidate, 'quality', 'unknown') bitrate_info = f" {candidate.bitrate}kbps" if hasattr(candidate, 'bitrate') and candidate.bitrate else "" - print(f" 🎵 {candidate.confidence:.2f} - {version} ({quality.upper()}{bitrate_info}) (penalty: {penalty:.2f}) - {candidate.filename[:100]}...") + print(f" {candidate.confidence:.2f} - {version} ({quality.upper()}{bitrate_info}) (penalty: {penalty:.2f}) - {candidate.filename[:100]}...") else: - print(f"⚠️ No verified matches found for '{spotify_track.name}' after checking file paths.") + print(f"No verified matches found for '{spotify_track.name}' after checking file paths.") return verified_candidates @@ -3130,7 +3130,7 @@ class DownloadMissingAlbumTracksModal(QDialog): self.process_finished.emit() self.reject() except RuntimeError as e: - print(f"⚠️ Modal object deleted during cancel: {e}") + print(f"Modal object deleted during cancel: {e}") pass def on_close_clicked(self): @@ -3141,12 +3141,12 @@ class DownloadMissingAlbumTracksModal(QDialog): self.process_finished.emit() self.reject() except RuntimeError as e: - print(f"⚠️ Modal object deleted during close: {e}") + print(f"Modal object deleted during close: {e}") pass def cancel_operations(self): """Cancel any ongoing operations""" - print("🛑 Cancelling album download operations...") + print("Cancelling album download operations...") self.cancel_requested = True # Stop workers @@ -3157,7 +3157,7 @@ class DownloadMissingAlbumTracksModal(QDialog): # Stop polling self.download_status_timer.stop() - print("🛑 Album modal operations cancelled successfully.") + print("Album modal operations cancelled successfully.") def on_correct_failed_matches_clicked(self): """Handle failed matches correction using ManualMatchModal from sync.py""" @@ -3173,13 +3173,13 @@ class DownloadMissingAlbumTracksModal(QDialog): def on_manual_match_resolved(self, resolved_track_info): """Handle a track being successfully resolved by the ManualMatchModal""" - print(f"🔧 Manual match resolved (Artists) - download_index: {resolved_track_info.get('download_index')}, table_index: {resolved_track_info.get('table_index')}") + print(f"Manual match resolved (Artists) - download_index: {resolved_track_info.get('download_index')}, table_index: {resolved_track_info.get('table_index')}") original_failed_track = next((t for t in self.permanently_failed_tracks if t['download_index'] == resolved_track_info['download_index']), None) if original_failed_track: self.permanently_failed_tracks.remove(original_failed_track) - print(f"✅ Removed track from permanently_failed_tracks (Artists) - remaining: {len(self.permanently_failed_tracks)}") + print(f"Removed track from permanently_failed_tracks (Artists) - remaining: {len(self.permanently_failed_tracks)}") else: - print("⚠️ Could not find original failed track to remove (Artists)") + print("Could not find original failed track to remove (Artists)") self.update_failed_matches_button() class ArtistsPage(QWidget): @@ -3270,7 +3270,7 @@ class ArtistsPage(QWidget): return # All conditions met - start incremental update - logger.info(f"🎵 Starting automatic incremental database update after {active_server.upper()} scan") + logger.info(f"Starting automatic incremental database update after {active_server.upper()} scan") self._start_automatic_incremental_update() except Exception as e: @@ -3307,13 +3307,13 @@ class ArtistsPage(QWidget): """Handle completion of automatic database update""" try: if successful > 0: - logger.info(f"✅ Automatic database update completed: {successful} items processed successfully") + logger.info(f"Automatic database update completed: {successful} items processed successfully") else: - logger.info("💡 Automatic database update completed - no new content found") + logger.info("Automatic database update completed - no new content found") # Emit the signal to notify the dashboard to refresh its statistics self.database_updated_externally.emit() - logger.info("📊 Emitted signal to refresh dashboard database statistics after auto update") + logger.info("Emitted signal to refresh dashboard database statistics after auto update") # Clean up the worker if hasattr(self, '_auto_database_worker'): @@ -3359,7 +3359,7 @@ class ArtistsPage(QWidget): download_path = config_manager.get('soulseek.download_path') if download_path and hasattr(self.soulseek_client, 'download_path'): self.soulseek_client.download_path = download_path - print(f"✅ Set soulseek_client download path for ArtistsPage to: {download_path}") + print(f"Set soulseek_client download path for ArtistsPage to: {download_path}") # --- END FIX --- # Initialize unified media scan manager now that clients are available @@ -3368,7 +3368,7 @@ class ArtistsPage(QWidget): self.scan_manager = MediaScanManager(delay_seconds=60) # Add automatic incremental database update after scan completion self.scan_manager.add_scan_completion_callback(self._on_media_scan_completed) - print("✅ MediaScanManager initialized for ArtistsPage") + print("MediaScanManager initialized for ArtistsPage") except Exception as e: print(f"Failed to initialize MediaScanManager: {e}") @@ -3470,7 +3470,7 @@ class ArtistsPage(QWidget): """) self.search_input.returnPressed.connect(self.perform_artist_search) - search_btn = QPushButton("🔍 Search Artists") + search_btn = QPushButton("Search Artists") search_btn.setFixedHeight(40) search_btn.setStyleSheet(""" QPushButton { @@ -3916,7 +3916,7 @@ class ArtistsPage(QWidget): self.toast_manager.error("Spotify authentication required") return - self.search_status.setText("🔍 Searching for artists...") + self.search_status.setText("Searching for artists...") self.search_status.setStyleSheet("color: #1db954; padding: 10px;") # Show toast for search start @@ -4030,21 +4030,21 @@ class ArtistsPage(QWidget): self.albums_status.setText("No releases found") return - print(f"📀 Processing {len(albums)} releases for {artist.name}") + print(f"Processing {len(albums)} releases for {artist.name}") # Store all releases and classify them self.all_releases = albums self.albums_only, singles, eps = self.classify_releases(albums) self.singles_and_eps = singles + eps - print(f"📊 Classification: {len(self.albums_only)} albums, {len(singles)} singles, {len(eps)} EPs") + print(f"Classification: {len(self.albums_only)} albums, {len(singles)} singles, {len(eps)} EPs") # Initialize match counter for real-time updates self.matched_count = 0 # Auto-switch to Singles & EPs if no albums available if len(self.albums_only) == 0 and len(self.singles_and_eps) > 0: - print("📀 No albums found, automatically switching to Singles & EPs view") + print("No albums found, automatically switching to Singles & EPs view") self.current_filter = "singles_eps" self.albums_button.setChecked(False) self.singles_eps_button.setChecked(True) @@ -4057,9 +4057,9 @@ class ArtistsPage(QWidget): # Handle both old format (set of owned album names) and new format (dict of statuses) if isinstance(ownership_info, dict): - print(f"🎨 Displaying {len(albums)} albums with detailed ownership info") + print(f"Displaying {len(albums)} albums with detailed ownership info") else: - print(f"🎨 Displaying {len(albums)} albums, {len(ownership_info)} owned") + print(f"Displaying {len(albums)} albums, {len(ownership_info)} owned") # Clear existing albums self.clear_albums() @@ -4148,10 +4148,10 @@ class ArtistsPage(QWidget): def on_plex_library_checked(self, album_statuses): """Handle final database library check completion with detailed status info""" - print(f"📨 Database check completed: {len(album_statuses)} album statuses") + print(f"Database check completed: {len(album_statuses)} album statuses") if not self.current_albums: - print("📨 No current albums, skipping final update") + print("No current albums, skipping final update") return # Count different types of ownership @@ -4188,7 +4188,7 @@ class ArtistsPage(QWidget): else: self.toast_manager.success(f"Found {complete_count} complete albums out of {total_count}") - print(f"✅ Database check complete: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {total_count} albums") + print(f"Database check complete: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {total_count} albums") # Update the album display with the final ownership statuses self.display_albums(self.current_albums, album_statuses) @@ -4196,11 +4196,11 @@ class ArtistsPage(QWidget): def on_album_matched(self, album_name, ownership_status): """Handle individual album match for real-time UI update with detailed status""" if ownership_status.is_complete: - print(f"🎯 Real-time match: '{album_name}' (complete)") + print(f"Real-time match: '{album_name}' (complete)") elif ownership_status.is_nearly_complete: - print(f"🎯 Real-time match: '{album_name}' (nearly complete {int(ownership_status.completion_ratio * 100)}%)") + print(f"Real-time match: '{album_name}' (nearly complete {int(ownership_status.completion_ratio * 100)}%)") else: - print(f"🎯 Real-time match: '{album_name}' (partial {int(ownership_status.completion_ratio * 100)}%)") + print(f"Real-time match: '{album_name}' (partial {int(ownership_status.completion_ratio * 100)}%)") # Update match counter self.matched_count += 1 @@ -4224,7 +4224,7 @@ class ArtistsPage(QWidget): status_text = f"nearly complete ({int(ownership_status.completion_ratio * 100)}%)" else: status_text = f"partial ({int(ownership_status.completion_ratio * 100)}%)" - print(f"🔄 Real-time update: '{album_name}' -> {status_text}") + print(f"Real-time update: '{album_name}' -> {status_text}") album_card.update_ownership(ownership_status) break @@ -4253,9 +4253,9 @@ class ArtistsPage(QWidget): """Handle real-time single/EP match results""" if ownership_status.is_owned: if ownership_status.is_complete: - print(f"🎯 Single/EP match: '{release_name}' (complete)") + print(f"Single/EP match: '{release_name}' (complete)") else: - print(f"🎯 Single/EP match: '{release_name}' (partial {int(ownership_status.completion_ratio * 100)}%)") + print(f"Single/EP match: '{release_name}' (partial {int(ownership_status.completion_ratio * 100)}%)") # Find the corresponding card and update it for i in range(self.albums_grid_layout.count()): @@ -4268,7 +4268,7 @@ class ArtistsPage(QWidget): def on_singles_eps_library_checked(self, release_statuses): """Handle singles/EPs library check completion""" - print(f"📨 Singles/EPs check completed: {len(release_statuses)} statuses") + print(f"Singles/EPs check completed: {len(release_statuses)} statuses") # Count results for summary complete_count = sum(1 for status in release_statuses.values() if status.is_complete) @@ -4295,14 +4295,14 @@ class ArtistsPage(QWidget): else: self.toast_manager.success(f"Found {owned_count} partial releases") - print(f"✅ Singles/EPs check complete: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {total_count} releases") + print(f"Singles/EPs check complete: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {total_count} releases") # Update the display with the final ownership statuses self.display_albums(self.current_albums, release_statuses) def on_singles_eps_check_failed(self, error): """Handle singles/EPs check failure""" - print(f"❌ Singles/EPs library check failed: {error}") + print(f"Singles/EPs library check failed: {error}") # Update status singles_count = len([r for r in self.singles_and_eps if r.total_tracks == 1]) @@ -4323,7 +4323,7 @@ class ArtistsPage(QWidget): def on_album_download_requested(self, album: Album): """Handle album download request from an AlbumCard using new modal system.""" - print(f"🎵 Download requested for album: {album.name} by {', '.join(album.artists)}") + print(f"Download requested for album: {album.name} by {', '.join(album.artists)}") # Find the album card for this album to pass to modal album_card = None @@ -4356,7 +4356,7 @@ class ArtistsPage(QWidget): # Check if the modal still exists and is valid try: if existing_modal and existing_modal.isVisible(): - print(f"🔄 Resuming existing active modal for album: {album.name}") + print(f"Resuming existing active modal for album: {album.name}") # Modal is already visible and active, just bring it to front existing_modal.activateWindow() existing_modal.raise_() @@ -4368,7 +4368,7 @@ class ArtistsPage(QWidget): elif existing_modal: # Modal exists but is not visible - check if downloads are still in progress if hasattr(existing_modal, 'download_in_progress') and existing_modal.download_in_progress: - print(f"🔄 Resuming hidden modal with active downloads for album: {album.name}") + print(f"Resuming hidden modal with active downloads for album: {album.name}") # Show the existing modal to resume progress tracking existing_modal.show() existing_modal.activateWindow() @@ -4378,18 +4378,18 @@ class ArtistsPage(QWidget): return else: # Modal finished or cancelled, safe to create fresh one - print("⚠️ Found finished modal, creating fresh modal") + print("Found finished modal, creating fresh modal") del self.active_album_sessions[album.id] else: # No modal reference, clean up stale session - print("⚠️ Stale session found, cleaning up") + print("Stale session found, cleaning up") del self.active_album_sessions[album.id] except RuntimeError: # Modal was deleted, remove from sessions - print("⚠️ Existing modal was deleted, creating fresh session") + print("Existing modal was deleted, creating fresh session") del self.active_album_sessions[album.id] - print("🚀 Fetching album tracks and creating DownloadMissingAlbumTracksModal...") + print("Fetching album tracks and creating DownloadMissingAlbumTracksModal...") # First, we need to fetch the tracks for this album try: @@ -4419,7 +4419,7 @@ class ArtistsPage(QWidget): track = Track.from_spotify_track(track_data) tracks.append(track) - print(f"✅ Fetched {len(tracks)} tracks for album '{album.name}'") + print(f"Fetched {len(tracks)} tracks for album '{album.name}'") # Create a copy of the album with tracks added album_with_tracks = album @@ -4464,23 +4464,23 @@ class ArtistsPage(QWidget): modal.exec() except Exception as e: - print(f"❌ Error fetching album tracks: {e}") + print(f"Error fetching album tracks: {e}") QMessageBox.critical(self, "Error", f"Failed to fetch album tracks: {str(e)}\n\nPlease check your Spotify connection and try again.") def on_album_download_process_finished(self, album_id: str, album_card): """Handle cleanup when album download process is actually finished (downloads completed)""" - print(f"🏁 Album download process finished for album: {album_id}") + print(f"Album download process finished for album: {album_id}") # Only mark as completed if downloads actually finished successfully if album_card and hasattr(album_card, 'set_download_completed'): album_card.set_download_completed() - print(f"✅ Marked album {album_id} as download completed") + print(f"Marked album {album_id} as download completed") - print("✅ Album download process cleanup completed") + print("Album download process cleanup completed") def on_album_modal_closed(self, album_id: str, album_card, result): """Handle cleanup when album modal is closed (regardless of reason)""" - print(f"📋 Album modal closed for album: {album_id}, result: {'Accepted' if result == 1 else 'Rejected/Cancelled'}") + print(f"Album modal closed for album: {album_id}, result: {'Accepted' if result == 1 else 'Rejected/Cancelled'}") # Clean up the session when modal is definitely closing if album_id in self.active_album_sessions: @@ -4490,18 +4490,18 @@ class ArtistsPage(QWidget): # Only remove session if downloads are completely finished or cancelled if result == 1: # QDialog.Accepted = 1 (downloads completed or all tracks exist) del self.active_album_sessions[album_id] - print(f"🗑️ Removed completed session for album {album_id}") + print(f"Removed completed session for album {album_id}") elif modal and hasattr(modal, 'cancel_requested') and modal.cancel_requested: # User explicitly cancelled - remove session for fresh modal on next click del self.active_album_sessions[album_id] - print(f"🗑️ Removed cancelled session for album {album_id} - user requested cancellation") + print(f"Removed cancelled session for album {album_id} - user requested cancellation") elif modal and hasattr(modal, 'download_in_progress') and not modal.download_in_progress: # Downloads are not in progress, safe to remove session del self.active_album_sessions[album_id] - print(f"🗑️ Removed finished session for album {album_id} - no downloads in progress") + print(f"Removed finished session for album {album_id} - no downloads in progress") else: # Downloads still in progress and not cancelled - keep session alive for resumption - print(f"💾 Keeping session for album {album_id} - downloads still in progress, can be resumed") + print(f"Keeping session for album {album_id} - downloads still in progress, can be resumed") if album_card: try: @@ -4509,14 +4509,14 @@ class ArtistsPage(QWidget): # Only mark as completed if downloads were actually successful if hasattr(album_card, 'set_download_completed'): album_card.set_download_completed() - print(f"✅ Marked album {album_id} as download completed") + print(f"Marked album {album_id} as download completed") else: # Modal was cancelled/closed - reset the card to allow reopening (but keep session) # Reset any download-in-progress indicators if hasattr(album_card, 'progress_overlay') and album_card.progress_overlay is not None: try: album_card.progress_overlay.hide() - print(f"🔄 Hidden progress overlay for album {album_id}") + print(f"Hidden progress overlay for album {album_id}") except RuntimeError: pass @@ -4529,16 +4529,16 @@ class ArtistsPage(QWidget): # Show a visual indicator that this album has an active session if hasattr(album_card, 'status_indicator'): try: - album_card.status_indicator.setText("▶️") + album_card.status_indicator.setText("") album_card.status_indicator.setToolTip("Click to resume download session") except RuntimeError: pass - print(f"🔄 Reset album card for {album_id} to allow resumption") + print(f"Reset album card for {album_id} to allow resumption") except Exception as e: - print(f"⚠️ Error handling album card state: {e}") + print(f"Error handling album card state: {e}") - print("✅ Album modal cleanup completed") + print("Album modal cleanup completed") # === LEGACY METHODS - NO LONGER USED WITH NEW MODAL SYSTEM === # These methods were part of the old manual album download flow @@ -4559,7 +4559,7 @@ class ArtistsPage(QWidget): # # # Delegate to the DownloadsPage to handle the matched download # # This will open the Spotify matching modal and add to the central queue - # print("🚀 Delegating to DownloadsPage to start matched album download...") + # print("Delegating to DownloadsPage to start matched album download...") # self.downloads_page.start_matched_album_download(album_result) # else: # QMessageBox.critical(self, "Error", "Downloads page is not connected. Cannot start download.") @@ -4592,7 +4592,7 @@ class ArtistsPage(QWidget): # # # Update album card to show download in progress # album_card.set_download_in_progress() - # print(f"📊 Started tracking album: {spotify_album.name} ({album_result.track_count} tracks)") + # print(f"Started tracking album: {spotify_album.name} ({album_result.track_count} tracks)") # === END LEGACY METHODS === @@ -4638,7 +4638,7 @@ class ArtistsPage(QWidget): self._is_status_update_running = False return - print(f"🔍 Starting album status check for {len(items_to_check)} downloads across {len(self.album_downloads)} albums") + print(f"Starting album status check for {len(items_to_check)} downloads across {len(self.album_downloads)} albums") # Create and start our dedicated album worker worker = AlbumStatusProcessingWorker( @@ -4694,7 +4694,7 @@ class ArtistsPage(QWidget): def _on_album_status_error(self, error_msg): """Handle errors from album status worker""" - print(f"❌ Album status worker error: {error_msg}") + print(f"Album status worker error: {error_msg}") self._is_status_update_running = False def update_active_downloads_from_queue(self): @@ -4712,7 +4712,7 @@ class ArtistsPage(QWidget): if hasattr(self.downloads_page.download_queue, 'finished_queue'): finished_items = self.downloads_page.download_queue.finished_queue.download_items - print(f"🔍 Checking {len(active_items)} active downloads and {len(finished_items)} finished downloads for album tracking") + print(f"Checking {len(active_items)} active downloads and {len(finished_items)} finished downloads for album tracking") # For each tracked album, check if any downloads match for album_id, album_info in self.album_downloads.items(): @@ -4722,7 +4722,7 @@ class ArtistsPage(QWidget): continue album_name = spotify_album.name if spotify_album else 'Unknown' - print(f"🎵 Looking for downloads matching album: {album_name} by {album_result.artist}") + print(f"Looking for downloads matching album: {album_name} by {album_result.artist}") # Look for downloads that match this album's tracks (both active and finished) matching_downloads = [] @@ -4739,20 +4739,20 @@ class ArtistsPage(QWidget): # Debug: show what download ID we're working with current_id = getattr(download_item, 'download_id', 'NO_ID') title = getattr(download_item, 'title', 'Unknown') - print(f" 🔍 Found matching item: '{title}' with download_id: {current_id}") + print(f" Found matching item: '{title}' with download_id: {current_id}") # Use the download ID directly from the item (should be the real one) if current_id and current_id != 'NO_ID': # Check if this item is in finished items (completed) if download_item in finished_items: completed_count += 1 - print(f" ✅ Found completed track: '{title}' (ID: {current_id})") + print(f" Found completed track: '{title}' (ID: {current_id})") else: # It's an active download - use the current ID matching_downloads.append(current_id) - print(f" 🔄 Added active download ID: {current_id} for '{title}'") + print(f" Added active download ID: {current_id} for '{title}'") else: - print(f" ⚠️ No download ID found for: '{title}'") + print(f" No download ID found for: '{title}'") # Update the active downloads and completed count for this album old_active = album_info.get('active_downloads', []) @@ -4762,18 +4762,18 @@ class ArtistsPage(QWidget): # Update completed tracks count if we found more completed items if completed_count > old_completed: - print(f"📈 Updating completed tracks: {old_completed} -> {completed_count}") + print(f"Updating completed tracks: {old_completed} -> {completed_count}") album_info['completed_tracks'] = completed_count # Trigger UI update self.update_album_card_progress(album_id) # Log changes if len(matching_downloads) != len(old_active) or completed_count != old_completed: - print(f"📊 Album '{album_name}': {len(old_active)} -> {len(matching_downloads)} active, {old_completed} -> {completed_count} completed") + print(f"Album '{album_name}': {len(old_active)} -> {len(matching_downloads)} active, {old_completed} -> {completed_count} completed") if not matching_downloads and completed_count == 0: total_tracks = album_info.get('total_tracks', 0) - print(f"❌ No matching downloads found for album: {album_name} (expected {total_tracks} tracks)") + print(f"No matching downloads found for album: {album_name} (expected {total_tracks} tracks)") def _get_real_download_id(self, download_item): """Extract the real slskd download ID from a download item""" @@ -4800,7 +4800,7 @@ class ArtistsPage(QWidget): # Try to find the real ID by querying current downloads by filename real_id = self._lookup_download_id_by_filename(download_item.filename) if real_id: - print(f"🔍 Found real ID {real_id} for composite ID {download_id}") + print(f"Found real ID {real_id} for composite ID {download_id}") return real_id # If we can't determine the real ID, return the composite one @@ -4854,7 +4854,7 @@ class ArtistsPage(QWidget): loop.close() except Exception as e: - print(f"❌ Error looking up download ID for {filename}: {e}") + print(f"Error looking up download ID for {filename}: {e}") return None @@ -4862,7 +4862,7 @@ class ArtistsPage(QWidget): """Enhanced matching logic to determine if a download belongs to the tracked album""" # Check for explicit album match flag (from Spotify matching modal) if hasattr(download_item, 'matched_download') and download_item.matched_download: - print(f" 🎯 Found explicitly matched download: {getattr(download_item, 'title', 'Unknown')}") + print(f" Found explicitly matched download: {getattr(download_item, 'title', 'Unknown')}") return True # Check for album metadata match @@ -4874,7 +4874,7 @@ class ArtistsPage(QWidget): if (download_album == spotify_album_name or download_album in spotify_album_name or spotify_album_name in download_album): - print(f" 🎵 Album name match: '{download_album}' ~ '{spotify_album_name}'") + print(f" Album name match: '{download_album}' ~ '{spotify_album_name}'") return True # Check artist matching @@ -4907,7 +4907,7 @@ class ArtistsPage(QWidget): if hasattr(download_item, 'created_time') or hasattr(download_item, 'start_time'): # Could add timestamp checking here if needed pass - print(f" 👤 Artist match found for: {getattr(download_item, 'title', 'Unknown')}") + print(f" Artist match found for: {getattr(download_item, 'title', 'Unknown')}") return True # Check filename-based matching as last resort @@ -4916,12 +4916,12 @@ class ArtistsPage(QWidget): # Check if filename contains album name if spotify_album and spotify_album.name.lower() in filename: - print(f" 📂 Filename contains album name: {download_item.filename}") + print(f" Filename contains album name: {download_item.filename}") return True # Check if filename contains artist name if album_result and album_result.artist and album_result.artist.lower() in filename: - print(f" 📂 Filename contains artist name: {download_item.filename}") + print(f" Filename contains artist name: {download_item.filename}") return True return False @@ -4932,7 +4932,7 @@ class ArtistsPage(QWidget): self._is_status_update_running = False return - print(f"📊 Processing {len(results)} album download status updates") + print(f"Processing {len(results)} album download status updates") albums_to_update = set() albums_completed = set() @@ -4949,7 +4949,7 @@ class ArtistsPage(QWidget): api_missing_count = result.get('api_missing_count', 0) # Check if this download was previously completed but now missing (due to cleanup) if self._was_download_previously_completed(download_id): - print(f"✅ Download {download_id} was previously completed (now cleaned up)") + print(f"Download {download_id} was previously completed (now cleaned up)") status = 'completed' # Treat as completed else: # Update the missing count in our tracking data for next poll @@ -4966,13 +4966,13 @@ class ArtistsPage(QWidget): break if not target_album_id or target_album_id not in self.album_downloads: - print(f"⚠️ Could not find album for download {download_id}") + print(f"Could not find album for download {download_id}") continue album_info = self.album_downloads[target_album_id] album_name = album_info.get('spotify_album', {}).name if album_info.get('spotify_album') else 'Unknown' - print(f"🎵 Album '{album_name}': Download {download_id} status = {status} ({progress:.1f}%)") + print(f"Album '{album_name}': Download {download_id} status = {status} ({progress:.1f}%)") # Handle status changes if status == 'completed': @@ -4986,27 +4986,27 @@ class ArtistsPage(QWidget): album_info['completed_tracks'] += 1 album_info['active_downloads'].remove(download_id) albums_to_update.add(target_album_id) - print(f"✅ Album track completed via polling: {album_info['completed_tracks']}/{album_info['total_tracks']}") + print(f"Album track completed via polling: {album_info['completed_tracks']}/{album_info['total_tracks']}") # Check if album is fully completed if (album_info['completed_tracks'] >= album_info['total_tracks'] and not album_info.get('active_downloads')): albums_completed.add(target_album_id) else: - print(f"✅ Download {download_id} already counted as completed") + print(f"Download {download_id} already counted as completed") elif status in ['failed', 'cancelled']: # Remove from active downloads but don't increment completed if download_id in album_info['active_downloads']: album_info['active_downloads'].remove(download_id) albums_to_update.add(target_album_id) - print(f"❌ Album track {status}: {download_id}") + print(f"Album track {status}: {download_id}") elif status in ['downloading', 'queued']: # Update progress for in-progress downloads albums_to_update.add(target_album_id) if progress > 0: - print(f"⏳ Track downloading: {progress:.1f}%") + print(f"Track downloading: {progress:.1f}%") # Update album cards for albums that had status changes for album_id in albums_to_update: @@ -5024,7 +5024,7 @@ class ArtistsPage(QWidget): # Remove from tracking del self.album_downloads[album_id] - print(f"🎉 Album download completed and removed from tracking: {album_name}") + print(f"Album download completed and removed from tracking: {album_name}") self._is_status_update_running = False @@ -5042,14 +5042,14 @@ class ArtistsPage(QWidget): did for did in album_info.get('active_downloads', []) if did != download_id ] - print(f"❌ Removed failed download {download_id} from album tracking") + print(f"Removed failed download {download_id} from album tracking") break def _mark_download_as_completed(self, download_id): """Mark a download as completed to handle cleanup detection""" if download_id: self.completed_downloads.add(download_id) - print(f"📝 Marked download {download_id} as completed") + print(f"Marked download {download_id} as completed") def _was_download_previously_completed(self, download_id): """Check if a download was previously marked as completed""" @@ -5057,13 +5057,13 @@ class ArtistsPage(QWidget): def notify_download_completed(self, download_id, download_item=None): """Called by downloads page when a download completes (before cleanup)""" - print(f"🔔 Downloads page notified completion of: {download_id}") + print(f"Downloads page notified completion of: {download_id}") if download_item: print(f" Item: '{getattr(download_item, 'title', 'Unknown')}' by '{getattr(download_item, 'artist', 'Unknown')}'") # Check if already processed to prevent double counting if self._was_download_previously_completed(download_id): - print(f"⏭️ Download {download_id} already processed, skipping") + print(f"Download {download_id} already processed, skipping") return # Mark as completed immediately @@ -5076,7 +5076,7 @@ class ArtistsPage(QWidget): for album_id, album_info in self.album_downloads.items(): if download_id in album_info.get('active_downloads', []): target_album_id = album_id - print(f"✅ Found album by direct ID match: {album_id}") + print(f"Found album by direct ID match: {album_id}") break # Approach 2: Match by download item attributes if we have the item @@ -5087,7 +5087,7 @@ class ArtistsPage(QWidget): if self._is_download_from_album(download_item, album_result, spotify_album): target_album_id = album_id - print(f"✅ Found album by item matching: {album_id}") + print(f"Found album by item matching: {album_id}") break # Approach 3: Remove any composite ID that might match this download @@ -5103,7 +5103,7 @@ class ArtistsPage(QWidget): album_info['active_downloads'].remove(active_id) album_info['active_downloads'].append(download_id) target_album_id = album_id - print(f"✅ Found album by title matching and updated ID: {active_id} -> {download_id}") + print(f"Found album by title matching and updated ID: {active_id} -> {download_id}") break if target_album_id: @@ -5124,7 +5124,7 @@ class ArtistsPage(QWidget): spotify_album = album_info.get('spotify_album') album_name = spotify_album.name if spotify_album else 'Unknown' - print(f"✅ Album '{album_name}' track completed via notification: {album_info['completed_tracks']}/{album_info['total_tracks']}") + print(f"Album '{album_name}' track completed via notification: {album_info['completed_tracks']}/{album_info['total_tracks']}") # Check if album is complete if (album_info['completed_tracks'] >= album_info['total_tracks'] and @@ -5136,9 +5136,9 @@ class ArtistsPage(QWidget): # Remove from tracking del self.album_downloads[target_album_id] - print(f"🎉 Album download completed via notification: {album_name}") + print(f"Album download completed via notification: {album_name}") else: - print(f"⚠️ Could not find album for completed download: {download_id}") + print(f"Could not find album for completed download: {download_id}") if download_item: print(f" Title: '{getattr(download_item, 'title', 'Unknown')}'") print(f" Artist: '{getattr(download_item, 'artist', 'Unknown')}'") @@ -5173,24 +5173,24 @@ class ArtistsPage(QWidget): if completed >= total and not active_downloads: # Album is fully complete - this will be handled in the main status handler # Don't call set_download_completed here to avoid duplicate processing - print(f"🎯 Album '{album_info.get('spotify_album', {}).name if album_info.get('spotify_album') else 'Unknown'}' is complete: {completed}/{total}") + print(f"Album '{album_info.get('spotify_album', {}).name if album_info.get('spotify_album') else 'Unknown'}' is complete: {completed}/{total}") return elif not active_downloads and completed == 0: # No active downloads and nothing completed - might be initializing album_card.set_download_in_progress() - print(f"🔄 Album initializing downloads...") + print(f"Album initializing downloads...") elif active_downloads: # Has active downloads - show progress album_card.update_download_progress(completed, total, percentage) - print(f"📊 Album progress: {completed}/{total} tracks ({percentage}%)") + print(f"Album progress: {completed}/{total} tracks ({percentage}%)") else: # Some completed but no active - might be stalled or failed if completed > 0: album_card.update_download_progress(completed, total, percentage) - print(f"⚠️ Album partially complete: {completed}/{total} tracks ({percentage}%)") + print(f"Album partially complete: {completed}/{total} tracks ({percentage}%)") else: album_card.set_download_in_progress() - print(f"🔄 Album status unclear, showing in progress...") + print(f"Album status unclear, showing in progress...") # Update the album card's status indicator to show download activity if hasattr(album_card, 'status_indicator'): @@ -5200,7 +5200,7 @@ class ArtistsPage(QWidget): album_card.status_indicator.setText(f"{percentage}%") album_card.status_indicator.setToolTip(f"Downloading: {completed}/{total} tracks ({percentage}%)") else: - album_card.status_indicator.setText("⏳") + album_card.status_indicator.setText("") album_card.status_indicator.setToolTip("Starting download...") elif completed > 0: # Show partial completion @@ -5344,12 +5344,12 @@ class ArtistsPage(QWidget): def cleanup_download_tracking(self): """Clean up download tracking resources""" - print("🧹 Starting album download tracking cleanup...") + print("Starting album download tracking cleanup...") # Stop the download status timer if hasattr(self, 'download_status_timer') and self.download_status_timer.isActive(): self.download_status_timer.stop() - print(" ⏹️ Stopped download status timer") + print(" Stopped download status timer") # Reset any album cards that are showing download progress cards_reset = 0 @@ -5368,7 +5368,7 @@ class ArtistsPage(QWidget): cards_reset += 1 if cards_reset > 0: - print(f" 🔄 Reset {cards_reset} album cards") + print(f" Reset {cards_reset} album cards") # Clear download tracking state tracked_albums = len(self.album_downloads) @@ -5378,7 +5378,7 @@ class ArtistsPage(QWidget): self._is_status_update_running = False if tracked_albums > 0: - print(f" 🗑️ Cleared tracking for {tracked_albums} albums") + print(f" Cleared tracking for {tracked_albums} albums") # Shutdown the download status thread pool gracefully if hasattr(self, 'download_status_pool'): @@ -5388,14 +5388,14 @@ class ArtistsPage(QWidget): # Wait for active tasks to complete (with timeout) if not self.download_status_pool.waitForDone(2000): # Wait up to 2 seconds - print(" ⚠️ Download status pool did not finish within timeout") + print(" Download status pool did not finish within timeout") else: - print(" ✅ Download status pool shut down cleanly") + print(" Download status pool shut down cleanly") except Exception as e: - print(f" ❌ Error cleaning up download status pool: {e}") + print(f" Error cleaning up download status pool: {e}") - print("🧹 Album download tracking cleanup completed") + print("Album download tracking cleanup completed") def cleanup_album_sessions(self): """Clean up active album download sessions""" @@ -5403,7 +5403,7 @@ class ArtistsPage(QWidget): return session_count = len(self.active_album_sessions) - print(f"🧹 Cleaning up {session_count} active album sessions...") + print(f"Cleaning up {session_count} active album sessions...") for album_id, session in list(self.active_album_sessions.items()): try: @@ -5412,51 +5412,51 @@ class ArtistsPage(QWidget): modal.cancel_operations() modal.close() except Exception as e: - print(f" ⚠️ Error cleaning up session for album {album_id}: {e}") + print(f" Error cleaning up session for album {album_id}: {e}") self.active_album_sessions.clear() - print(f"🧹 Cleaned up {session_count} album sessions") + print(f"Cleaned up {session_count} album sessions") def restart_download_tracking(self): """Restart download tracking timer if stopped""" if hasattr(self, 'download_status_timer') and not self.download_status_timer.isActive(): self.download_status_timer.start(2000) - print("🔄 Download tracking timer restarted") + print("Download tracking timer restarted") def stop_all_workers(self): """Stop all background workers""" - print("🛑 Stopping all artist page workers...") + print("Stopping all artist page workers...") workers_stopped = 0 if self.artist_search_worker and self.artist_search_worker.isRunning(): - print(" 🔍 Stopping artist search worker...") + print(" Stopping artist search worker...") self.artist_search_worker.terminate() if self.artist_search_worker.wait(2000): # Wait up to 2 seconds - print(" ✅ Artist search worker stopped") + print(" Artist search worker stopped") else: - print(" ⚠️ Artist search worker did not stop within timeout") + print(" Artist search worker did not stop within timeout") self.artist_search_worker = None workers_stopped += 1 if self.album_fetch_worker and self.album_fetch_worker.isRunning(): - print(" 📀 Stopping album fetch worker...") + print(" Stopping album fetch worker...") self.album_fetch_worker.terminate() if self.album_fetch_worker.wait(2000): # Wait up to 2 seconds - print(" ✅ Album fetch worker stopped") + print(" Album fetch worker stopped") else: - print(" ⚠️ Album fetch worker did not stop within timeout") + print(" Album fetch worker did not stop within timeout") self.album_fetch_worker = None workers_stopped += 1 if self.plex_library_worker and self.plex_library_worker.isRunning(): - print(" 📚 Stopping Plex library worker...") + print(" Stopping Plex library worker...") self.plex_library_worker.stop() self.plex_library_worker.terminate() if self.plex_library_worker.wait(2000): # Wait up to 2 seconds - print(" ✅ Plex library worker stopped") + print(" Plex library worker stopped") else: - print(" ⚠️ Plex library worker did not stop within timeout") + print(" Plex library worker did not stop within timeout") self.plex_library_worker = None if hasattr(self, 'singles_eps_worker') and self.singles_eps_worker: @@ -5466,7 +5466,7 @@ class ArtistsPage(QWidget): workers_stopped += 1 if workers_stopped > 0: - print(f" 🛑 Stopped {workers_stopped} background workers") + print(f" Stopped {workers_stopped} background workers") # Stop download tracking (this includes its own worker cleanup) self.cleanup_download_tracking() @@ -5474,7 +5474,7 @@ class ArtistsPage(QWidget): # Clean up active album sessions self.cleanup_album_sessions() - print("🛑 All workers stopped") + print("All workers stopped") def clear_artist_results(self): """Clear artist search results""" diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index 362dfccb..c9dd81eb 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -381,10 +381,10 @@ class DownloadMissingWishlistTracksModal(QDialog): title_section.addWidget(subtitle) dashboard_layout = QHBoxLayout() - self.total_card = self.create_compact_counter_card("📀 Total", "0", "#1db954") - self.matched_card = self.create_compact_counter_card("✅ Found", "0", "#4CAF50") + self.total_card = self.create_compact_counter_card("Total", "0", "#1db954") + self.matched_card = self.create_compact_counter_card("Found", "0", "#4CAF50") self.download_card = self.create_compact_counter_card("⬇️ Missing", "0", "#ff6b6b") - self.downloaded_card = self.create_compact_counter_card("✅ Downloaded", "0", "#4CAF50") + self.downloaded_card = self.create_compact_counter_card("Downloaded", "0", "#4CAF50") dashboard_layout.addWidget(self.total_card) dashboard_layout.addWidget(self.matched_card) dashboard_layout.addWidget(self.download_card) @@ -421,7 +421,7 @@ class DownloadMissingWishlistTracksModal(QDialog): progress_frame.setStyleSheet("background-color: #2d2d2d; border: 1px solid #444444; border-radius: 8px; padding: 12px;") layout = QVBoxLayout(progress_frame) analysis_container = QVBoxLayout() - analysis_label = QLabel("🔍 Library Analysis") + analysis_label = QLabel("Library Analysis") analysis_label.setFont(QFont("Arial", 11, QFont.Weight.Bold)) self.analysis_progress = QProgressBar() self.analysis_progress.setFixedHeight(20) @@ -482,7 +482,7 @@ class DownloadMissingWishlistTracksModal(QDialog): # --- DURATION LOGIC REMOVED --- # "Matched" is now column 2 - matched_item = QTableWidgetItem("⏳ Pending") + matched_item = QTableWidgetItem("Pending") matched_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.track_table.setItem(i, 2, matched_item) @@ -575,10 +575,10 @@ class DownloadMissingWishlistTracksModal(QDialog): cancel_button = layout.itemAt(0).widget() if cancel_button: cancel_button.setEnabled(False) - cancel_button.setText("✓") + cancel_button.setText("") # Update status to cancelled (column 3 for dashboard) - self.track_table.setItem(row, 3, QTableWidgetItem("🚫 Cancelled")) + self.track_table.setItem(row, 3, QTableWidgetItem("Cancelled")) # Add to cancelled tracks set if not hasattr(self, 'cancelled_tracks'): @@ -586,7 +586,7 @@ class DownloadMissingWishlistTracksModal(QDialog): self.cancelled_tracks.add(row) track = self.wishlist_tracks[row] - print(f"🚫 Track cancelled: {track.name} (row {row})") + print(f"Track cancelled: {track.name} (row {row})") # If downloads are active, also handle active download cancellation download_index = None @@ -596,7 +596,7 @@ class DownloadMissingWishlistTracksModal(QDialog): for download in self.active_downloads: if download.get('table_index') == row: download_index = download.get('download_index', row) - print(f"🚫 Found active download {download_index} for cancelled track") + print(f"Found active download {download_index} for cancelled track") break # Check parallel_search_tracking for download index @@ -604,23 +604,23 @@ class DownloadMissingWishlistTracksModal(QDialog): for idx, track_info in self.parallel_search_tracking.items(): if track_info.get('table_index') == row: download_index = idx - print(f"🚫 Found parallel tracking {download_index} for cancelled track") + print(f"Found parallel tracking {download_index} for cancelled track") break # If we found an active download, trigger completion to free up the worker if download_index is not None and hasattr(self, 'on_parallel_track_completed'): - print(f"🚫 Triggering completion for active download {download_index}") + print(f"Triggering completion for active download {download_index}") self.on_parallel_track_completed(download_index, success=False) def create_buttons(self): button_frame = QFrame(styleSheet="background-color: transparent; padding: 10px;") layout = QHBoxLayout(button_frame) - self.correct_failed_btn = QPushButton("🔧 Correct Failed Matches") + self.correct_failed_btn = QPushButton("Correct Failed Matches") self.correct_failed_btn.setFixedWidth(220) self.correct_failed_btn.setStyleSheet("QPushButton { background-color: #ffc107; color: #000; border-radius: 20px; font-weight: bold; }") self.correct_failed_btn.clicked.connect(self.on_correct_failed_matches_clicked) self.correct_failed_btn.hide() - self.clear_wishlist_btn = QPushButton("🗑️ Clear Wishlist") + self.clear_wishlist_btn = QPushButton("Clear Wishlist") self.clear_wishlist_btn.setFixedSize(150, 40) self.clear_wishlist_btn.setStyleSheet("QPushButton { background-color: #d32f2f; color: #fff; border-radius: 20px; font-size: 14px; font-weight: bold; }") self.clear_wishlist_btn.clicked.connect(self.on_clear_wishlist_clicked) @@ -701,7 +701,7 @@ class DownloadMissingWishlistTracksModal(QDialog): self.analysis_progress.setValue(track_index) row_index = track_index - 1 if result.exists_in_plex: - matched_text = f"✅ Found ({result.confidence:.1f})" + matched_text = f"Found ({result.confidence:.1f})" self.matched_tracks_count += 1 self.matched_count_label.setText(str(self.matched_tracks_count)) @@ -713,7 +713,7 @@ class DownloadMissingWishlistTracksModal(QDialog): logger.warning(f"Could not remove pre-existing track '{track_id_to_remove}' from wishlist.") else: - matched_text = "❌ Missing" + matched_text = "Missing" self.tracks_to_download_count += 1 self.download_count_label.setText(str(self.tracks_to_download_count)) # Add cancel button for missing tracks only @@ -764,13 +764,13 @@ class DownloadMissingWishlistTracksModal(QDialog): if track_index != -1: # Skip if track was cancelled if hasattr(self, 'cancelled_tracks') and track_index in self.cancelled_tracks: - print(f"🚫 Skipping cancelled track at index {track_index}: {track.name}") + print(f"Skipping cancelled track at index {track_index}: {track.name}") self.download_queue_index += 1 self.completed_downloads += 1 continue # FIX: Changed column index from 4 to 3 to target the "Status" column. - self.track_table.setItem(track_index, 3, QTableWidgetItem("🔍 Searching...")) + self.track_table.setItem(track_index, 3, QTableWidgetItem("Searching...")) self.search_and_download_track_parallel(track, self.download_queue_index, track_index) self.active_parallel_downloads += 1 self.download_queue_index += 1 @@ -940,18 +940,18 @@ class DownloadMissingWishlistTracksModal(QDialog): if not next_candidate: self.on_parallel_track_failed(download_index, "No alternative sources in cache") return - self.track_table.setItem(failed_download_info['table_index'], 3, QTableWidgetItem(f"🔄 Retrying ({track_info['retry_count']})...")) + self.track_table.setItem(failed_download_info['table_index'], 3, QTableWidgetItem(f"Retrying ({track_info['retry_count']})...")) self.start_validated_download_parallel(next_candidate, track_info['spotify_track'], track_info['track_index'], track_info['table_index'], download_index) def on_parallel_track_completed(self, download_index, success): if not hasattr(self, 'parallel_search_tracking'): - print(f"⚠️ parallel_search_tracking not initialized yet, skipping completion for download {download_index}") + print(f"parallel_search_tracking not initialized yet, skipping completion for download {download_index}") return track_info = self.parallel_search_tracking.get(download_index) if not track_info or track_info.get('completed', False): return track_info['completed'] = True if success: - self.track_table.setItem(track_info['table_index'], 3, QTableWidgetItem("✅ Downloaded")) + self.track_table.setItem(track_info['table_index'], 3, QTableWidgetItem("Downloaded")) # Hide cancel button since track is now downloaded self.hide_cancel_button_for_row(track_info['table_index']) self.downloaded_tracks_count += 1 @@ -966,10 +966,10 @@ class DownloadMissingWishlistTracksModal(QDialog): # Check if track was cancelled (don't overwrite cancelled status) table_index = track_info['table_index'] current_status = self.track_table.item(table_index, 3) - if current_status and "🚫 Cancelled" in current_status.text(): - print(f"🔧 Track {download_index} was cancelled - preserving cancelled status") + if current_status and "Cancelled" in current_status.text(): + print(f"Track {download_index} was cancelled - preserving cancelled status") else: - self.track_table.setItem(table_index, 3, QTableWidgetItem("❌ Failed")) + self.track_table.setItem(table_index, 3, QTableWidgetItem("Failed")) if track_info not in self.permanently_failed_tracks: self.permanently_failed_tracks.append(track_info) self.failed_downloads += 1 @@ -986,7 +986,7 @@ class DownloadMissingWishlistTracksModal(QDialog): def update_failed_matches_button(self): count = len(self.permanently_failed_tracks) if count > 0: - self.correct_failed_btn.setText(f"🔧 Correct {count} Failed Match{'es' if count > 1 else ''}") + self.correct_failed_btn.setText(f"Correct {count} Failed Match{'es' if count > 1 else ''}") self.correct_failed_btn.show() else: self.correct_failed_btn.hide() @@ -1033,8 +1033,8 @@ class DownloadMissingWishlistTracksModal(QDialog): status_item = self.track_table.item(cancelled_row, 3) current_status = status_item.text() if status_item else "" - if "✅ Downloaded" in current_status: - print(f"🚫 Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist re-addition") + if "Downloaded" in current_status: + print(f"Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist re-addition") else: cancelled_track_info = { 'download_index': cancelled_row, @@ -1048,9 +1048,9 @@ class DownloadMissingWishlistTracksModal(QDialog): # Check if not already in permanently_failed_tracks if not any(t.get('table_index') == cancelled_row for t in self.permanently_failed_tracks): self.permanently_failed_tracks.append(cancelled_track_info) - print(f"🚫 Added cancelled missing track {cancelled_track.name} to failed list for wishlist re-addition") + print(f"Added cancelled missing track {cancelled_track.name} to failed list for wishlist re-addition") else: - print(f"🚫 Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist re-addition") + print(f"Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist re-addition") wishlist_added_count = 0 if self.permanently_failed_tracks: @@ -1061,7 +1061,7 @@ class DownloadMissingWishlistTracksModal(QDialog): final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing tracks!\n\n" if wishlist_added_count > 0: - final_message += f"✨ Re-added {wishlist_added_count} failed track{'s' if wishlist_added_count > 1 else ''} to wishlist for future retry.\n\n" + final_message += f"Re-added {wishlist_added_count} failed track{'s' if wishlist_added_count > 1 else ''} to wishlist for future retry.\n\n" if self.permanently_failed_tracks: final_message += "You can also manually correct failed downloads." else: @@ -1322,7 +1322,7 @@ class SimpleWishlistDownloadWorker(QRunnable): """Run the download with detailed status updates""" try: # Update status: Starting search - self.signals.status_updated.emit(self.download_index, "🔍 Searching...") + self.signals.status_updated.emit(self.download_index, "Searching...") # Use async method in sync context loop = asyncio.new_event_loop() @@ -1330,7 +1330,7 @@ class SimpleWishlistDownloadWorker(QRunnable): try: # Update status: Found candidates, analyzing - self.signals.status_updated.emit(self.download_index, "🔎 Analyzing results...") + self.signals.status_updated.emit(self.download_index, "Analyzing results...") # Use the enhanced search method that provides more feedback results = loop.run_until_complete( @@ -1339,7 +1339,7 @@ class SimpleWishlistDownloadWorker(QRunnable): if results and len(results) > 0: # Update status: Found candidates, starting download - self.signals.status_updated.emit(self.download_index, f"📋 Found {len(results)} candidates") + self.signals.status_updated.emit(self.download_index, f"Found {len(results)} candidates") time.sleep(0.5) # Brief pause so user can see the status # Get the best result and start download @@ -1369,7 +1369,7 @@ class SimpleWishlistDownloadWorker(QRunnable): """Search for tracks with progress updates""" try: # Emit search progress - self.signals.status_updated.emit(self.download_index, "🌐 Searching network...") + self.signals.status_updated.emit(self.download_index, "Searching network...") # Perform the search (this would ideally use the soulseek client's search methods) # For now, we'll use the existing search_and_download_best method @@ -1690,7 +1690,7 @@ class MetadataUpdateWorker(QThread): print(f"No albums found for artist '{artist.title}'") return 0 - print(f"🎨 Checking artwork for {len(albums)} albums by '{artist.title}'...") + print(f"Checking artwork for {len(albums)} albums by '{artist.title}'...") for album in albums: try: @@ -1741,10 +1741,10 @@ class MetadataUpdateWorker(QThread): continue total_processed = updated_count + skipped_count - print(f"🎨 Artwork summary for '{artist.title}': {updated_count} updated, {skipped_count} skipped (already have good artwork)") + print(f"Artwork summary for '{artist.title}': {updated_count} updated, {skipped_count} skipped (already have good artwork)") if updated_count == 0 and skipped_count == len(albums): - print(f" ✅ All albums already have good artwork - no Spotify API calls needed!") + print(f" All albums already have good artwork - no Spotify API calls needed!") return updated_count except Exception as e: @@ -1758,17 +1758,17 @@ class MetadataUpdateWorker(QThread): # Check if album has any thumb at all if not hasattr(album, 'thumb') or not album.thumb: - if debug: print(f" 🎨 Album '{album_title}' has NO THUMB - needs update") + if debug: print(f" Album '{album_title}' has NO THUMB - needs update") return False thumb_url = str(album.thumb) - if debug: print(f" 🔍 Album '{album_title}' artwork URL: {thumb_url}") + if debug: print(f" Album '{album_title}' artwork URL: {thumb_url}") # CONSERVATIVE APPROACH: Only mark as "needs update" in very obvious cases # Case 1: Completely empty or None if not thumb_url or thumb_url.strip() == '': - if debug: print(f" 🎨 Album '{album_title}' has empty URL - needs update") + if debug: print(f" Album '{album_title}' has empty URL - needs update") return False # Case 2: Obvious placeholder text in URL @@ -1784,20 +1784,20 @@ class MetadataUpdateWorker(QThread): thumb_lower = thumb_url.lower() for placeholder in obvious_placeholders: if placeholder in thumb_lower: - if debug: print(f" 🎨 Album '{album_title}' has obvious placeholder ({placeholder}) - needs update") + if debug: print(f" Album '{album_title}' has obvious placeholder ({placeholder}) - needs update") return False # Case 3: Extremely short URLs (likely broken) if len(thumb_url) < 20: - if debug: print(f" 🎨 Album '{album_title}' has very short URL ({len(thumb_url)} chars) - needs update") + if debug: print(f" Album '{album_title}' has very short URL ({len(thumb_url)} chars) - needs update") return False # OTHERWISE: Assume it has valid artwork and SKIP updating - if debug: print(f" ✅ Album '{album_title}' appears to have artwork - SKIPPING (URL: {len(thumb_url)} chars)") + if debug: print(f" Album '{album_title}' appears to have artwork - SKIPPING (URL: {len(thumb_url)} chars)") return True except Exception as e: - if debug: print(f" ❌ Error checking artwork for album '{album_title}': {e}") + if debug: print(f" Error checking artwork for album '{album_title}': {e}") # If we can't check, be conservative and skip updating return True @@ -1819,9 +1819,9 @@ class MetadataUpdateWorker(QThread): # Upload using media client success = self.media_client.update_album_poster(album, image_data) if success: - print(f"✅ Updated artwork for album '{album_title}'") + print(f"Updated artwork for album '{album_title}'") else: - print(f"❌ Failed to upload artwork for album '{album_title}'") + print(f"Failed to upload artwork for album '{album_title}'") return success @@ -1980,7 +1980,7 @@ class DashboardDataProvider(QObject): self.session_completed_downloads += 1 # Emit signal for activity feed with specific track info - self.activity_item_added.emit("📥", "Download Complete", f"'{title}' by {artist}", "Now") + self.activity_item_added.emit("", "Download Complete", f"'{title}' by {artist}", "Now") def update_service_status(self, service: str, connected: bool, response_time: float = 0.0, error: str = ""): if service in self.service_status: @@ -2734,7 +2734,7 @@ class DashboardPage(QWidget): self.scan_manager = MediaScanManager(delay_seconds=60) # Add automatic incremental database update after scan completion self.scan_manager.add_scan_completion_callback(self._on_media_scan_completed) - logger.info("✅ MediaScanManager initialized for Dashboard wishlist modal") + logger.info("MediaScanManager initialized for Dashboard wishlist modal") except Exception as e: logger.error(f"Failed to initialize MediaScanManager: {e}") @@ -2792,7 +2792,7 @@ class DashboardPage(QWidget): return # All conditions met - start incremental update - logger.info(f"🎵 Starting automatic incremental database update after {active_server.upper()} scan") + logger.info(f"Starting automatic incremental database update after {active_server.upper()} scan") self._start_automatic_incremental_update() except Exception as e: @@ -2843,9 +2843,9 @@ class DashboardPage(QWidget): """Handle completion of automatic database update""" try: if successful > 0: - logger.info(f"✅ Automatic database update completed: {successful} items processed successfully") + logger.info(f"Automatic database update completed: {successful} items processed successfully") else: - logger.info("💡 Automatic database update completed - no new content found") + logger.info("Automatic database update completed - no new content found") self.refresh_database_statistics() # Clean up the worker if hasattr(self, '_auto_database_worker'): @@ -2970,7 +2970,7 @@ class DashboardPage(QWidget): buttons_layout.setSpacing(10) # Wishlist button - self.wishlist_button = QPushButton("🎵 Wishlist (0)") + self.wishlist_button = QPushButton("Wishlist (0)") self.wishlist_button.setFixedHeight(45) self.wishlist_button.setFixedWidth(150) self.wishlist_button.clicked.connect(self.on_wishlist_button_clicked) @@ -2997,7 +2997,7 @@ class DashboardPage(QWidget): """) # Watchlist button - self.watchlist_button = QPushButton("👁️ Watchlist (0)") + self.watchlist_button = QPushButton("Watchlist (0)") self.watchlist_button.setFixedHeight(45) self.watchlist_button.setFixedWidth(150) self.watchlist_button.clicked.connect(self.on_watchlist_button_clicked) @@ -3166,7 +3166,7 @@ class DashboardPage(QWidget): self.activity_layout = activity_layout # Add initial placeholder - placeholder_item = ActivityItem("📊", "System Started", "Dashboard initialized successfully", "Now") + placeholder_item = ActivityItem("", "System Started", "Dashboard initialized successfully", "Now") activity_layout.addWidget(placeholder_item) layout.addWidget(header_label) @@ -3192,7 +3192,7 @@ class DashboardPage(QWidget): card.status_text.setText("Testing connection...") # Add activity item for test initiation - self.add_activity_item("🔍", f"Testing {service.capitalize()}", "Connection test initiated", "Now") + self.add_activity_item("", f"Testing {service.capitalize()}", "Connection test initiated", "Now") # Start test self.data_provider.test_service_connection(service) @@ -3216,7 +3216,7 @@ class DashboardPage(QWidget): # Check that we have a data provider if not hasattr(self, 'data_provider'): - self.add_activity_item("❌", "Database Update", "Service clients not available", "Now") + self.add_activity_item("", "Database Update", "Service clients not available", "Now") return # Get the active media server and check if client is available @@ -3224,13 +3224,13 @@ class DashboardPage(QWidget): active_server = config_manager.get_active_media_server() if active_server == "plex" and not self.data_provider.service_clients.get('plex_client'): - self.add_activity_item("❌", "Database Update", "Plex client not available", "Now") + self.add_activity_item("", "Database Update", "Plex client not available", "Now") return elif active_server == "jellyfin": # Jellyfin client will be created on-demand, just verify config exists jellyfin_config = config_manager.get_jellyfin_config() if not jellyfin_config.get('base_url') or not jellyfin_config.get('api_key'): - self.add_activity_item("❌", "Database Update", "Jellyfin not configured", "Now") + self.add_activity_item("", "Database Update", "Jellyfin not configured", "Now") return try: @@ -3242,7 +3242,7 @@ class DashboardPage(QWidget): reply = QMessageBox.question( self, "Confirm Full Database Refresh", - "⚠️ You've selected FULL REFRESH mode.\n\n" + "You've selected FULL REFRESH mode.\n\n" "This will completely rebuild your database and may take several minutes.\n" "All existing data will be cleared and rebuilt from your Plex library.\n\n" "Are you sure you want to continue?", @@ -3267,7 +3267,7 @@ class DashboardPage(QWidget): media_client = JellyfinClient() else: logger.error(f"Unknown active server: {active_server}") - self.add_activity_item("❌", "Database Update", f"Unknown server type: {active_server}", "Now") + self.add_activity_item("", "Database Update", f"Unknown server type: {active_server}", "Now") return # Start the database update worker @@ -3289,7 +3289,7 @@ class DashboardPage(QWidget): self.database_widget.update_progress(True, "Initializing...", 0, 0, 0.0) update_type = "Full refresh" if full_refresh else "Incremental update" server_display = active_server.title() # "Plex" or "Jellyfin" - self.add_activity_item("🗄️", "Database Update", f"Starting {update_type.lower()} from {server_display}...", "Now") + self.add_activity_item("", "Database Update", f"Starting {update_type.lower()} from {server_display}...", "Now") self.database_worker.start() @@ -3297,7 +3297,7 @@ class DashboardPage(QWidget): self.start_database_stats_refresh() except Exception as e: - self.add_activity_item("❌", "Database Update", f"Failed to start: {str(e)}", "Now") + self.add_activity_item("", "Database Update", f"Failed to start: {str(e)}", "Now") def stop_database_update(self): """Stop the database update process""" @@ -3308,7 +3308,7 @@ class DashboardPage(QWidget): self.database_worker.terminate() self.database_widget.update_progress(False, "", 0, 0, 0.0) - self.add_activity_item("⏹️", "Database Update", "Stopped database update process", "Now") + self.add_activity_item("", "Database Update", "Stopped database update process", "Now") # Stop statistics refresh timer self.stop_database_stats_refresh() @@ -3320,15 +3320,15 @@ class DashboardPage(QWidget): def on_database_artist_processed(self, artist_name: str, success: bool, details: str, album_count: int, track_count: int): """Handle individual artist processing completion""" if success: - self.add_activity_item("✅", "Artist Processed", f"'{artist_name}' - {details}", "Now") + self.add_activity_item("", "Artist Processed", f"'{artist_name}' - {details}", "Now") else: - self.add_activity_item("❌", "Artist Failed", f"'{artist_name}' - {details}", "Now") + self.add_activity_item("", "Artist Failed", f"'{artist_name}' - {details}", "Now") def on_database_finished(self, total_artists: int, total_albums: int, total_tracks: int, successful: int, failed: int): """Handle database update completion""" self.database_widget.update_progress(False, "", 0, 0, 0.0) summary = f"Processed {total_artists} artists, {total_albums} albums, {total_tracks} tracks" - self.add_activity_item("🗄️", "Database Complete", summary, "Now") + self.add_activity_item("", "Database Complete", summary, "Now") # Stop statistics refresh timer and do final update self.stop_database_stats_refresh() @@ -3337,7 +3337,7 @@ class DashboardPage(QWidget): def on_database_error(self, error_message: str): """Handle database update error""" self.database_widget.update_progress(False, "", 0, 0, 0.0) - self.add_activity_item("❌", "Database Error", error_message, "Now") + self.add_activity_item("", "Database Error", error_message, "Now") # Stop statistics refresh timer self.stop_database_stats_refresh() @@ -3461,16 +3461,16 @@ class DashboardPage(QWidget): if active_server == "jellyfin": media_client = self.data_provider.service_clients.get('jellyfin_client') if not media_client: - self.add_activity_item("❌", "Metadata Update", "Jellyfin client not available", "Now") + self.add_activity_item("", "Metadata Update", "Jellyfin client not available", "Now") return else: media_client = self.data_provider.service_clients.get('plex_client') if not media_client: - self.add_activity_item("❌", "Metadata Update", "Plex client not available", "Now") + self.add_activity_item("", "Metadata Update", "Plex client not available", "Now") return if not self.data_provider.service_clients.get('spotify_client'): - self.add_activity_item("❌", "Metadata Update", "Spotify client not available", "Now") + self.add_activity_item("", "Metadata Update", "Spotify client not available", "Now") return try: @@ -3496,19 +3496,19 @@ class DashboardPage(QWidget): # Update UI and start if self.metadata_widget: self.metadata_widget.update_progress(True, "Loading artists...", 0, 0, 0.0) - self.add_activity_item("🎵", "Metadata Update", "Loading artists from library...", "Now") + self.add_activity_item("", "Metadata Update", "Loading artists from library...", "Now") self.metadata_worker.start() except Exception as e: - self.add_activity_item("❌", "Metadata Update", f"Failed to start: {str(e)}", "Now") + self.add_activity_item("", "Metadata Update", f"Failed to start: {str(e)}", "Now") def on_artists_loaded(self, total_artists, artists_to_process): """Handle when artists are loaded and filtered""" if artists_to_process == 0: - self.add_activity_item("✅", "Metadata Update", "All artists already have good metadata", "Now") + self.add_activity_item("", "Metadata Update", "All artists already have good metadata", "Now") else: - self.add_activity_item("🎵", "Metadata Update", f"Processing {artists_to_process} of {total_artists} artists", "Now") + self.add_activity_item("", "Metadata Update", f"Processing {artists_to_process} of {total_artists} artists", "Now") def stop_metadata_update(self): """Stop the metadata update process""" @@ -3520,7 +3520,7 @@ class DashboardPage(QWidget): if self.metadata_widget: self.metadata_widget.update_progress(False, "", 0, 0, 0.0) - self.add_activity_item("⏹️", "Metadata Update", "Stopped metadata update process", "Now") + self.add_activity_item("", "Metadata Update", "Stopped metadata update process", "Now") def artist_needs_processing(self, artist): """Check if an artist needs metadata processing using smart detection""" @@ -3564,22 +3564,22 @@ class DashboardPage(QWidget): def on_artist_updated(self, artist_name, success, details): """Handle individual artist update completion""" if success: - self.add_activity_item("✅", "Artist Updated", f"'{artist_name}' - {details}", "Now") + self.add_activity_item("", "Artist Updated", f"'{artist_name}' - {details}", "Now") else: - self.add_activity_item("❌", "Artist Failed", f"'{artist_name}' - {details}", "Now") + self.add_activity_item("", "Artist Failed", f"'{artist_name}' - {details}", "Now") def on_metadata_finished(self, total_processed, successful, failed): """Handle metadata update completion""" if self.metadata_widget: self.metadata_widget.update_progress(False, "", 0, 0, 0.0) summary = f"Processed {total_processed} artists: {successful} updated, {failed} failed" - self.add_activity_item("🎵", "Metadata Complete", summary, "Now") + self.add_activity_item("", "Metadata Complete", summary, "Now") def on_metadata_error(self, error_message): """Handle metadata update error""" if self.metadata_widget: self.metadata_widget.update_progress(False, "", 0, 0, 0.0) - self.add_activity_item("❌", "Metadata Error", error_message, "Now") + self.add_activity_item("", "Metadata Error", error_message, "Now") def on_service_status_updated(self, service: str, connected: bool, response_time: float, error: str): """Handle service status updates from data provider""" @@ -3591,7 +3591,7 @@ class DashboardPage(QWidget): self.previous_service_status[service] = connected status = "Connected" if connected else "Disconnected" - icon = "✅" if connected else "❌" + icon = "" if connected else "" self.add_activity_item(icon, f"{service.capitalize()} {status}", f"Response time: {response_time:.0f}ms" if connected else f"Error: {error}" if error else "Connection test completed", "Now") @@ -3676,20 +3676,20 @@ class DashboardPage(QWidget): from ui.components.toast_manager import ToastType # Success activities that deserve toasts - if icon == "✅" and any(keyword in title.lower() for keyword in ["download started", "sync completed", "complete"]): + if icon == "" and any(keyword in title.lower() for keyword in ["download started", "sync completed", "complete"]): self.toast_manager.success(f"{title}: {subtitle}") return - if icon == "📥" and "Download Started" in title: + if icon == "" and "Download Started" in title: self.toast_manager.success(f"{subtitle}") return - if icon == "🔍" and "Search Complete" in title: + if icon == "" and "Search Complete" in title: self.toast_manager.info(f"{subtitle}") return # Error activities that need immediate attention - if icon == "❌": + if icon == "": # Skip routine background errors if any(skip_term in title.lower() for skip_term in ["metadata", "connection test", "routine"]): return @@ -3700,12 +3700,12 @@ class DashboardPage(QWidget): return # Warning activities - if icon == "⚠️": + if icon == "": self.toast_manager.warning(f"{title}: {subtitle}") return # Info activities for searches and connections - if icon == "🔍" and "Search Started" in title: + if icon == "" and "Search Started" in title: self.toast_manager.info(f"{subtitle}") return @@ -3811,7 +3811,7 @@ class DashboardPage(QWidget): count = self.wishlist_service.get_wishlist_count() if hasattr(self, 'wishlist_button'): - self.wishlist_button.setText(f"🎵 Wishlist ({count})") + self.wishlist_button.setText(f"Wishlist ({count})") # Enable/disable button based on count if count == 0: @@ -3913,7 +3913,7 @@ class DashboardPage(QWidget): count = database.get_watchlist_count() if hasattr(self, 'watchlist_button'): - self.watchlist_button.setText(f"👁️ Watchlist ({count})") + self.watchlist_button.setText(f"Watchlist ({count})") # Enable/disable button based on count if count == 0: diff --git a/ui/pages/downloads.py b/ui/pages/downloads.py index 2c04fcb6..40f1e068 100644 --- a/ui/pages/downloads.py +++ b/ui/pages/downloads.py @@ -97,7 +97,7 @@ class DownloadCompletionWorker(QRunnable): def run(self): """Process download completion in background thread""" try: - print(f"🧵 Background worker processing: '{self.download_item.title}' by '{self.download_item.matched_artist.name}'") + print(f"Background worker processing: '{self.download_item.title}' by '{self.download_item.matched_artist.name}'") # Add a small delay to ensure file is fully written import time @@ -110,7 +110,7 @@ class DownloadCompletionWorker(QRunnable): self.signals.completed.emit(self.download_item, organized_path or self.absolute_file_path) except Exception as e: - print(f"❌ Error in background worker: {e}") + print(f"Error in background worker: {e}") import traceback traceback.print_exc() # Emit error signal @@ -280,7 +280,7 @@ class OptimizedDownloadCompletionWorker(QRunnable): raise FileNotFoundError(f"Download file not found: {self.absolute_file_path}") except Exception as e: - print(f"❌ Error in optimized worker: {e}") + print(f"Error in optimized worker: {e}") import traceback traceback.print_exc() self.signals.error.emit(self.download_item, str(e)) @@ -552,7 +552,7 @@ class SpotifyMatchingModal(QDialog): self.confirm_btn.setEnabled(True) self.confirm_btn.setText(f"Confirm: {artist.name[:30]}...") # Update header to indicate completion - self.header_label.setText("✅ Artist Selected - Ready to Download") + self.header_label.setText("Artist Selected - Ready to Download") else: # For album mode, proceed to album selection self.transition_to_album_stage() @@ -746,12 +746,12 @@ class ArtistSuggestionThread(QThread): def run(self): """Generate artist suggestions""" try: - print(f"🔍 Starting auto suggestions for: {self.track_result.artist} - {self.track_result.title}") + print(f"Starting auto suggestions for: {self.track_result.artist} - {self.track_result.title}") suggestions = self.generate_artist_suggestions() - print(f"✅ Generated {len(suggestions)} auto suggestions") + print(f"Generated {len(suggestions)} auto suggestions") self.suggestions_ready.emit(suggestions) except Exception as e: - print(f"❌ Error generating suggestions: {e}") + print(f"Error generating suggestions: {e}") self.suggestions_ready.emit([]) def generate_artist_suggestions(self) -> List[ArtistMatch]: @@ -759,7 +759,7 @@ class ArtistSuggestionThread(QThread): suggestions = [] # Debug logging - print(f"🔍 [DEBUG] Auto suggestion input data:") + print(f"[DEBUG] Auto suggestion input data:") print(f" track_result.artist: '{getattr(self.track_result, 'artist', 'NOT_FOUND')}'") print(f" track_result.title: '{getattr(self.track_result, 'title', 'NOT_FOUND')}'") print(f" track_result.album: '{getattr(self.track_result, 'album', 'NOT_FOUND')}'") @@ -772,7 +772,7 @@ class ArtistSuggestionThread(QThread): # Special handling for albums - use album title to find artist instead of track data if self.is_album and self.album_result and self.album_result.album_title: - print(f"🎵 [DEBUG] Album mode detected - using album title for artist search") + print(f"[DEBUG] Album mode detected - using album title for artist search") print(f" album_title: '{self.album_result.album_title}'") print(f" album_artist: '{getattr(self.album_result, 'artist', 'NOT_FOUND')}'") @@ -783,9 +783,9 @@ class ArtistSuggestionThread(QThread): print(f" clean_album_title: '{clean_album_title}'") # Strategy: Search tracks using album title to find the artist - print(f"🔍 Album Strategy: Searching tracks for album '{clean_album_title}'") + print(f"Album Strategy: Searching tracks for album '{clean_album_title}'") tracks = self.spotify_client.search_tracks(clean_album_title, limit=20) - print(f"📊 Found {len(tracks)} tracks from album search") + print(f"Found {len(tracks)} tracks from album search") # Collect unique artist names and their associated tracks/albums first unique_artists = {} # artist_name -> list of (track, album) tuples @@ -795,7 +795,7 @@ class ArtistSuggestionThread(QThread): unique_artists[artist_name] = [] unique_artists[artist_name].append((track, track.album)) - print(f"🚀 [PERF] Found {len(unique_artists)} unique artists to lookup (down from {sum(len(track.artists) for track in tracks)} total)") + print(f"[PERF] Found {len(unique_artists)} unique artists to lookup (down from {sum(len(track.artists) for track in tracks)} total)") # Batch fetch artist objects using concurrent futures for speed from concurrent.futures import ThreadPoolExecutor, as_completed @@ -811,7 +811,7 @@ class ArtistSuggestionThread(QThread): if matches: return artist_name, matches[0] except Exception as e: - print(f"⚠️ Error fetching artist '{artist_name}': {e}") + print(f"Error fetching artist '{artist_name}': {e}") return artist_name, None # Use limited concurrency to respect rate limits while improving speed @@ -824,7 +824,7 @@ class ArtistSuggestionThread(QThread): artist_objects[artist_name] = artist_obj fetch_time = time.time() - start_time - print(f"⚡ [PERF] Fetched {len(artist_objects)} artists in {fetch_time:.2f}s using concurrent API calls") + print(f"[PERF] Fetched {len(artist_objects)} artists in {fetch_time:.2f}s using concurrent API calls") # Now calculate confidence scores for each artist artist_scores = {} @@ -867,14 +867,14 @@ class ArtistSuggestionThread(QThread): # Add high-confidence album artists to suggestions for artist_data in artist_scores.values(): if artist_data['confidence'] >= 0.6: # Higher threshold for album matches - print(f"✅ Added album artist match: {artist_data['artist'].name} ({artist_data['confidence']:.2f}) via '{artist_data['album_match']}'") + print(f"Added album artist match: {artist_data['artist'].name} ({artist_data['confidence']:.2f}) via '{artist_data['album_match']}'") suggestions.append(ArtistMatch( artist=artist_data['artist'], confidence=artist_data['confidence'], match_reason=f"Album match via '{artist_data['album_match']}'" )) - print(f"🎯 [DEBUG] Album strategy generated {len(suggestions)} suggestions") + print(f"[DEBUG] Album strategy generated {len(suggestions)} suggestions") # If we found good album matches, return them (don't try track-based strategies) if suggestions: @@ -885,7 +885,7 @@ class ArtistSuggestionThread(QThread): unique_suggestions[suggestion.artist.id] = suggestion final_suggestions = sorted(unique_suggestions.values(), key=lambda x: x.confidence, reverse=True) - print(f"🎯 [DEBUG] Returning {len(final_suggestions)} album-based suggestions") + print(f"[DEBUG] Returning {len(final_suggestions)} album-based suggestions") return final_suggestions[:5] # Try to get artist name from different sources (for singles or fallback) @@ -902,14 +902,14 @@ class ArtistSuggestionThread(QThread): if ' - ' in filename: artist_name = filename.split(' - ')[0].strip() - print(f"🎯 [DEBUG] Determined artist name: '{artist_name}'") + print(f"[DEBUG] Determined artist name: '{artist_name}'") # Strategy 1: Search for the artist name directly if artist_name and artist_name != "Unknown Artist": artist_query = self.matching_engine.normalize_string(artist_name) - print(f"🔍 Strategy 1: Searching for artist '{artist_query}'") + print(f"Strategy 1: Searching for artist '{artist_query}'") artists = self.spotify_client.search_artists(artist_query, limit=10) - print(f"📊 Found {len(artists)} artists from Spotify") + print(f"Found {len(artists)} artists from Spotify") for artist in artists: confidence = self.matching_engine.similarity_score( @@ -918,21 +918,21 @@ class ArtistSuggestionThread(QThread): ) if confidence >= 0.3: # Minimum threshold - print(f"✅ Added artist match: {artist.name} ({confidence:.2f})") + print(f"Added artist match: {artist.name} ({confidence:.2f})") suggestions.append(ArtistMatch( artist=artist, confidence=confidence, match_reason="Artist name match" )) else: - print(f"❌ Strategy 1 skipped: artist_name='{artist_name}', original_artist='{getattr(self.track_result, 'artist', 'NO_ATTR')}'") + print(f"Strategy 1 skipped: artist_name='{artist_name}', original_artist='{getattr(self.track_result, 'artist', 'NO_ATTR')}'") # Strategy 2: Search for "artist - title" combination if artist_name and self.track_result.title: combined_query = f"{artist_name} {self.track_result.title}" - print(f"🔍 Strategy 2: Searching for combined query '{combined_query}'") + print(f"Strategy 2: Searching for combined query '{combined_query}'") tracks = self.spotify_client.search_tracks(combined_query, limit=10) - print(f"📊 Found {len(tracks)} tracks from Spotify") + print(f"Found {len(tracks)} tracks from Spotify") for track in tracks: for artist_name in track.artists: @@ -969,7 +969,7 @@ class ArtistSuggestionThread(QThread): final_suggestions = sorted(unique_suggestions.values(), key=lambda x: x.confidence, reverse=True) # Debug final results - print(f"🎯 [DEBUG] Final suggestions count: {len(final_suggestions)}") + print(f"[DEBUG] Final suggestions count: {len(final_suggestions)}") for i, suggestion in enumerate(final_suggestions[:5]): print(f" {i+1}. {suggestion.artist.name} ({suggestion.confidence:.2f}) - {suggestion.match_reason}") @@ -1094,13 +1094,13 @@ class AlbumSuggestionThread(QThread): target_album_name = "" if isinstance(self.original_result, AlbumResult): target_album_name = self.original_result.album_title - print(f"🎵 Album context for auto-match from AlbumResult: '{target_album_name}'") + print(f"Album context for auto-match from AlbumResult: '{target_album_name}'") elif hasattr(self.original_result, 'album') and self.original_result.album: target_album_name = self.original_result.album - print(f"🎵 Album context for auto-match from TrackResult: '{target_album_name}'") + print(f"Album context for auto-match from TrackResult: '{target_album_name}'") else: target_album_name = self.original_result.title - print(f"🎵 Album context for auto-match using fallback to track title: '{target_album_name}'") + print(f"Album context for auto-match using fallback to track title: '{target_album_name}'") if not target_album_name: self.suggestions_ready.emit([]) @@ -1117,20 +1117,20 @@ class AlbumSuggestionThread(QThread): # 3. Clean up any remaining leading/trailing dashes or spaces. cleaned_search_term = cleaned_search_term.strip(' -') - print(f"🧹 Cleaned album search term: '{target_album_name}' -> '{cleaned_search_term}'") + print(f"Cleaned album search term: '{target_album_name}' -> '{cleaned_search_term}'") search_query = f"artist:{self.artist.name} album:{cleaned_search_term}" - print(f"🔍 Searching Spotify for albums with query: '{search_query}'") + print(f"Searching Spotify for albums with query: '{search_query}'") spotify_albums = self.spotify_client.search_albums(search_query, limit=10) - print(f"📊 Found {len(spotify_albums)} potential albums from Spotify search.") + print(f"Found {len(spotify_albums)} potential albums from Spotify search.") suggestions = [] for album in spotify_albums: is_correct_artist = any(self.matching_engine.similarity_score(self.artist.name, art) > 0.85 for art in album.artists) if not is_correct_artist: - print(f"⚠️ Skipping album '{album.name}' as artist does not match '{self.artist.name}'") + print(f"Skipping album '{album.name}' as artist does not match '{self.artist.name}'") continue confidence = self.matching_engine.similarity_score( @@ -1142,7 +1142,7 @@ class AlbumSuggestionThread(QThread): confidence = max(confidence, 0.95) if confidence >= 0.5: - print(f"✅ Found album match: '{album.name}' with confidence {confidence:.2f}") + print(f"Found album match: '{album.name}' with confidence {confidence:.2f}") suggestions.append(AlbumMatch( album=album, confidence=confidence, @@ -1173,16 +1173,16 @@ class AlbumSuggestionThread(QThread): is_new_better = True if is_new_better: - print(f"🔄 Replacing duplicate album '{album_key}' with a better version (type: {suggestion.album.album_type}, tracks: {getattr(suggestion.album, 'total_tracks', 'N/A')})") + print(f"Replacing duplicate album '{album_key}' with a better version (type: {suggestion.album.album_type}, tracks: {getattr(suggestion.album, 'total_tracks', 'N/A')})") unique_suggestions[album_key] = suggestion final_suggestions = sorted(unique_suggestions.values(), key=lambda x: x.confidence, reverse=True) - print(f"✅ Generated {len(final_suggestions)} final album suggestions.") + print(f"Generated {len(final_suggestions)} final album suggestions.") self.suggestions_ready.emit(final_suggestions[:4]) except Exception as e: - print(f"❌ Error generating album suggestions: {e}") + print(f"Error generating album suggestions: {e}") import traceback traceback.print_exc() self.suggestions_ready.emit([]) @@ -1361,7 +1361,7 @@ class AudioPlayer(QMediaPlayer): QMediaPlayer.PlaybackState.PlayingState: "PLAYING", QMediaPlayer.PlaybackState.PausedState: "PAUSED" } - print(f"🎵 AudioPlayer state changed to: {state_names.get(state, 'UNKNOWN')}") + print(f"AudioPlayer state changed to: {state_names.get(state, 'UNKNOWN')}") self.is_playing = (state == QMediaPlayer.PlaybackState.PlayingState) def play_file(self, file_path): @@ -1382,7 +1382,7 @@ class AudioPlayer(QMediaPlayer): self.play() # is_playing will be set automatically by _on_playback_state_changed - print(f"🎵 Started playing: {os.path.basename(file_path)}") + print(f"Started playing: {os.path.basename(file_path)}") return True except Exception as e: @@ -1394,20 +1394,20 @@ class AudioPlayer(QMediaPlayer): def toggle_playback(self): """Toggle between play and pause""" current_state = self.playbackState() - print(f"🔄 toggle_playback() - Current state: {current_state}") - print(f"🔄 toggle_playback() - Current source: {self.source().toString()}") + print(f"toggle_playback() - Current state: {current_state}") + print(f"toggle_playback() - Current source: {self.source().toString()}") if current_state == QMediaPlayer.PlaybackState.PlayingState: - print("⏸️ AudioPlayer: Pausing playback") + print("AudioPlayer: Pausing playback") self.pause() # is_playing will be set automatically by _on_playback_state_changed return False # Now paused else: - print("▶️ AudioPlayer: Attempting to resume/play") + print("AudioPlayer: Attempting to resume/play") # Check if we have a valid source to play if not self.source().isValid() and self.current_file_path: - print(f"🔧 AudioPlayer: No source set, restoring from: {self.current_file_path}") + print(f"AudioPlayer: No source set, restoring from: {self.current_file_path}") self.setSource(QUrl.fromLocalFile(self.current_file_path)) self.play() @@ -1416,7 +1416,7 @@ class AudioPlayer(QMediaPlayer): def stop_playback(self): """Stop playback and reset""" - print("⏹️ AudioPlayer: stop_playback() called") + print("AudioPlayer: stop_playback() called") self.stop() # is_playing will be set automatically by _on_playback_state_changed self.release_file() @@ -1428,28 +1428,28 @@ class AudioPlayer(QMediaPlayer): clear_file_path (bool): Whether to clear the stored file path. Set to False to keep the path for potential resuming. """ - print(f"🔓 AudioPlayer: release_file() called - clearing source: {self.source().toString()}") + print(f"AudioPlayer: release_file() called - clearing source: {self.source().toString()}") self.setSource(QUrl()) # Clear the media source to release file handle if clear_file_path: self.current_file_path = None - print("🔓 Released audio file handle") + print("Released audio file handle") def _on_media_status_changed(self, status): """Handle media status changes""" if status == QMediaPlayer.MediaStatus.EndOfMedia: - print("🎵 Playback finished") + print("Playback finished") # is_playing will be set automatically by _on_playback_state_changed self.playback_finished.emit() elif status == QMediaPlayer.MediaStatus.InvalidMedia: error_msg = "Invalid media file or unsupported format" - print(f"❌ {error_msg}") + print(f"{error_msg}") # is_playing will be set automatically by _on_playback_state_changed self.playback_error.emit(error_msg) def _on_error_occurred(self, error, error_string): """Handle playback errors""" error_msg = f"Audio playback error: {error_string}" - print(f"❌ {error_msg}") + print(f"{error_msg}") # is_playing will be set automatically by _on_playback_state_changed self.playback_error.emit(error_msg) @@ -1730,7 +1730,7 @@ class ApiCleanupThread(QThread): self.cleanup_completed.emit(success, self.download_id, self.username) except Exception as e: - print(f"⚠️ Error in API cleanup thread: {e}") + print(f"Error in API cleanup thread: {e}") self.cleanup_completed.emit(False, self.download_id, self.username) finally: # Ensure proper cleanup @@ -1961,12 +1961,12 @@ class StreamingThread(QThread): # Track 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}") + print(f"Download entered queue state: {original_state}") self.streaming_queued.emit(f"Queuing with uploader...", self.search_result) 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}") + print(f"Download started actively downloading: {original_state}") # Emit a progress update to indicate downloading has started if api_progress > 0: self.streaming_progress.emit(api_progress, self.search_result) @@ -1981,19 +1981,19 @@ class StreamingThread(QThread): # Check if download is complete if is_completed: - print(f"✓ Download completed via API status: {original_state}") + print(f"Download completed via API status: {original_state}") # Try to find the actual file - with retries for file system sync for retry_count in range(5): # Try up to 5 times with delays found_file = self._find_downloaded_file(download_path) if found_file: - print(f"✓ Found completed file after {retry_count} retries: {found_file}") + print(f"Found completed file after {retry_count} retries: {found_file}") break else: - print(f"⏳ File not found yet, waiting... (retry {retry_count + 1}/5)") + print(f"File not found yet, waiting... (retry {retry_count + 1}/5)") time.sleep(1) # Wait 1 second for file system to sync if found_file: - print(f"✓ Found downloaded file: {found_file}") + print(f"Found downloaded file: {found_file}") # Move the file to Stream folder with original filename original_filename = os.path.basename(found_file) @@ -2002,7 +2002,7 @@ class StreamingThread(QThread): try: # Move file to Stream folder shutil.move(found_file, stream_path) - print(f"✓ Moved file to stream folder: {stream_path}") + print(f"Moved file to stream folder: {stream_path}") # Clean up empty directories left behind self._cleanup_empty_directories(download_path, found_file) @@ -2011,7 +2011,7 @@ class StreamingThread(QThread): self.streaming_progress.emit(100.0, self.search_result) self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result) self.temp_file_path = stream_path - print(f"✓ Stream file ready for playback: {stream_path}") + print(f"Stream file ready for playback: {stream_path}") # Signal API that download is complete try: @@ -2029,11 +2029,11 @@ class StreamingThread(QThread): ) loop.close() if success: - print(f"✓ Successfully signaled completion for download {download_id}") + print(f"Successfully signaled completion for download {download_id}") else: - print(f"⚠️ Failed to signal completion for download {download_id}") + print(f"Failed to signal completion for download {download_id}") except Exception as e: - print(f"⚠️ Error signaling download completion: {e}") + print(f"Error signaling download completion: {e}") break # Exit main polling loop @@ -2057,7 +2057,7 @@ class StreamingThread(QThread): found_file = self._find_downloaded_file(download_path) if found_file: - print(f"✓ Found downloaded file: {found_file}") + print(f"Found downloaded file: {found_file}") # Move the file to Stream folder with original filename original_filename = os.path.basename(found_file) @@ -2066,7 +2066,7 @@ class StreamingThread(QThread): try: # Move file to Stream folder shutil.move(found_file, stream_path) - print(f"✓ Moved file to stream folder: {stream_path}") + print(f"Moved file to stream folder: {stream_path}") # Clean up empty directories left behind self._cleanup_empty_directories(download_path, found_file) @@ -2075,7 +2075,7 @@ class StreamingThread(QThread): self.streaming_progress.emit(100.0, self.search_result) self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result) self.temp_file_path = stream_path - print(f"✓ Stream file ready for playback: {stream_path}") + print(f"Stream file ready for playback: {stream_path}") break except Exception as e: @@ -2088,7 +2088,7 @@ class StreamingThread(QThread): time.sleep(poll_interval) else: # Timed out waiting for file - print(f"❌ Polling loop completed, timeout reached. found_file = {found_file}") + print(f"Polling loop completed, timeout reached. found_file = {found_file}") self.streaming_failed.emit("Stream download timed out - file not found", self.search_result) else: @@ -2131,21 +2131,21 @@ class StreamingThread(QThread): target_filename = os.path.basename(self.search_result.filename) target_username = self.search_result.username - print(f"🔍 Looking for streaming download - Target: {target_username}:{target_filename}") - print(f"🔍 Found {len(all_transfers)} total transfers in API") + print(f"Looking for streaming download - Target: {target_username}:{target_filename}") + print(f"Found {len(all_transfers)} total transfers in API") for i, transfer in enumerate(all_transfers): transfer_filename = os.path.basename(transfer.get('filename', '')) transfer_username = transfer.get('username', '') - print(f"📁 Transfer {i+1}: {transfer_username}:{transfer_filename} - State: {transfer.get('state')} - Progress: {transfer.get('percentComplete', 0):.1f}%") + print(f"Transfer {i+1}: {transfer_username}:{transfer_filename} - State: {transfer.get('state')} - Progress: {transfer.get('percentComplete', 0):.1f}%") if (transfer_filename == target_filename and transfer_username == target_username): - print(f"✅ Found matching streaming download: {transfer.get('state')} - {transfer.get('percentComplete', 0):.1f}%") + print(f"Found matching streaming download: {transfer.get('state')} - {transfer.get('percentComplete', 0):.1f}%") return transfer - print(f"❌ No matching streaming download found for {target_username}:{target_filename}") + print(f"No matching streaming download found for {target_username}:{target_filename}") return None except Exception as e: print(f"Error finding streaming download in transfers: {e}") @@ -2282,7 +2282,7 @@ class TrackItem(QFrame): # Always show artist information prominently for tracks within albums if self.track_result.artist: - details.append(f"🎤 {self.track_result.artist}") + details.append(f"{self.track_result.artist}") details.append(self.track_result.quality.upper()) if self.track_result.bitrate: @@ -2309,7 +2309,7 @@ class TrackItem(QFrame): button_layout.setSpacing(8) # Play button - play_btn = QPushButton("▶️") + play_btn = QPushButton("") play_btn.setFixedSize(32, 32) play_btn.clicked.connect(self.request_stream) play_btn.setStyleSheet(""" @@ -2344,7 +2344,7 @@ class TrackItem(QFrame): """) # Matched Download button - matched_download_btn = QPushButton("📱") + matched_download_btn = QPushButton("") matched_download_btn.setFixedSize(32, 32) matched_download_btn.clicked.connect(self.request_matched_download) matched_download_btn.setToolTip("Download with Spotify Matching") @@ -2400,12 +2400,12 @@ class TrackItem(QFrame): def set_loading_state(self): """Set play button to loading state""" - self.play_btn.setText("⏳") + self.play_btn.setText("") self.play_btn.setEnabled(False) def set_queue_state(self): """Set play button to queue state""" - self.play_btn.setText("📋") + self.play_btn.setText("") self.play_btn.setEnabled(False) self.play_btn.setStyleSheet(""" QPushButton { @@ -2419,7 +2419,7 @@ class TrackItem(QFrame): def set_download_queued_state(self): """Set download button to queued state (disabled, shows queued)""" - self.download_btn.setText("⏳") + self.download_btn.setText("") self.download_btn.setEnabled(False) self.download_btn.setStyleSheet(""" QPushButton { @@ -2433,7 +2433,7 @@ class TrackItem(QFrame): def set_download_downloading_state(self): """Set download button to downloading state""" - self.download_btn.setText("📥") + self.download_btn.setText("") self.download_btn.setEnabled(False) self.download_btn.setStyleSheet(""" QPushButton { @@ -2447,7 +2447,7 @@ class TrackItem(QFrame): def set_download_completed_state(self): """Set download button to completed state""" - self.download_btn.setText("✅") + self.download_btn.setText("") self.download_btn.setEnabled(False) self.download_btn.setStyleSheet(""" QPushButton { @@ -2478,12 +2478,12 @@ class TrackItem(QFrame): def set_playing_state(self): """Set play button to playing/pause state""" - self.play_btn.setText("⏸️") + self.play_btn.setText("") self.play_btn.setEnabled(True) def reset_play_state(self): """Reset play button to default state""" - self.play_btn.setText("▶️") + self.play_btn.setText("") self.play_btn.setEnabled(True) class AlbumResultItem(QFrame): @@ -2542,7 +2542,7 @@ class AlbumResultItem(QFrame): # Album icon with expand indicator icon_container = QVBoxLayout() - album_icon = QLabel("💿") + album_icon = QLabel("") album_icon.setFixedSize(48, 48) # Larger for better presence album_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) album_icon.setStyleSheet(""" @@ -2558,7 +2558,7 @@ class AlbumResultItem(QFrame): """) # Expand indicator - self.expand_indicator = QLabel("▶") + self.expand_indicator = QLabel("") self.expand_indicator.setFixedSize(20, 20) # Slightly larger self.expand_indicator.setAlignment(Qt.AlignmentFlag.AlignCenter) self.expand_indicator.setStyleSheet(""" @@ -2590,7 +2590,7 @@ class AlbumResultItem(QFrame): # Make artist more prominent by placing it first and with better formatting if self.album_result.artist: - details.append(f"🎤 {self.album_result.artist}") + details.append(f"{self.album_result.artist}") details.append(f"{self.album_result.track_count} tracks") details.append(f"{self.album_result.size_mb}MB") @@ -2609,7 +2609,7 @@ class AlbumResultItem(QFrame): album_details.setStyleSheet("color: rgba(179, 179, 179, 0.9);") # User info - user_info = QLabel(f"👤 {self.album_result.username}") + user_info = QLabel(f"{self.album_result.username}") user_info.setFont(QFont("Arial", 9)) user_info.setStyleSheet("color: rgba(29, 185, 84, 0.8);") @@ -2651,7 +2651,7 @@ class AlbumResultItem(QFrame): """) # Matched Download button - self.matched_download_btn = QPushButton("🎯 Matched Album") + self.matched_download_btn = QPushButton("Matched Album") self.matched_download_btn.setFixedSize(160, 36) # Match the other button self.matched_download_btn.clicked.connect(self.request_matched_album_download) self.matched_download_btn.setToolTip("Download Album with Spotify Matching") @@ -2718,13 +2718,13 @@ class AlbumResultItem(QFrame): def request_album_download(self): """Request download of the entire album""" - self.download_btn.setText("⏳") + self.download_btn.setText("") self.download_btn.setEnabled(False) self.album_download_requested.emit(self.album_result) def request_matched_album_download(self): """Request matched download of the entire album with Spotify integration""" - self.matched_download_btn.setText("⏳") + self.matched_download_btn.setText("") self.matched_download_btn.setEnabled(False) self.matched_album_download_requested.emit(self.album_result) @@ -2743,7 +2743,7 @@ class AlbumResultItem(QFrame): else: # Collapse to hide tracks self.tracks_container.setVisible(False) - self.expand_indicator.setText("▶") + self.expand_indicator.setText("") self.setFixedHeight(self.collapsed_height) # Force layout update @@ -2765,13 +2765,13 @@ class AlbumResultItem(QFrame): if speed > 0: # Use same logic as Singles but return text only (no icons for inline display) if speed > 200: - icon = "🚀" + icon = "" elif speed > 100: - icon = "🚀" if slots > 0 else "⚡" + icon = "" if slots > 0 else "" elif speed > 50: - icon = "⚡" + icon = "" else: - icon = "🐌" + icon = "" # Convert to MB/s and format speed_mb = speed / 1024 @@ -2832,7 +2832,7 @@ class SearchResultItem(QFrame): left_section.setSpacing(12) # Increased from 8px for better separation # Enhanced music icon with modern styling - music_icon = QLabel("🎵") + music_icon = QLabel("") music_icon.setFixedSize(44, 44) # Slightly larger for better presence music_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) music_icon.setStyleSheet(""" @@ -2870,7 +2870,7 @@ class SearchResultItem(QFrame): buttons_layout.setSpacing(8) # Increased from 4px for better button separation # Play button for streaming preview - self.play_btn = QPushButton("▶️") + self.play_btn = QPushButton("") self.play_btn.setFixedSize(46, 46) # Larger for better accessibility self.play_btn.clicked.connect(self.request_stream) self.play_btn.setStyleSheet(""" @@ -2926,7 +2926,7 @@ class SearchResultItem(QFrame): """) # Matched Download button - self.matched_download_btn = QPushButton("🎯") + self.matched_download_btn = QPushButton("") self.matched_download_btn.setFixedSize(46, 46) # Match other buttons self.matched_download_btn.clicked.connect(self.request_matched_download) self.matched_download_btn.setToolTip("Download with Spotify Matching") @@ -3012,7 +3012,7 @@ class SearchResultItem(QFrame): # Add artist information if available if hasattr(result, 'artist') and result.artist: - info_parts.append(f"🎤 {result.artist}") + info_parts.append(f"{result.artist}") # Add quality info quality_text = result.quality.upper() @@ -3032,7 +3032,7 @@ class SearchResultItem(QFrame): self.secondary_info.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) # Create separate uploader info with green styling like albums - self.uploader_info = QLabel(f"👤 {result.username}") + self.uploader_info = QLabel(f"{result.username}") self.uploader_info.setFont(QFont("Arial", 9)) self.uploader_info.setStyleSheet("color: rgba(29, 185, 84, 0.8);") self.uploader_info.setWordWrap(False) @@ -3336,23 +3336,23 @@ class SearchResultItem(QFrame): # Speed-focused logic (slots as bonus, not requirement) if speed > 200: indicator_color = "#1db954" - icon = "🚀" + icon = "" tooltip = f"Very Fast: {speed} KB/s" + (f", {slots} slots" if slots > 0 else "") elif speed > 100: indicator_color = "#1db954" if slots > 0 else "#4CAF50" - icon = "🚀" if slots > 0 else "⚡" + icon = "" if slots > 0 else "" tooltip = f"Fast: {speed} KB/s" + (f", {slots} slots" if slots > 0 else "") elif speed > 50: indicator_color = "#ffa500" - icon = "⚡" + icon = "" tooltip = f"Good: {speed} KB/s" + (f", {slots} slots" if slots > 0 else "") elif speed > 0: indicator_color = "#ffaa00" - icon = "🐌" + icon = "" tooltip = f"Slow: {speed} KB/s" + (f", {slots} slots" if slots > 0 else "") else: indicator_color = "#e22134" - icon = "⏳" + icon = "" tooltip = "No speed data available" # Convert KB/s to MB/s and format nicely @@ -3386,7 +3386,7 @@ class SearchResultItem(QFrame): def request_download(self): if not self.is_downloading: self.is_downloading = True - self.download_btn.setText("⏳") + self.download_btn.setText("") self.download_btn.setEnabled(False) self.download_requested.emit(self.search_result) @@ -3420,13 +3420,13 @@ class SearchResultItem(QFrame): if is_playing: self.set_playing_state() else: - self.play_btn.setText("▶️") # Play icon when paused + self.play_btn.setText("") # Play icon when paused self.play_btn.setEnabled(True) return # Otherwise, start new streaming # Change button state to indicate streaming is starting - self.play_btn.setText("⏸️") # Pause icon to indicate playing + self.play_btn.setText("") # Pause icon to indicate playing self.play_btn.setEnabled(False) # Emit streaming request @@ -3446,14 +3446,14 @@ class SearchResultItem(QFrame): parent = parent.parent() return None - def reset_play_state(self, original_text="▶️"): + def reset_play_state(self, original_text=""): """Reset the play button state""" self.play_btn.setText(original_text) self.play_btn.setEnabled(True) def set_playing_state(self): """Set button to playing state""" - self.play_btn.setText("⏸️") + self.play_btn.setText("") self.play_btn.setEnabled(True) def set_loading_state(self): @@ -3463,7 +3463,7 @@ class SearchResultItem(QFrame): def set_queue_state(self): """Set play button to queue state""" - self.play_btn.setText("📋") + self.play_btn.setText("") self.play_btn.setEnabled(False) self.play_btn.setStyleSheet(""" QPushButton { @@ -3665,7 +3665,7 @@ class DownloadItem(QFrame): } """) else: - self.action_btn.setText("📂 Open") + self.action_btn.setText("Open") self.action_btn.clicked.connect(self.open_download_location) self.action_btn.setStyleSheet(""" QPushButton { @@ -3813,7 +3813,7 @@ class DownloadItem(QFrame): } """) else: - self.action_btn.setText("📂 Open") + self.action_btn.setText("Open") # Disconnect old connections self.action_btn.clicked.disconnect() self.action_btn.clicked.connect(self.open_download_location) @@ -4068,7 +4068,7 @@ class CompactDownloadItem(QFrame): if 'completed' in final_status or 'succeeded' in final_status: # For successfully completed downloads, show the 'Open' button. - open_btn = QPushButton("📂 Open") + open_btn = QPushButton("Open") open_btn.setFixedSize(60, 35) open_btn.clicked.connect(self.open_download_location) open_btn.setStyleSheet(""" @@ -4096,7 +4096,7 @@ class CompactDownloadItem(QFrame): action_layout.addWidget(status_label) else: # Fallback for any other unexpected status - open_btn = QPushButton("📂 Open") + open_btn = QPushButton("Open") open_btn.setFixedSize(60, 35) open_btn.clicked.connect(self.open_download_location) action_layout.addWidget(open_btn) @@ -4237,7 +4237,7 @@ class CompactDownloadItem(QFrame): """Cancel the download using soulseek client""" print(f"[DEBUG] Cancel button clicked - download_id: {self.download_id}, username: {self.username}, title: {self.title}") if self.soulseek_client and self.download_id: - print(f"🚫 Cancelling download: {self.download_id}") + print(f"Cancelling download: {self.download_id}") # Find the parent DownloadsPage to use its async helper parent_page = self.parent() @@ -4249,13 +4249,13 @@ class CompactDownloadItem(QFrame): def on_success(result): print(f"[DEBUG] Cancel result: {result}") if result: - print(f"✅ Successfully cancelled download: {self.title}") + print(f"Successfully cancelled download: {self.title}") self.update_status("cancelled") else: - print(f"❌ Failed to cancel download: {self.title}") + print(f"Failed to cancel download: {self.title}") def on_error(error): - print(f"❌ Failed to cancel download: {error}") + print(f"Failed to cancel download: {error}") parent_page._run_async_operation( self.soulseek_client.cancel_download, @@ -4270,7 +4270,7 @@ class CompactDownloadItem(QFrame): def retry_download(self): """Retry a failed download""" - print(f"🔄 Retrying download: {self.title}") + print(f"Retrying download: {self.title}") # This would trigger a new download attempt # Implementation depends on how retries are handled in the main system self.update_status("queued", 0) @@ -4298,9 +4298,9 @@ class CompactDownloadItem(QFrame): else: # Linux os.system(f'xdg-open "{download_path}"') - print(f"📂 Opened downloads folder: {download_path}") + print(f"Opened downloads folder: {download_path}") except Exception as e: - print(f"❌ Failed to open downloads folder: {e}") + print(f"Failed to open downloads folder: {e}") return try: @@ -4319,9 +4319,9 @@ class CompactDownloadItem(QFrame): else: # Linux os.system(f"xdg-open '{folder_path}'") - print(f"📂 Opened folder: {folder_path}") + print(f"Opened folder: {folder_path}") else: - print(f"❌ File not found: {file_path}") + print(f"File not found: {file_path}") # Try to find the file in the downloads directory using the filename filename = os.path.basename(self.file_path) print(f"[DEBUG] Searching for file: {filename}") @@ -4352,9 +4352,9 @@ class CompactDownloadItem(QFrame): else: # Linux os.system(f'xdg-open "{folder_path}"') - print(f"📂 Opened folder: {folder_path}") + print(f"Opened folder: {folder_path}") else: - print(f"❌ Could not find file {filename} in downloads directory") + print(f"Could not find file {filename} in downloads directory") # Fallback to opening the downloads folder system = platform.system() if system == "Windows": @@ -4364,10 +4364,10 @@ class CompactDownloadItem(QFrame): else: # Linux os.system(f'xdg-open "{download_path}"') - print(f"📂 Opened downloads folder as fallback: {download_path}") + print(f"Opened downloads folder as fallback: {download_path}") except Exception as e: - print(f"❌ Failed to open download location: {e}") + print(f"Failed to open download location: {e}") class DownloadQueue(QFrame): def __init__(self, title="Download Queue", queue_type="active", parent=None): @@ -4733,14 +4733,14 @@ class TabbedDownloadManager(QTabWidget): # Start the thread cleanup_thread.start() - print(f"🧵 Started API cleanup thread for download {download_item.download_id}") + print(f"Started API cleanup thread for download {download_item.download_id}") else: - print(f"⚠️ Cannot find parent DownloadsPage for API cleanup thread") + print(f"Cannot find parent DownloadsPage for API cleanup thread") # Fallback: Skip API cleanup to prevent blocking - print(f"⚠️ Skipping API cleanup for download {download_item.download_id}") + print(f"Skipping API cleanup for download {download_item.download_id}") except Exception as e: - print(f"⚠️ Error setting up download completion cleanup: {e}") + print(f"Error setting up download completion cleanup: {e}") self.update_tab_counts() @@ -4781,9 +4781,9 @@ class TabbedDownloadManager(QTabWidget): thread.wait(1000) # Wait up to 1 second for completion thread.deleteLater() - print(f"🧹 Cleaned up API cleanup thread") + print(f"Cleaned up API cleanup thread") except Exception as e: - print(f"⚠️ Error cleaning up API cleanup thread: {e}") + print(f"Error cleaning up API cleanup thread: {e}") def update_tab_counts(self): """Schedule a batched tab count update to prevent excessive UI updates""" @@ -4886,7 +4886,7 @@ class DownloadsPage(QWidget): download_path = config_manager.get('soulseek.download_path') if download_path and hasattr(self.soulseek_client, 'download_path'): self.soulseek_client.download_path = download_path - print(f"✅ Set soulseek_client download path to: {download_path}") + print(f"Set soulseek_client download path to: {download_path}") # --- END FIX --- self.search_thread = None @@ -5028,7 +5028,7 @@ class DownloadsPage(QWidget): title_section = QVBoxLayout() title_section.setSpacing(6) # Increased for better title hierarchy - title_label = QLabel("🎵 Music Downloads") + title_label = QLabel("Music Downloads") title_label.setFont(QFont("Segoe UI", 28, QFont.Weight.Bold)) # Larger for better prominence title_label.setStyleSheet(""" color: #ffffff; @@ -5253,7 +5253,7 @@ class DownloadsPage(QWidget): """) # Enhanced search button - self.search_btn = QPushButton("🔍 Search") + self.search_btn = QPushButton("Search") self.search_btn.setFixedSize(120, 40) self.search_btn.clicked.connect(self.perform_search) self.search_btn.setStyleSheet(""" @@ -5284,7 +5284,7 @@ class DownloadsPage(QWidget): """) # Cancel search button (initially hidden) - self.cancel_search_btn = QPushButton("✕ Cancel") + self.cancel_search_btn = QPushButton("Cancel") self.cancel_search_btn.setFixedSize(100, 40) self.cancel_search_btn.clicked.connect(self.cancel_search) self.cancel_search_btn.setVisible(False) # Hidden by default @@ -6063,7 +6063,7 @@ class DownloadsPage(QWidget): if total_filtered > 0: if total_filtered > self.results_per_page: filter_status += f" (showing first {len(results_to_show)})" - self.search_status.setText(f"✨ {filter_status} • {total_albums} albums, {total_tracks} singles") + self.search_status.setText(f"{filter_status} • {total_albums} albums, {total_tracks} singles") else: self.search_status.setText(f"No results found for '{self.current_filter}' filter") @@ -6132,7 +6132,7 @@ class DownloadsPage(QWidget): controls_layout.setContentsMargins(10, 10, 10, 10) controls_layout.setSpacing(6) - clear_btn = QPushButton("🗑️ Clear Completed") + clear_btn = QPushButton("Clear Completed") clear_btn.setFixedHeight(28) clear_btn.clicked.connect(self.clear_completed_downloads) clear_btn.setStyleSheet(self._get_control_button_style("#e22134")) @@ -6189,14 +6189,14 @@ class DownloadsPage(QWidget): layout.setContentsMargins(16, 8, 16, 8) layout.setSpacing(12) - connection_status = QLabel("🟢 slskd Connected") + connection_status = QLabel("slskd Connected") connection_status.setFont(QFont("Arial", 10)) connection_status.setStyleSheet("color: rgba(29, 185, 84, 0.9);") layout.addWidget(connection_status) layout.addStretch() - download_path_info = QLabel(f"📁 Downloads: {self.soulseek_client.download_path if self.soulseek_client else './downloads'}") + download_path_info = QLabel(f"Downloads: {self.soulseek_client.download_path if self.soulseek_client else './downloads'}") download_path_info.setFont(QFont("Arial", 9)) download_path_info.setStyleSheet("color: rgba(255, 255, 255, 0.6);") layout.addWidget(download_path_info) @@ -6272,7 +6272,7 @@ class DownloadsPage(QWidget): } """) - self.search_btn = QPushButton("🔍 Search") + self.search_btn = QPushButton("Search") self.search_btn.setFixedSize(100, 40) self.search_btn.clicked.connect(self.perform_search) self.search_btn.setStyleSheet(""" @@ -6338,11 +6338,11 @@ class DownloadsPage(QWidget): def perform_search(self): query = self.search_input.text().strip() if not query: - self.update_search_status("⚠️ Please enter a search term", "#ffa500") + self.update_search_status("Please enter a search term", "#ffa500") return if not self.soulseek_client: - self.update_search_status("❌ Soulseek client not available", "#e22134") + self.update_search_status("Soulseek client not available", "#e22134") return # Stop any existing search @@ -6378,12 +6378,12 @@ class DownloadsPage(QWidget): self.filter_container.setVisible(False) # Enhanced searching state with animation - self.search_btn.setText("🔍 Searching...") + self.search_btn.setText("Searching...") self.search_btn.setEnabled(False) self.update_search_status(f"Searching for '{query}'... Results will appear as they are found", "#1db954") # Emit activity signal for search start - self.download_activity.emit("🔍", "Search Started", f"Searching for '{query}'", "Now") + self.download_activity.emit("", "Search Started", f"Searching for '{query}'", "Now") # Show loading animations self.start_search_animations() @@ -6411,7 +6411,7 @@ class DownloadsPage(QWidget): self.search_thread.terminate() # Reset UI state - self.search_btn.setText("🔍 Search") + self.search_btn.setText("Search") self.search_btn.setEnabled(True) self.search_btn.setVisible(True) self.cancel_search_btn.setVisible(False) @@ -6570,12 +6570,12 @@ class DownloadsPage(QWidget): # Update status message with real-time feedback if self.displayed_results < self.results_per_page: - self.update_search_status(f"✨ Found {total_results} results ({len(tracks)} tracks, {len(albums)} albums) from {response_count} users • Live updating...", "#1db954") + self.update_search_status(f"Found {total_results} results ({len(tracks)} tracks, {len(albums)} albums) from {response_count} users • Live updating...", "#1db954") else: - self.update_search_status(f"✨ Found {total_results} results ({len(tracks)} tracks, {len(albums)} albums) from {response_count} users • Showing first {self.results_per_page} (scroll for more)", "#1db954") + self.update_search_status(f"Found {total_results} results ({len(tracks)} tracks, {len(albums)} albums) from {response_count} users • Showing first {self.results_per_page} (scroll for more)", "#1db954") def on_search_completed(self, results): - self.search_btn.setText("🔍 Search") + self.search_btn.setText("Search") self.search_btn.setEnabled(True) self.search_btn.setVisible(True) self.cancel_search_btn.setVisible(False) @@ -6608,9 +6608,9 @@ class DownloadsPage(QWidget): if total_results == 0: if self.displayed_results == 0: - self.update_search_status("😔 No results found • Try a different search term or artist name", "#ffa500") + self.update_search_status("No results found • Try a different search term or artist name", "#ffa500") else: - self.update_search_status(f"✨ Search completed • Found {self.displayed_results} total results", "#1db954") + self.update_search_status(f"Search completed • Found {self.displayed_results} total results", "#1db954") # Hide filter controls when no results self.filter_container.setVisible(False) return @@ -6632,14 +6632,14 @@ class DownloadsPage(QWidget): # Emit activity signal for search completion search_query = self.search_input.text().strip() - self.download_activity.emit("✅", "Search Complete", f"Found {total_results} results for '{search_query}'", "Now") + self.download_activity.emit("", "Search Complete", f"Found {total_results} results for '{search_query}'", "Now") # Update status based on whether there are more results to load if self.displayed_results < total_results: remaining = total_results - self.displayed_results - self.update_search_status(f"✅ Search completed • Found {result_summary} • Showing first {self.displayed_results} (scroll down for {remaining} more)", "#1db954") + self.update_search_status(f"Search completed • Found {result_summary} • Showing first {self.displayed_results} (scroll down for {remaining} more)", "#1db954") else: - self.update_search_status(f"✅ Search completed • Found {result_summary}", "#1db954") + self.update_search_status(f"Search completed • Found {result_summary}", "#1db954") def clear_search_results(self): """Clear all search result items from the layout""" @@ -6690,7 +6690,7 @@ class DownloadsPage(QWidget): self._results_to_load_queue.extend(results_to_add) # Update status to show that we are loading more - self.update_search_status(f"✨ Loading more results...", "#1db954") + self.update_search_status(f"Loading more results...", "#1db954") # Kick off the staggered loading process by loading the first item if self._results_to_load_queue: @@ -6720,7 +6720,7 @@ class DownloadsPage(QWidget): self.currently_expanded_item = None def on_search_failed(self, error_msg): - self.search_btn.setText("🔍 Search") + self.search_btn.setText("Search") self.search_btn.setEnabled(True) self.search_btn.setVisible(True) self.cancel_search_btn.setVisible(False) @@ -6728,10 +6728,10 @@ class DownloadsPage(QWidget): # Stop loading animations self.stop_search_animations() - self.update_search_status(f"❌ Search failed: {error_msg}", "#e22134") + self.update_search_status(f"Search failed: {error_msg}", "#e22134") def on_search_progress(self, message): - self.update_search_status(f"🔍 {message}", "#1db954") + self.update_search_status(f"{message}", "#1db954") def start_download(self, search_result): """Start downloading a search result using threaded approach""" @@ -6856,7 +6856,7 @@ class DownloadsPage(QWidget): print(f"[DEBUG] Created download item with album context: album='{album_name}', track_number={track_number}") # Emit activity signal for download start - self.download_activity.emit("📥", "Download Started", f"'{title}' by {artist}", "Now") + self.download_activity.emit("", "Download Started", f"'{title}' by {artist}", "Now") # Create and start download thread download_thread = DownloadThread(self.soulseek_client, search_result, download_item) @@ -6882,7 +6882,7 @@ class DownloadsPage(QWidget): def start_album_download(self, album_result): """Start downloading all tracks in an album""" try: - print(f"🎵 Starting album download: {album_result.album_title} by {album_result.artist}") + print(f"Starting album download: {album_result.album_title} by {album_result.artist}") # First, find and disable all track download buttons for this album self.disable_album_track_buttons(album_result) @@ -6891,7 +6891,7 @@ class DownloadsPage(QWidget): for track in album_result.tracks: self.start_download(track) - print(f"✓ Queued {len(album_result.tracks)} tracks for download from album: {album_result.album_title}") + print(f"Queued {len(album_result.tracks)} tracks for download from album: {album_result.album_title}") except Exception as e: print(f"Failed to start album download: {str(e)}") @@ -6923,15 +6923,15 @@ class DownloadsPage(QWidget): ) if modal.exec() == QDialog.DialogCode.Accepted: - print("✅ Match confirmed via modal.") + print("Match confirmed via modal.") elif hasattr(modal, 'skipped_matching') and modal.skipped_matching: - print("🔄 Matching skipped, proceeding with normal download.") + print("Matching skipped, proceeding with normal download.") self.start_download(search_result) else: - print("🚫 Match process cancelled by user.") + print("Match process cancelled by user.") except Exception as e: - print(f"❌ Error in matched download process: {e}") + print(f"Error in matched download process: {e}") self.start_download(search_result) def start_matched_album_download(self, album_result): @@ -6944,7 +6944,7 @@ class DownloadsPage(QWidget): # Use the first track as a reference for the modal first_track = album_result.tracks[0] if album_result.tracks else None if not first_track: - print("❌ Cannot start matched download for an empty album.") + print("Cannot start matched download for an empty album.") self.start_album_download(album_result) return @@ -6954,22 +6954,22 @@ class DownloadsPage(QWidget): ) if modal.exec() == QDialog.DialogCode.Accepted: - print("✅ Album match confirmed via modal.") + print("Album match confirmed via modal.") elif hasattr(modal, 'skipped_matching') and modal.skipped_matching: - print("🔄 Album matching skipped, proceeding with normal download.") + print("Album matching skipped, proceeding with normal download.") self.start_album_download(album_result) else: - print("🚫 Album match process cancelled by user.") + print("Album match process cancelled by user.") except Exception as e: - print(f"❌ Error in matched album download process: {e}") + print(f"Error in matched album download process: {e}") self.start_album_download(album_result) def _handle_matched_download(self, search_result, artist: Artist): """Handle the download after artist selection from modal""" try: - print(f"🎯 Starting matched download for '{search_result.title}' by '{artist.name}'") + print(f"Starting matched download for '{search_result.title}' by '{artist.name}'") # Store the selected artist metadata with the search result search_result.matched_artist = artist @@ -6978,13 +6978,13 @@ class DownloadsPage(QWidget): download_item = self._start_download_with_artist(search_result, artist) if download_item: - print(f"✅ Successfully created matched download for '{download_item.title}'") + print(f"Successfully created matched download for '{download_item.title}'") else: - print(f"❌ Failed to create matched download, falling back to normal download") + print(f"Failed to create matched download, falling back to normal download") self.start_download(search_result) except Exception as e: - print(f"❌ Error handling matched download: {e}") + print(f"Error handling matched download: {e}") # Fallback to normal download self.start_download(search_result) @@ -6995,7 +6995,7 @@ class DownloadsPage(QWidget): try: if is_album_download: # This is a full album download - print(f"🎯 Confirmed album match: '{album.name}' by '{artist.name}'") + print(f"Confirmed album match: '{album.name}' by '{artist.name}'") # Now, we process each track in the original album_result for track in original_result.tracks: track.matched_artist = artist @@ -7004,7 +7004,7 @@ class DownloadsPage(QWidget): self._start_download_with_artist(track, artist) else: # This is a single track download - print(f"🎯 Confirmed single track match: '{original_result.title}' -> Artist: '{artist.name}', Album: '{album.name}'") + print(f"Confirmed single track match: '{original_result.title}' -> Artist: '{artist.name}', Album: '{album.name}'") original_result.matched_artist = artist original_result.matched_album = album @@ -7016,7 +7016,7 @@ class DownloadsPage(QWidget): self._start_download_with_artist(original_result, artist) except Exception as e: - print(f"❌ Error handling confirmed match: {e}") + print(f"Error handling confirmed match: {e}") # Fallback to normal download if is_album_download: self.start_album_download(original_result) @@ -7103,9 +7103,9 @@ class DownloadsPage(QWidget): if not track_number: track_number = self._extract_track_number_from_filename(filename, title) if track_number: - print(f"✅ Extracted track number from filename: {track_number}") + print(f"Extracted track number from filename: {track_number}") else: - print(f"❌ Could not extract track number from filename: '{filename}'") + print(f"Could not extract track number from filename: '{filename}'") # Generate download ID import time @@ -7132,10 +7132,10 @@ class DownloadsPage(QWidget): # Immediately assign the matched artist - no timing delays if download_item: download_item.matched_artist = artist - print(f"✅ Matched artist '{artist.name}' assigned to download item '{download_item.title}'") + print(f"Matched artist '{artist.name}' assigned to download item '{download_item.title}'") # Emit activity signal for download start (with matched artist) - self.download_activity.emit("📥", "Download Started", f"'{title}' by {artist.name if artist else original_artist}", "Now") + self.download_activity.emit("", "Download Started", f"'{title}' by {artist.name if artist else original_artist}", "Now") # Start the download thread download_thread = DownloadThread(self.soulseek_client, search_result, download_item) @@ -7153,7 +7153,7 @@ class DownloadsPage(QWidget): return download_item except Exception as e: - print(f"❌ Failed to start download with artist: {str(e)}") + print(f"Failed to start download with artist: {str(e)}") return None def _ensure_album_consistency(self, download_items, artist: Artist, album_name: str): @@ -7166,22 +7166,22 @@ class DownloadsPage(QWidget): # Store the definitive album name self.album_name_cache[album_key] = album_name - print(f"🔒 Cached album name: '{album_name}' for key: '{album_key}'") + print(f"Cached album name: '{album_name}' for key: '{album_key}'") # Ensure all download items use the same album name for download_item in download_items: if hasattr(download_item, 'album'): download_item.album = album_name - print(f" ✅ Set album name for '{download_item.title}': '{album_name}'") + print(f" Set album name for '{download_item.title}': '{album_name}'") except Exception as e: - print(f"❌ Error ensuring album consistency: {e}") + print(f"Error ensuring album consistency: {e}") def _assign_matched_artist_to_download_item(self, search_result, artist: Artist): """Assign matched artist to the corresponding download item""" try: - print(f"🔍 Looking for download item matching: '{search_result.title}' by '{search_result.artist}'") - print(f"📋 Current download items: {len(self.download_queue.download_items)}") + print(f"Looking for download item matching: '{search_result.title}' by '{search_result.artist}'") + print(f"Current download items: {len(self.download_queue.download_items)}") matched = False # Find the download item for this search result @@ -7191,26 +7191,26 @@ class DownloadsPage(QWidget): if (hasattr(download_item, 'title') and download_item.title == search_result.title and hasattr(download_item, 'artist') and download_item.artist == search_result.artist): download_item.matched_artist = artist - print(f"✅ Assigned matched artist '{artist.name}' to download item '{download_item.title}'") + print(f"Assigned matched artist '{artist.name}' to download item '{download_item.title}'") matched = True break if not matched: - print(f"❌ Could not find matching download item for '{search_result.title}' by '{search_result.artist}'") + print(f"Could not find matching download item for '{search_result.title}' by '{search_result.artist}'") # Try a more lenient search for i, download_item in enumerate(self.download_queue.download_items): if (hasattr(download_item, 'title') and self.matching_engine.normalize_string(download_item.title) == self.matching_engine.normalize_string(search_result.title)): download_item.matched_artist = artist - print(f"✅ Assigned matched artist '{artist.name}' to download item '{download_item.title}' (lenient match)") + print(f"Assigned matched artist '{artist.name}' to download item '{download_item.title}' (lenient match)") matched = True break if not matched: - print(f"❌ Still could not find matching download item - assignment failed") + print(f"Still could not find matching download item - assignment failed") except Exception as e: - print(f"❌ Error assigning matched artist to download item: {e}") + print(f"Error assigning matched artist to download item: {e}") def _handle_matched_album_download(self, album_result, artist: Artist): """Handle the album download after artist selection from modal""" @@ -7219,11 +7219,11 @@ class DownloadsPage(QWidget): return self._handle_matched_album_download_v2(album_result, artist) try: - print(f"🎯 Starting matched album download for '{album_result.album_title}' by '{artist.name}'") - print(f"📀 Processing {len(album_result.tracks)} tracks with matched artist") + print(f"Starting matched album download for '{album_result.album_title}' by '{artist.name}'") + print(f"Processing {len(album_result.tracks)} tracks with matched artist") # Store the selected artist metadata and album context with each track - print(f"🔍 Album context being set:") + print(f"Album context being set:") print(f" Album result title: '{album_result.album_title}'") print(f" Matched artist: '{artist.name}'") @@ -7244,44 +7244,44 @@ class DownloadsPage(QWidget): original_track_num = self._extract_track_number_from_filename(filename, track.title) if original_track_num: track.track_number = original_track_num - print(f" 🎵 Preserved original track number: {original_track_num}") + print(f" Preserved original track number: {original_track_num}") else: # Only use sequential as fallback if no original number found track.track_number = track_index - print(f" ⚠️ Using fallback sequential track number: {track_index}") + print(f" Using fallback sequential track number: {track_index}") else: # Fallback to sequential numbering if no filename available track.track_number = track_index - print(f" ⚠️ Using fallback sequential track number (no filename): {track_index}") + print(f" Using fallback sequential track number (no filename): {track_index}") # Clean up track title - remove artist prefix if present clean_track_title = self._clean_track_title(track.title, artist.name) track.title = clean_track_title - print(f" 🎵 Track {track_index}: '{clean_track_title}' -> Artist: '{artist.name}', Album: '{clean_album_title}', Track#: {track.track_number}") + print(f" Track {track_index}: '{clean_track_title}' -> Artist: '{artist.name}', Album: '{clean_album_title}', Track#: {track.track_number}") # Start downloading all tracks with matched artist immediately import time download_items = [] for track_index, track in enumerate(album_result.tracks, 1): - print(f"🎬 Starting download {track_index}/{len(album_result.tracks)}: {track.title}") + print(f"Starting download {track_index}/{len(album_result.tracks)}: {track.title}") download_item = self._start_download_with_artist(track, artist) if download_item: download_items.append(download_item) # Small delay between downloads to avoid overwhelming Soulseek time.sleep(0.1) - print(f"✅ Successfully queued {len(download_items)}/{len(album_result.tracks)} tracks with matched artist") + print(f"Successfully queued {len(download_items)}/{len(album_result.tracks)} tracks with matched artist") # Pre-calculate and cache the album name to ensure consistency if download_items: self._ensure_album_consistency(download_items, artist, clean_album_title) - print(f"✓ Queued {len(album_result.tracks)} tracks for matched download from album: {album_result.album_title}") - print(f"🎯 All tracks have album context preserved: '{album_result.album_title}'") + print(f"Queued {len(album_result.tracks)} tracks for matched download from album: {album_result.album_title}") + print(f"All tracks have album context preserved: '{album_result.album_title}'") except Exception as e: - print(f"❌ Error handling matched album download: {e}") + print(f"Error handling matched album download: {e}") # Fallback to normal album download self.start_album_download(album_result) @@ -7325,7 +7325,7 @@ class DownloadsPage(QWidget): download_items.append(download_item) # No sleep - let the system handle queuing naturally except Exception as e: - print(f"❌ Failed to start download for track {track.title}: {e}") + print(f"Failed to start download for track {track.title}: {e}") # Ensure album consistency in background if download_items: @@ -7334,14 +7334,14 @@ class DownloadsPage(QWidget): # Reset bulk operation flag self._bulk_operation_active = False - print(f"✅ Queued {len(download_items)}/{len(prepared_tracks)} tracks") + print(f"Queued {len(download_items)}/{len(prepared_tracks)} tracks") return download_items # Submit to optimized thread pool self._optimized_api_pool.submit(batch_download_tracks) except Exception as e: - print(f"❌ Optimized album download failed: {e}") + print(f"Optimized album download failed: {e}") self._bulk_operation_active = False # Fallback to original method self._handle_matched_album_download(album_result, artist) @@ -7349,14 +7349,14 @@ class DownloadsPage(QWidget): def _handle_matched_album_download_with_album(self, album_result, artist: Artist, selected_album: Album): """Handle the album download after both artist and album selection from modal""" try: - print(f"🎯 Starting matched album download with FORCED album selection") + print(f"Starting matched album download with FORCED album selection") print(f" Original album: '{album_result.album_title}'") print(f" Selected artist: '{artist.name}'") print(f" Selected album: '{selected_album.name}'") - print(f" 🔒 ALL tracks will be forced into: '{selected_album.name}'") + print(f" ALL tracks will be forced into: '{selected_album.name}'") # Fetch official track titles from Spotify album - print(f"🎵 Fetching official track titles from Spotify album...") + print(f"Fetching official track titles from Spotify album...") spotify_tracks = self._get_spotify_album_tracks(selected_album) download_items = [] @@ -7382,13 +7382,13 @@ class DownloadsPage(QWidget): # Match to Spotify track title if available spotify_title = self._match_track_to_spotify_title(track, spotify_tracks) if spotify_title: - print(f" 🎵 Track {track_index}: '{track.title}' -> Spotify title: '{spotify_title}'") + print(f" Track {track_index}: '{track.title}' -> Spotify title: '{spotify_title}'") track._spotify_title = spotify_title # Store the official Spotify title track._spotify_clean_title = spotify_title # This will be used for file naming else: - print(f" 🎵 Track {track_index}: '{track.title}' -> No Spotify match found, using original") + print(f" Track {track_index}: '{track.title}' -> No Spotify match found, using original") - print(f" 🔒 FORCED into Album: {selected_album.name}") + print(f" FORCED into Album: {selected_album.name}") # Start individual track download with enhanced metadata download_item = self._start_download_with_artist(track, artist) @@ -7400,12 +7400,12 @@ class DownloadsPage(QWidget): # Apply Spotify title to download item if available if hasattr(track, '_spotify_clean_title'): download_item._spotify_clean_title = track._spotify_clean_title - print(f"✅ Applied Spotify title to download item: '{track._spotify_clean_title}'") + print(f"Applied Spotify title to download item: '{track._spotify_clean_title}'") download_items.append(download_item) - print(f"✓ Successfully queued track: {track.title}") + print(f"Successfully queued track: {track.title}") else: - print(f"❌ Failed to queue track: {track.title}") + print(f"Failed to queue track: {track.title}") # Ensure all download items have consistent album information for download_item in download_items: @@ -7413,25 +7413,25 @@ class DownloadsPage(QWidget): download_item._force_album_name = selected_album.name download_item._force_album_mode = True - print(f"✓ Queued {len(album_result.tracks)} tracks for matched download - ALL FORCED into album: {selected_album.name}") + print(f"Queued {len(album_result.tracks)} tracks for matched download - ALL FORCED into album: {selected_album.name}") except Exception as e: - print(f"❌ Error handling matched album download with album selection: {e}") + print(f"Error handling matched album download with album selection: {e}") # Fallback to normal album download self.start_album_download(album_result) def _assign_matched_artist_to_album_downloads(self, album_result, artist: Artist): """Assign matched artist to all download items for an album""" try: - print(f"📋 Assigning matched artist '{artist.name}' to all album download items") - print(f"📀 Album has {len(album_result.tracks)} tracks") - print(f"📋 Current download queue has {len(self.download_queue.download_items)} items") + print(f"Assigning matched artist '{artist.name}' to all album download items") + print(f"Album has {len(album_result.tracks)} tracks") + print(f"Current download queue has {len(self.download_queue.download_items)} items") assigned_count = 0 # Find download items for all tracks in this album for track_idx, track in enumerate(album_result.tracks): - print(f"🔍 Looking for track {track_idx + 1}: '{track.title}' by '{track.artist}'") + print(f"Looking for track {track_idx + 1}: '{track.title}' by '{track.artist}'") matched = False for download_item in self.download_queue.download_items: @@ -7439,26 +7439,26 @@ class DownloadsPage(QWidget): hasattr(download_item, 'artist') and download_item.artist == track.artist): download_item.matched_artist = artist assigned_count += 1 - print(f" ✅ Assigned to: {download_item.title}") + print(f" Assigned to: {download_item.title}") matched = True break if not matched: - print(f" ❌ Could not find download item for: {track.title}") + print(f" Could not find download item for: {track.title}") # Try a more lenient search for download_item in self.download_queue.download_items: if (hasattr(download_item, 'title') and self.matching_engine.normalize_string(download_item.title) == self.matching_engine.normalize_string(track.title)): download_item.matched_artist = artist assigned_count += 1 - print(f" ✅ Assigned to: {download_item.title} (lenient match)") + print(f" Assigned to: {download_item.title} (lenient match)") matched = True break if not matched: - print(f" ❌ Still could not find download item for: {track.title}") + print(f" Still could not find download item for: {track.title}") - print(f"✅ Successfully assigned matched artist to {assigned_count}/{len(album_result.tracks)} album tracks") + print(f"Successfully assigned matched artist to {assigned_count}/{len(album_result.tracks)} album tracks") # If we didn't assign to all tracks, let's try again with a longer delay if assigned_count < len(album_result.tracks): @@ -7466,12 +7466,12 @@ class DownloadsPage(QWidget): QTimer.singleShot(1000, lambda: self._retry_album_assignment(album_result, artist, assigned_count)) except Exception as e: - print(f"❌ Error assigning matched artist to album download items: {e}") + print(f"Error assigning matched artist to album download items: {e}") def _retry_album_assignment(self, album_result, artist: Artist, previous_count: int): """Retry assignment for album tracks that failed the first time""" try: - print(f"🔄 Retrying album assignment for remaining tracks...") + print(f"Retrying album assignment for remaining tracks...") new_assigned = 0 for track in album_result.tracks: @@ -7490,24 +7490,24 @@ class DownloadsPage(QWidget): self.matching_engine.normalize_string(download_item.title) == self.matching_engine.normalize_string(track.title)): download_item.matched_artist = artist new_assigned += 1 - print(f" ✅ Retry assigned to: {download_item.title}") + print(f" Retry assigned to: {download_item.title}") break total_assigned = previous_count + new_assigned - print(f"🔄 Retry complete: {total_assigned}/{len(album_result.tracks)} tracks now assigned") + print(f"Retry complete: {total_assigned}/{len(album_result.tracks)} tracks now assigned") except Exception as e: - print(f"❌ Error in retry assignment: {e}") + print(f"Error in retry assignment: {e}") def _handle_modal_cancelled(self, search_result): """Handle when modal is cancelled for single track downloads""" - print(f"🚫 Modal cancelled for track: {search_result.title}") + print(f"Modal cancelled for track: {search_result.title}") # Re-enable any disabled download buttons for this track # Since track downloads don't disable buttons, this is mainly for consistency def _handle_album_modal_cancelled(self, album_result): """Handle when modal is cancelled for album downloads - re-enable buttons""" - print(f"🚫 Album modal cancelled for: {album_result.album_title}") + print(f"Album modal cancelled for: {album_result.album_title}") # Re-enable all track download buttons for this album self._enable_album_track_buttons(album_result) @@ -7523,7 +7523,7 @@ class DownloadsPage(QWidget): if hasattr(widget, 'album_result') and widget.album_result == album_result: # Re-enable the matched download button if hasattr(widget, 'matched_download_btn'): - widget.matched_download_btn.setText("🎯 Download w/ Matching") + widget.matched_download_btn.setText("Download w/ Matching") widget.matched_download_btn.setEnabled(True) # Re-enable the regular download button @@ -7531,10 +7531,10 @@ class DownloadsPage(QWidget): widget.download_btn.setText("⬇️ Download Album") widget.download_btn.setEnabled(True) - print(f"✅ Re-enabled buttons for album: {album_result.album_title}") + print(f"Re-enabled buttons for album: {album_result.album_title}") break except Exception as e: - print(f"❌ Error re-enabling album buttons: {e}") + print(f"Error re-enabling album buttons: {e}") def _cleanup_empty_directories(self, download_path, moved_file_path): @@ -7584,11 +7584,11 @@ class DownloadsPage(QWidget): from pathlib import Path if not hasattr(download_item, 'matched_artist'): - print("❌ No matched artist information found") + print("No matched artist information found") return None artist = download_item.matched_artist - print(f"🎯 Organizing download for artist: {artist.name}") + print(f"Organizing download for artist: {artist.name}") # --- FIX: Get transfer directory from config instead of hardcoding --- # OLD CODE: @@ -7608,7 +7608,7 @@ class DownloadsPage(QWidget): # Resolve consistent album name for grouping tracks from same album if album_info and album_info['is_album']: - print(f"\n🎯 SMART ALBUM GROUPING for track: '{download_item.title}'") + print(f"\nSMART ALBUM GROUPING for track: '{download_item.title}'") print(f" Original album: '{getattr(download_item, 'album', 'None')}'") print(f" Detected album: '{album_info.get('album_name', 'None')}'") @@ -7616,11 +7616,11 @@ class DownloadsPage(QWidget): album_info['album_name'] = consistent_album_name print(f" Final album name: '{consistent_album_name}'") - print(f"🔗 ✅ Album grouping complete!\n") + print(f"Album grouping complete!\n") if album_info and album_info['is_album']: # Album track structure: Transfer/ARTIST/ARTIST - ALBUM/TRACK# TRACK.ext - print(f"🔍 Creating album folder:") + print(f"Creating album folder:") print(f" Artist name: '{artist.name}'") print(f" Album name from album_info: '{album_info['album_name']}'") print(f" Original download item title: '{download_item.title}'") @@ -7644,8 +7644,8 @@ class DownloadsPage(QWidget): track_filename = f"{track_number:02d} - {self._sanitize_filename(clean_track_name)}{file_ext}" new_file_path = os.path.join(album_dir, track_filename) - print(f"📁 Album folder created: '{album_folder_name}'") - print(f"🎵 Track filename: '{track_filename}'") + print(f"Album folder created: '{album_folder_name}'") + print(f"Track filename: '{track_filename}'") else: # Single track structure: Transfer/ARTIST/ARTIST - SINGLE/SINGLE.ext @@ -7668,32 +7668,32 @@ class DownloadsPage(QWidget): single_filename = f"{self._sanitize_filename(clean_track_name)}{file_ext}" new_file_path = os.path.join(single_dir, single_filename) - print(f"📁 Single track: {single_folder_name}/{single_filename}") + print(f"Single track: {single_folder_name}/{single_filename}") # Check if source file exists, and try to find it if not if not os.path.exists(original_file_path): - print(f"❌ Source file not found: {original_file_path}") + print(f"Source file not found: {original_file_path}") # Try to find the file using different methods found_file = self._find_downloaded_file(original_file_path, download_item) if found_file: - print(f"✅ Found file at: {found_file}") + print(f"Found file at: {found_file}") original_file_path = found_file else: - print(f"❌ Could not locate downloaded file anywhere") + print(f"Could not locate downloaded file anywhere") return None # File organization and overwrite logic will be handled after metadata enhancement # 🆕 METADATA ENHANCEMENT - Enhance BEFORE moving to avoid race conditions if self._enhance_file_metadata(original_file_path, download_item, artist, album_info): - print(f"✅ Metadata enhanced with Spotify data") + print(f"Metadata enhanced with Spotify data") else: - print(f"⚠️ Metadata enhancement failed, using original tags") + print(f"Metadata enhancement failed, using original tags") # Verify source file exists before attempting move if not os.path.exists(original_file_path): - print(f"❌ Source file not found: {original_file_path}") + print(f"Source file not found: {original_file_path}") return None # Explicit overwrite: check if file exists, remove it, then move @@ -7705,21 +7705,21 @@ class DownloadsPage(QWidget): # Verify the move was successful if not os.path.exists(new_file_path): - print(f"❌ File move failed - destination file not found: {new_file_path}") + print(f"File move failed - destination file not found: {new_file_path}") return None if os.path.exists(original_file_path): try: os.remove(original_file_path) except Exception as cleanup_error: - print(f"⚠️ Could not remove original file: {cleanup_error}") + print(f"Could not remove original file: {cleanup_error}") # Clean up any empty directories left in the downloads folder try: downloads_path = config_manager.get('soulseek.download_path', './Downloads') self._cleanup_empty_directories(downloads_path, original_file_path) except Exception as cleanup_error: - print(f"⚠️ Could not clean up empty directories: {cleanup_error}") + print(f"Could not clean up empty directories: {cleanup_error}") # Download cover art for both albums and singles if album_info and album_info['is_album']: @@ -7734,11 +7734,11 @@ class DownloadsPage(QWidget): # Generate LRC lyrics file at final location (elegant addition) self._generate_lrc_file(new_file_path, download_item, artist, album_info) - print(f"✅ Successfully organized matched download: {new_file_path}") + print(f"Successfully organized matched download: {new_file_path}") return new_file_path except Exception as e: - print(f"❌ Error organizing matched download: {e}") + print(f"Error organizing matched download: {e}") return None @@ -7766,7 +7766,7 @@ class DownloadsPage(QWidget): # Start with the original title original = album_title.strip() cleaned = original - print(f"🧹 Album Title Cleaning: '{original}' (artist: '{artist_name}')") + print(f"Album Title Cleaning: '{original}' (artist: '{artist_name}')") # Remove "Album - " prefix cleaned = re.sub(r'^Album\s*-\s*', '', cleaned, flags=re.IGNORECASE) @@ -7831,9 +7831,9 @@ class DownloadsPage(QWidget): # Remove artist prefix from fallback fallback = re.sub(f'^{re.escape(artist_name)}\\s*-\\s*', '', fallback, flags=re.IGNORECASE) cleaned = fallback.strip() if fallback.strip() else album_title - print(f"🧹 Album Title used fallback: '{fallback}'") + print(f"Album Title used fallback: '{fallback}'") - print(f"🧹 Album Title Result: '{original}' -> '{cleaned}'") + print(f"Album Title Result: '{original}' -> '{cleaned}'") return cleaned def _clean_track_title(self, track_title: str, artist_name: str) -> str: @@ -7843,7 +7843,7 @@ class DownloadsPage(QWidget): # Start with the original title original = track_title.strip() cleaned = original - print(f"🧹 Track Title Cleaning: '{original}' (artist: '{artist_name}')") + print(f"Track Title Cleaning: '{original}' (artist: '{artist_name}')") # Remove track numbers from the beginning if present # Handles cases like "01 - Track Name", "1. Track Name", "01. Track Name" @@ -7880,9 +7880,9 @@ class DownloadsPage(QWidget): fallback = re.sub(r'^\d{1,2}[\.\s\-]+', '', fallback) # Remove track numbers fallback = re.sub(f'^{re.escape(artist_name)}\\s*-\\s*', '', fallback, flags=re.IGNORECASE) cleaned = fallback.strip() if fallback.strip() else track_title - print(f"🧹 Track Title used fallback: '{fallback}'") + print(f"Track Title used fallback: '{fallback}'") - print(f"🧹 Track Title Result: '{original}' -> '{cleaned}'") + print(f"Track Title Result: '{original}' -> '{cleaned}'") return cleaned def _resolve_album_group(self, download_item, artist: Artist, album_info: dict) -> str: @@ -7916,10 +7916,10 @@ class DownloadsPage(QWidget): # Check if we already have a cached result for this album if album_key in self.album_name_cache: cached_name = self.album_name_cache[album_key] - print(f"🔍 Using cached album name for '{album_key}': '{cached_name}'") + print(f"Using cached album name for '{album_key}': '{cached_name}'") return cached_name - print(f"🔍 Album grouping - Key: '{album_key}', Detected: '{detected_album}'") + print(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'") # Check if this track indicates a deluxe edition is_deluxe_track = False @@ -7933,7 +7933,7 @@ class DownloadsPage(QWidget): # SMART ALGORITHM: Upgrade to deluxe if this track is deluxe if is_deluxe_track and current_edition == "standard": - print(f"🎯 UPGRADE: Album '{base_album}' upgraded from standard to deluxe!") + print(f"UPGRADE: Album '{base_album}' upgraded from standard to deluxe!") self.album_editions[album_key] = "deluxe" current_edition = "deluxe" @@ -7948,12 +7948,12 @@ class DownloadsPage(QWidget): self.album_name_cache[album_key] = final_album_name self.album_artists[album_key] = artist.name - print(f"🔗 Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})") + print(f"Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})") return final_album_name except Exception as e: - print(f"❌ Error resolving album group: {e}") + print(f"Error resolving album group: {e}") return album_info.get('album_name', download_item.title) def _normalize_base_album_name(self, base_album: str, artist_name: str) -> str: @@ -7975,7 +7975,7 @@ class DownloadsPage(QWidget): for key, correction in known_corrections.items(): if key == normalized_lower: - print(f"📝 Base album correction: '{base_album}' -> '{correction}'") + print(f"Base album correction: '{base_album}' -> '{correction}'") return correction # If no specific correction, return cleaned version @@ -8028,7 +8028,7 @@ class DownloadsPage(QWidget): normalized = correction + suffix break - print(f"📀 Album variant normalization: '{album_name}' -> '{normalized}'") + print(f"Album variant normalization: '{album_name}' -> '{normalized}'") return normalized def _detect_deluxe_edition(self, album_name: str) -> bool: @@ -8057,7 +8057,7 @@ class DownloadsPage(QWidget): for indicator in deluxe_indicators: if indicator in album_lower: - print(f"🎯 Detected deluxe edition: '{album_name}' contains '{indicator}'") + print(f"Detected deluxe edition: '{album_name}' contains '{indicator}'") return True return False @@ -8083,14 +8083,14 @@ class DownloadsPage(QWidget): def _detect_album_info(self, download_item, artist: Artist) -> Optional[dict]: """Detect if track is part of an album using Spotify API as primary source""" try: - print(f"🔍 Album detection for '{download_item.title}' by '{artist.name}':") + print(f"Album detection for '{download_item.title}' by '{artist.name}':") print(f" Has album attr: {hasattr(download_item, 'album')}") if hasattr(download_item, 'album'): print(f" Album value: '{download_item.album}'") # CHECK FOR FORCED ALBUM MODE FIRST if hasattr(download_item, '_force_album_mode') and download_item._force_album_mode: - print(f"🔒 FORCED ALBUM MODE DETECTED - Using forced album name") + print(f"FORCED ALBUM MODE DETECTED - Using forced album name") forced_album = getattr(download_item, '_force_album_name', 'Unknown Album') print(f" Forced album: '{forced_album}'") @@ -8111,20 +8111,20 @@ class DownloadsPage(QWidget): # PRIORITY 1: Try album-aware search if we have album context if hasattr(download_item, 'album') and download_item.album and download_item.album.strip() and download_item.album != "Unknown Album": - print(f"🎯 ALBUM-AWARE SEARCH: Looking for '{download_item.title}' in album '{download_item.album}'") + print(f"ALBUM-AWARE SEARCH: Looking for '{download_item.title}' in album '{download_item.album}'") album_result = self._search_track_in_album_context(download_item, artist) if album_result: - print(f"✅ Found track in album context - using album classification") + print(f"Found track in album context - using album classification") return album_result else: - print(f"⚠️ Track not found in album context, falling back to individual search") + print(f"Track not found in album context, falling back to individual search") # PRIORITY 2: Fallback to individual track search for clean metadata - print(f"🔍 Searching Spotify for individual track info (PRIORITY 2)...") + print(f"Searching Spotify for individual track info (PRIORITY 2)...") # Clean the track title before searching - remove artist prefix clean_title = self._clean_track_title(download_item.title, artist.name) - print(f"🧹 Cleaned title: '{download_item.title}' -> '{clean_title}'") + print(f"Cleaned title: '{download_item.title}' -> '{clean_title}'") # Search for the track by artist and cleaned title query = f"artist:{artist.name} track:{clean_title}" @@ -8154,25 +8154,25 @@ class DownloadsPage(QWidget): # If we found a good Spotify match, use it for clean metadata if best_match and best_confidence > 0.6: - print(f"✅ Found matching Spotify track: '{best_match.name}' - Album: '{best_match.album}' (confidence: {best_confidence:.2f})") + print(f"Found matching Spotify track: '{best_match.name}' - Album: '{best_match.album}' (confidence: {best_confidence:.2f})") # Get detailed track information using Spotify's track API detailed_track = None if hasattr(best_match, 'id') and best_match.id: - print(f"🔍 Getting detailed track info from Spotify API for track ID: {best_match.id}") + print(f"Getting detailed track info from Spotify API for track ID: {best_match.id}") detailed_track = self.spotify_client.get_track_details(best_match.id) # Use detailed track data if available if detailed_track: - print(f"✅ Got detailed track data from Spotify API") + print(f"Got detailed track data from Spotify API") album_name = self._clean_album_title(detailed_track['album']['name'], artist.name) clean_track_name = detailed_track['name'] # Use Spotify's clean track name album_type = detailed_track['album'].get('album_type', 'album') total_tracks = detailed_track['album'].get('total_tracks', 1) spotify_track_number = detailed_track.get('track_number', 1) - print(f"📀 Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})") - print(f"🎵 Clean track name from Spotify: '{clean_track_name}'") + print(f"Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})") + print(f"Clean track name from Spotify: '{clean_track_name}'") # Enhanced album detection using detailed API data is_album = ( @@ -8187,7 +8187,7 @@ class DownloadsPage(QWidget): ) track_num = spotify_track_number - print(f"🎯 Using Spotify track number: {track_num}") + print(f"Using Spotify track number: {track_num}") # Store the clean Spotify track name for use in file organization (only if not already set) if not hasattr(download_item, '_spotify_clean_title') or not download_item._spotify_clean_title: @@ -8201,7 +8201,7 @@ class DownloadsPage(QWidget): album_image_url = detailed_track['album']['images'][0]['url'] if is_album: - print(f"🎯 Spotify detection: Album track - '{album_name}'") + print(f"Spotify detection: Album track - '{album_name}'") return { 'is_album': True, 'album_name': album_name, @@ -8211,7 +8211,7 @@ class DownloadsPage(QWidget): 'album_image_url': album_image_url } else: - print(f"🎯 Spotify detection: Single track - using clean track name") + print(f"Spotify detection: Single track - using clean track name") return { 'is_album': False, 'album_name': clean_track_name, # Use clean track name for single structure @@ -8222,7 +8222,7 @@ class DownloadsPage(QWidget): } else: - print(f"⚠️ Could not get detailed track data, using basic Spotify search data") + print(f"Could not get detailed track data, using basic Spotify search data") album_name = self._clean_album_title(best_match.album, artist.name) clean_track_name = best_match.name @@ -8259,15 +8259,15 @@ class DownloadsPage(QWidget): } # PRIORITY 3: Fallback to Soulseek album context if Spotify search failed - print(f"🔍 No good Spotify match found (confidence: {best_confidence:.2f}), checking Soulseek album context...") + print(f"No good Spotify match found (confidence: {best_confidence:.2f}), checking Soulseek album context...") if hasattr(download_item, 'album') and download_item.album and download_item.album != "Unknown Album": clean_album = self._clean_album_title(download_item.album, artist.name) clean_title = self._clean_track_title(download_item.title, artist.name) track_num = self._extract_track_number(download_item) - print(f"✅ Using cleaned Soulseek album context: '{clean_album}' (cleaned from '{download_item.album}')") - print(f"🧹 Cleaned track title: '{clean_title}' (cleaned from '{download_item.title}')") + print(f"Using cleaned Soulseek album context: '{clean_album}' (cleaned from '{download_item.album}')") + print(f"Cleaned track title: '{clean_title}' (cleaned from '{download_item.title}')") # Only set if not already set (preserve original Spotify title from modal) if not hasattr(download_item, '_spotify_clean_title') or not download_item._spotify_clean_title: @@ -8289,7 +8289,7 @@ class DownloadsPage(QWidget): } # PRIORITY 4: Complete fallback - single track with cleaned title - print(f"🎯 No album context found, defaulting to single track structure with cleaned title") + print(f"No album context found, defaulting to single track structure with cleaned title") clean_title = self._clean_track_title(download_item.title, artist.name) # Only set if not already set (preserve original Spotify title from modal) @@ -8311,7 +8311,7 @@ class DownloadsPage(QWidget): } except Exception as e: - print(f"❌ Error detecting album info: {e}") + print(f"Error detecting album info: {e}") # Emergency fallback to single structure with basic cleaning clean_title = self._clean_track_title(download_item.title, artist.name) @@ -8335,7 +8335,7 @@ class DownloadsPage(QWidget): album_name = download_item.album track_title = download_item.title - print(f"🎯 Album-aware search: '{track_title}' in album '{album_name}' by '{artist.name}'") + print(f"Album-aware search: '{track_title}' in album '{album_name}' by '{artist.name}'") # Clean the album name for better search results clean_album = self._clean_album_title(album_name, artist.name) @@ -8343,21 +8343,21 @@ class DownloadsPage(QWidget): # Search for the specific album first album_query = f"album:{clean_album} artist:{artist.name}" - print(f"🔍 Searching albums: {album_query}") + print(f"Searching albums: {album_query}") albums = self.spotify_client.search_albums(album_query, limit=5) if not albums: - print(f"❌ No albums found for query: {album_query}") + print(f"No albums found for query: {album_query}") return None # Check each album to see if our track is in it for album in albums: - print(f"🎵 Checking album: '{album.name}' ({album.total_tracks} tracks)") + print(f"Checking album: '{album.name}' ({album.total_tracks} tracks)") # Get tracks from this album album_tracks_data = self.spotify_client.get_album_tracks(album.id) if not album_tracks_data or 'items' not in album_tracks_data: - print(f"❌ Could not get tracks for album: {album.name}") + print(f"Could not get tracks for album: {album.name}") continue # Check if our track is in this album @@ -8376,8 +8376,8 @@ class DownloadsPage(QWidget): threshold = 0.9 if is_remix else 0.7 # Much stricter for remixes if similarity > threshold: - print(f"✅ FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})") - print(f"🎯 Forcing album classification for track in '{album.name}'") + print(f"FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})") + print(f"Forcing album classification for track in '{album.name}'") # Return album info - force album classification! return { @@ -8390,13 +8390,13 @@ class DownloadsPage(QWidget): 'source': 'album_context_search' } - print(f"❌ Track '{clean_track}' not found in album '{album.name}'") + print(f"Track '{clean_track}' not found in album '{album.name}'") - print(f"❌ Track '{clean_track}' not found in any matching albums") + print(f"Track '{clean_track}' not found in any matching albums") return None except Exception as e: - print(f"❌ Error in album-aware search: {e}") + print(f"Error in album-aware search: {e}") return None def _download_cover_art(self, artist: Artist, album_info: dict, target_dir: str): @@ -8409,7 +8409,7 @@ class DownloadsPage(QWidget): # Skip if cover already exists if os.path.exists(cover_path): - print("📷 Cover art already exists") + print("Cover art already exists") return image_url = None @@ -8419,12 +8419,12 @@ class DownloadsPage(QWidget): if album_info.get('album_image_url'): image_url = album_info['album_image_url'] source_description = f"album artwork for '{album_info.get('album_name', 'Unknown Album')}'" - print(f"📷 Using album artwork from album_info") + print(f"Using album artwork from album_info") # Priority 2: Try to get album artwork via Spotify search if we have album name elif album_info.get('album_name') and hasattr(self, 'spotify_client'): try: - print(f"📷 Searching Spotify for album artwork: '{album_info['album_name']}' by '{artist.name}'") + print(f"Searching Spotify for album artwork: '{album_info['album_name']}' by '{artist.name}'") # Search for the specific album search_query = f"album:{album_info['album_name']} artist:{artist.name}" albums = self.spotify_client.search_albums(search_query, limit=1) @@ -8434,36 +8434,36 @@ class DownloadsPage(QWidget): if album.image_url: image_url = album.image_url source_description = f"Spotify album artwork for '{album.name}'" - print(f"📷 Found album artwork via Spotify search") + print(f"Found album artwork via Spotify search") else: - print(f"📷 Album found but no image available") + print(f"Album found but no image available") else: - print(f"📷 No album found in Spotify search") + print(f"No album found in Spotify search") except Exception as e: - print(f"📷 Error searching Spotify for album artwork: {e}") + print(f"Error searching Spotify for album artwork: {e}") # Priority 3: Fall back to artist image if not image_url and artist.image_url: image_url = artist.image_url source_description = f"artist image for '{artist.name}'" - print(f"📷 Falling back to artist image") + print(f"Falling back to artist image") # No image available if not image_url: - print("📷 No cover art available (no album artwork or artist image)") + print("No cover art available (no album artwork or artist image)") return - print(f"📷 Downloading {source_description} from: {image_url}") + print(f"Downloading {source_description} from: {image_url}") response = requests.get(image_url, timeout=10) response.raise_for_status() with open(cover_path, 'wb') as f: f.write(response.content) - print(f"✅ Cover art downloaded: {cover_path}") + print(f"Cover art downloaded: {cover_path}") except Exception as e: - print(f"❌ Error downloading cover art: {e}") + print(f"Error downloading cover art: {e}") def _create_single_track_album_info(self, download_item, artist: Artist) -> Optional[dict]: """Create album_info dict for single track cover art download""" @@ -8471,7 +8471,7 @@ class DownloadsPage(QWidget): # Check if we have matched_album from Spotify matching if hasattr(download_item, 'matched_album') and download_item.matched_album: album = download_item.matched_album - print(f"📷 Single track has matched album: '{album.name}' with image: {bool(album.image_url)}") + print(f"Single track has matched album: '{album.name}' with image: {bool(album.image_url)}") return { 'album_name': album.name, 'album_image_url': album.image_url, @@ -8484,9 +8484,9 @@ class DownloadsPage(QWidget): album_name = None if hasattr(download_item, 'album') and download_item.album and download_item.album.strip(): album_name = download_item.album.strip() - print(f"📷 Using album name from download_item: '{album_name}'") + print(f"Using album name from download_item: '{album_name}'") else: - print(f"📷 No album information available for single track") + print(f"No album information available for single track") return None # Create basic album_info for cover art search @@ -8499,7 +8499,7 @@ class DownloadsPage(QWidget): } except Exception as e: - print(f"❌ Error creating single track album info: {e}") + print(f"Error creating single track album info: {e}") return None def _generate_lrc_file(self, file_path: str, download_item, artist, album_info: dict) -> bool: @@ -8550,25 +8550,25 @@ class DownloadsPage(QWidget): ) if success: - print(f"🎵 LRC file generated for: {track_name}") + print(f"LRC file generated for: {track_name}") else: - print(f"🎵 No lyrics found for: {track_name}") + print(f"No lyrics found for: {track_name}") return success except Exception as e: - print(f"❌ Error generating LRC file for {file_path}: {e}") + print(f"Error generating LRC file for {file_path}: {e}") return False def _extract_track_number(self, download_item, spotify_track=None) -> int: """Extract track number from various sources""" try: - print(f"🔢 Extracting track number for: '{download_item.title}'") + print(f"Extracting track number for: '{download_item.title}'") # Method 1: Check if download_item has track_number attribute (explicit metadata) if hasattr(download_item, 'track_number') and download_item.track_number: track_num = int(download_item.track_number) - print(f" ✅ Found track_number attribute: {track_num}") + print(f" Found track_number attribute: {track_num}") return track_num # Method 2: Parse from filename (e.g., "01. Track Name.mp3", "01 - Track Name.flac") @@ -8584,7 +8584,7 @@ class DownloadsPage(QWidget): match = re.match(pattern, download_item.title.strip()) if match: track_num = int(match.group(1)) - print(f" ✅ Parsed from title pattern '{pattern}': {track_num}") + print(f" Parsed from title pattern '{pattern}': {track_num}") return track_num # Method 3: Parse from filename if available @@ -8603,22 +8603,22 @@ class DownloadsPage(QWidget): match = re.match(pattern, base_name.strip()) if match: track_num = int(match.group(1)) - print(f" ✅ Parsed from filename pattern '{pattern}': {track_num}") + print(f" Parsed from filename pattern '{pattern}': {track_num}") return track_num # Method 4: Get from Spotify track data (would need album API call) if spotify_track: # This would require additional Spotify API call to get full album # For now, we'll skip this but could be enhanced later - print(f" ⏭️ Spotify track data available but not implemented yet") + print(f" Spotify track data available but not implemented yet") pass # Default to 1 if no track number found - print(f" ⚠️ No track number found, defaulting to 1") + print(f" No track number found, defaulting to 1") return 1 except Exception as e: - print(f"❌ Error extracting track number: {e}") + print(f"Error extracting track number: {e}") return 1 def _find_downloaded_file(self, original_file_path: str, download_item) -> Optional[str]: @@ -8628,7 +8628,7 @@ class DownloadsPage(QWidget): import glob from pathlib import Path - print(f"🔍 Searching for downloaded file...") + print(f"Searching for downloaded file...") print(f" Original path: {original_file_path}") # Get the download directory @@ -8644,7 +8644,7 @@ class DownloadsPage(QWidget): # Method 1: Try the exact path first (but normalized) if os.path.exists(original_file_path): - print(f" ✅ Found exact path: {original_file_path}") + print(f" Found exact path: {original_file_path}") return original_file_path # Method 2: Search in the download directory recursively @@ -8666,7 +8666,7 @@ class DownloadsPage(QWidget): # Return the first match that exists and has reasonable size for match in audio_matches: if os.path.exists(match) and os.path.getsize(match) > 1024: # At least 1KB - print(f" ✅ Found match: {match}") + print(f" Found match: {match}") return match # Method 3: Look for recently modified files in download directory @@ -8675,7 +8675,7 @@ class DownloadsPage(QWidget): audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'} # Debug: List all files in download directory - print(f" 📂 Files in download directory:") + print(f" Files in download directory:") for root, dirs, files in os.walk(download_dir): for file in files: file_path = os.path.join(root, file) @@ -8697,14 +8697,14 @@ class DownloadsPage(QWidget): # Simple filename matching if (download_item.title.lower() in os.path.basename(file_path).lower() or download_item.artist.lower() in os.path.basename(file_path).lower()): - print(f" ✅ Found recent match: {file_path}") + print(f" Found recent match: {file_path}") return file_path - print(f" ❌ No matching files found") + print(f" No matching files found") return None except Exception as e: - print(f"❌ Error searching for downloaded file: {e}") + print(f"Error searching for downloaded file: {e}") return None def update_album_track_button_states(self, download_item, status): @@ -8727,18 +8727,18 @@ class DownloadsPage(QWidget): # Update button state based on download status if status == 'downloading': track_item.set_download_downloading_state() - print(f"[DEBUG] Set button to downloading state (📥)") + print(f"[DEBUG] Set button to downloading state ()") elif status in ['completed', 'finished']: track_item.set_download_completed_state() - print(f"[DEBUG] Set button to completed state (✅)") + print(f"[DEBUG] Set button to completed state ()") elif status in ['queued', 'initializing']: track_item.set_download_queued_state() - print(f"[DEBUG] Set button to queued state (⏳)") + print(f"[DEBUG] Set button to queued state ()") elif status in ['failed', 'cancelled', 'canceled']: track_item.reset_download_state() # Allow retry - print(f"[DEBUG] 🔓 RESET button to downloadable state (⬇️) - track can now be downloaded again!") + print(f"[DEBUG] RESET button to downloadable state (⬇️) - track can now be downloaded again!") else: - print(f"[DEBUG] ⚠️ Unknown status '{status}' - no button update performed") + print(f"[DEBUG] Unknown status '{status}' - no button update performed") return @@ -8750,31 +8750,31 @@ class DownloadsPage(QWidget): current_track_id = getattr(self, 'current_track_id', None) new_track_id = f"{search_result.username}:{search_result.filename}" - print(f"🎮 start_stream() called for: {search_result.filename}") - print(f"🎮 Current track ID: {current_track_id}") - print(f"🎮 New track ID: {new_track_id}") - print(f"🎮 Currently playing button: {self.currently_playing_button}") - print(f"🎮 Result item: {result_item}") - print(f"🎮 Button match: {self.currently_playing_button == result_item}") - print(f"🎮 Track ID match: {current_track_id == new_track_id}") + print(f"start_stream() called for: {search_result.filename}") + print(f"Current track ID: {current_track_id}") + print(f"New track ID: {new_track_id}") + print(f"Currently playing button: {self.currently_playing_button}") + print(f"Result item: {result_item}") + print(f"Button match: {self.currently_playing_button == result_item}") + print(f"Track ID match: {current_track_id == new_track_id}") if current_track_id == new_track_id and self.currently_playing_button == result_item: # Same track clicked - toggle playback - print(f"🔄 Toggling playback for: {search_result.filename}") + print(f"Toggling playback for: {search_result.filename}") toggle_result = self.audio_player.toggle_playback() - print(f"🔄 toggle_playback() returned: {toggle_result}") + print(f"toggle_playback() returned: {toggle_result}") if toggle_result: # Now playing result_item.set_playing_state() self.track_resumed.emit() - print("🎵 Song card: Resumed playback") + print("Song card: Resumed playback") else: # Now paused result_item.set_loading_state() # Use loading as "paused" state self.track_paused.emit() - print("⏸️ Song card: Paused playback") + print("Song card: Paused playback") return else: @@ -8810,7 +8810,7 @@ class DownloadsPage(QWidget): is_audio = any(filename_lower.endswith(ext) for ext in audio_extensions) if is_audio: - print(f"✓ Streaming audio file: {search_result.filename}") + print(f"Streaming audio file: {search_result.filename}") print(f" Quality: {search_result.quality}") print(f" Size: {search_result.size // (1024*1024)}MB") print(f" User: {search_result.username}") @@ -8821,7 +8821,7 @@ class DownloadsPage(QWidget): 'filename': search_result.filename, 'download_id': None # Will be set when download starts } - print(f"🎯 Tracking new streaming download: {search_result.username}:{search_result.filename}") + print(f"Tracking new streaming download: {search_result.username}:{search_result.filename}") # Create and start streaming thread streaming_thread = StreamingThread(self.soulseek_client, search_result) @@ -8844,7 +8844,7 @@ class DownloadsPage(QWidget): streaming_thread.start() else: - print(f"✗ Cannot stream non-audio file: {search_result.filename}") + print(f"Cannot stream non-audio file: {search_result.filename}") except Exception as e: print(f"Failed to start stream: {str(e)}") @@ -8874,7 +8874,7 @@ class DownloadsPage(QWidget): finished_track_id = f"{search_result.username}:{search_result.filename}" if current_track_id != finished_track_id: - print(f"🚫 Ignoring old streaming result for: {search_result.filename}") + print(f"Ignoring old streaming result for: {search_result.filename}") print(f" Current track: {current_track_id}") print(f" Finished track: {finished_track_id}") return @@ -8899,7 +8899,7 @@ class DownloadsPage(QWidget): # Start audio playback success = self.audio_player.play_file(stream_file) if success: - print(f"🎵 Started audio playback: {os.path.basename(stream_file)}") + print(f"Started audio playback: {os.path.basename(stream_file)}") # Set button to playing state if self.currently_playing_button: try: @@ -8912,7 +8912,7 @@ class DownloadsPage(QWidget): self.track_loading_finished.emit(self.current_track_result) self.track_started.emit(self.current_track_result) else: - print(f"❌ Failed to start audio playback") + print(f"Failed to start audio playback") # Reset button on failure if self.currently_playing_button: try: @@ -8922,7 +8922,7 @@ class DownloadsPage(QWidget): pass self.currently_playing_button = None else: - print(f"❌ Stream file not found in {stream_folder}") + print(f"Stream file not found in {stream_folder}") # Reset button on failure if self.currently_playing_button: try: @@ -8933,7 +8933,7 @@ class DownloadsPage(QWidget): self.currently_playing_button = None except Exception as e: - print(f"❌ Error starting audio playback: {e}") + print(f"Error starting audio playback: {e}") # Reset button on error if self.currently_playing_button: try: @@ -8956,7 +8956,7 @@ class DownloadsPage(QWidget): # Emit progress signal for media player self.track_loading_progress.emit(progress_percent, search_result) else: - print(f"🚫 Ignoring progress for old streaming result: {search_result.filename}") + print(f"Ignoring progress for old streaming result: {search_result.filename}") def on_streaming_queued(self, queue_msg, search_result): """Handle streaming queue state updates""" @@ -8975,9 +8975,9 @@ class DownloadsPage(QWidget): except RuntimeError: # Button was deleted, ignore pass - print(f"📋 Showing queue status for current track") + print(f"Showing queue status for current track") else: - print(f"🚫 Ignoring queue status for old streaming result: {search_result.filename}") + print(f"Ignoring queue status for old streaming result: {search_result.filename}") def on_streaming_failed(self, error_msg, search_result): """Handle streaming failure""" @@ -8994,35 +8994,35 @@ class DownloadsPage(QWidget): def _stop_all_streaming_threads(self): """Stop all active streaming threads to prevent old downloads from interrupting new streams""" if hasattr(self, 'streaming_threads'): - print(f"🛑 Stopping {len(self.streaming_threads)} active streaming threads") + print(f"Stopping {len(self.streaming_threads)} active streaming threads") for thread in self.streaming_threads[:]: # Use slice copy to avoid modification during iteration try: if thread.isRunning(): - print(f"🛑 Stopping streaming thread for: {getattr(thread.search_result, 'filename', 'unknown')}") + print(f"Stopping streaming thread for: {getattr(thread.search_result, 'filename', 'unknown')}") thread.stop() # Request stop # Give thread more time to stop gracefully (3 seconds) if not thread.wait(3000): # Wait up to 3 seconds - print(f"⚠️ Streaming thread taking longer to stop, giving more time...") + print(f"Streaming thread taking longer to stop, giving more time...") # Try one more time with longer wait if not thread.wait(2000): # Additional 2 seconds - print(f"⚠️ Force terminating unresponsive streaming thread") + print(f"Force terminating unresponsive streaming thread") thread.terminate() thread.wait(1000) # Wait for termination else: - print(f"✓ Streaming thread stopped gracefully (delayed)") + print(f"Streaming thread stopped gracefully (delayed)") else: - print(f"✓ Streaming thread stopped gracefully") + print(f"Streaming thread stopped gracefully") # Remove from list if thread in self.streaming_threads: self.streaming_threads.remove(thread) except Exception as e: - print(f"⚠️ Error stopping streaming thread: {e}") + print(f"Error stopping streaming thread: {e}") - print(f"✓ All streaming threads stopped") + print(f"All streaming threads stopped") async def _cancel_current_streaming_download(self): """Cancel the current streaming download via slskd API to prevent queue clogging""" @@ -9032,7 +9032,7 @@ class DownloadsPage(QWidget): try: username = self.current_streaming_download['username'] filename = self.current_streaming_download['filename'] - print(f"🚫 Attempting to cancel streaming download: {username}:{os.path.basename(filename)}") + print(f"Attempting to cancel streaming download: {username}:{os.path.basename(filename)}") # Find the download ID by searching current transfers all_transfers = await self.soulseek_client._make_request('GET', 'transfers/downloads') @@ -9053,27 +9053,27 @@ class DownloadsPage(QWidget): break if download_id: - print(f"🚫 Found streaming download ID: {download_id}") + print(f"Found streaming download ID: {download_id}") # Cancel the download with remove=False (slskd won't allow remove=True for active downloads) success = await self.soulseek_client.cancel_download(download_id, username, remove=False) if success: - print(f"✓ Successfully cancelled streaming download: {os.path.basename(filename)}") + print(f"Successfully cancelled streaming download: {os.path.basename(filename)}") else: - print(f"⚠️ Failed to cancel streaming download: {os.path.basename(filename)}") + print(f"Failed to cancel streaming download: {os.path.basename(filename)}") # Try without remove flag as fallback try: success = await self.soulseek_client.cancel_download(download_id, username, remove=False) if success: - print(f"✓ Cancelled streaming download with fallback method: {os.path.basename(filename)}") + print(f"Cancelled streaming download with fallback method: {os.path.basename(filename)}") except Exception as fallback_e: - print(f"⚠️ Fallback cancellation also failed: {fallback_e}") + print(f"Fallback cancellation also failed: {fallback_e}") else: - print(f"⚠️ Could not find download ID for streaming download: {os.path.basename(filename)}") + print(f"Could not find download ID for streaming download: {os.path.basename(filename)}") except Exception as e: - print(f"⚠️ Error cancelling streaming download: {e}") + print(f"Error cancelling streaming download: {e}") # Continue with graceful fallback - don't let cancellation errors break streaming - print(f"🔄 Continuing with new stream despite cancellation error") + print(f"Continuing with new stream despite cancellation error") finally: # Clean up any partial files from the cancelled streaming download if hasattr(self, 'current_streaming_download') and self.current_streaming_download: @@ -9081,7 +9081,7 @@ class DownloadsPage(QWidget): # Clear tracking regardless of success to prevent stuck state self.current_streaming_download = None - print(f"🧹 Cleared streaming download tracking") + print(f"Cleared streaming download tracking") # Also clean up any completed streaming downloads to prevent queue clogging await self._cleanup_completed_streaming_downloads() @@ -9089,7 +9089,7 @@ class DownloadsPage(QWidget): async def _cleanup_completed_streaming_downloads(self): """Remove completed streaming downloads from slskd to prevent queue clogging""" try: - print(f"🧹 Cleaning up completed streaming downloads...") + print(f"Cleaning up completed streaming downloads...") # Get current transfers to find completed ones all_transfers = await self.soulseek_client._make_request('GET', 'transfers/downloads') @@ -9126,19 +9126,19 @@ class DownloadsPage(QWidget): download['id'], download['username'], remove=True ) if success: - print(f"🧹 Cleaned up completed streaming download: {os.path.basename(download['filename'])}") + print(f"Cleaned up completed streaming download: {os.path.basename(download['filename'])}") else: - print(f"⚠️ Failed to clean up: {os.path.basename(download['filename'])}") + print(f"Failed to clean up: {os.path.basename(download['filename'])}") except Exception as e: - print(f"⚠️ Error cleaning up download {download['id']}: {e}") + print(f"Error cleaning up download {download['id']}: {e}") if completed_streaming_downloads: - print(f"🧹 Completed streaming download cleanup: {len(completed_streaming_downloads[:max_cleanup])} items removed") + print(f"Completed streaming download cleanup: {len(completed_streaming_downloads[:max_cleanup])} items removed") else: - print(f"🧹 No completed streaming downloads found to clean up") + print(f"No completed streaming downloads found to clean up") except Exception as e: - print(f"⚠️ Error during streaming download cleanup: {e}") + print(f"Error during streaming download cleanup: {e}") async def _cleanup_cancelled_streaming_files(self, download_info): """Clean up partial files from cancelled streaming downloads""" @@ -9149,7 +9149,7 @@ class DownloadsPage(QWidget): if not username or not filename: return - print(f"🧹 Cleaning up cancelled streaming files for: {os.path.basename(filename)}") + print(f"Cleaning up cancelled streaming files for: {os.path.basename(filename)}") # Get downloads directory from config from config.settings import config_manager @@ -9168,17 +9168,17 @@ class DownloadsPage(QWidget): file_path = os.path.join(root, file) try: - print(f"🗑️ Removing cancelled streaming file: {file_path}") + print(f"Removing cancelled streaming file: {file_path}") os.remove(file_path) # Clean up empty directories self._cleanup_empty_directories(download_path, file_path) except Exception as e: - print(f"⚠️ Error removing file {file_path}: {e}") + print(f"Error removing file {file_path}: {e}") except Exception as e: - print(f"⚠️ Error cleaning up cancelled streaming files: {e}") + print(f"Error cleaning up cancelled streaming files: {e}") def _cancel_current_streaming_download_sync(self): """Synchronous wrapper for cancelling current streaming download""" @@ -9217,8 +9217,8 @@ class DownloadsPage(QWidget): loop.close() except Exception as e: - print(f"⚠️ Error in sync streaming download cancellation: {e}") - print(f"🔄 Continuing with new stream despite sync cancellation error") + print(f"Error in sync streaming download cancellation: {e}") + print(f"Continuing with new stream despite sync cancellation error") # Clear tracking as fallback to prevent stuck state self.current_streaming_download = None @@ -9250,7 +9250,7 @@ class DownloadsPage(QWidget): def on_audio_playback_finished(self): """Handle when audio playback finishes""" - print("🎵 Audio playback completed") + print("Audio playback completed") # Reset the play button to play state if self.currently_playing_button: try: @@ -9272,7 +9272,7 @@ class DownloadsPage(QWidget): def on_audio_playback_error(self, error_msg): """Handle audio playback errors""" - print(f"❌ Audio playback error: {error_msg}") + print(f"Audio playback error: {error_msg}") # Reset the play button to play state if self.currently_playing_button: try: @@ -9298,11 +9298,11 @@ class DownloadsPage(QWidget): from PyQt6.QtMultimedia import QMediaPlayer current_state = self.audio_player.playbackState() - print(f"🎮 handle_sidebar_play_pause() - Current state: {current_state}") - print(f"🎮 handle_sidebar_play_pause() - Current source: {self.audio_player.source().toString()}") + print(f"handle_sidebar_play_pause() - Current state: {current_state}") + print(f"handle_sidebar_play_pause() - Current source: {self.audio_player.source().toString()}") if current_state == QMediaPlayer.PlaybackState.PlayingState: - print("⏸️ Sidebar: Pausing playback") + print("Sidebar: Pausing playback") self.audio_player.pause() # is_playing will be set automatically by _on_playback_state_changed if self.currently_playing_button: @@ -9312,9 +9312,9 @@ class DownloadsPage(QWidget): # Button was deleted, ignore pass self.track_paused.emit() - print("⏸️ Paused from sidebar") + print("Paused from sidebar") else: - print("▶️ Sidebar: Attempting to resume/play") + print("Sidebar: Attempting to resume/play") self.audio_player.play() # is_playing will be set automatically by _on_playback_state_changed if self.currently_playing_button: @@ -9324,7 +9324,7 @@ class DownloadsPage(QWidget): # Button was deleted, ignore pass self.track_resumed.emit() - print("🎵 Resumed from sidebar") + print("Resumed from sidebar") def handle_sidebar_stop(self): """Handle stop request from sidebar media player""" @@ -9346,12 +9346,12 @@ class DownloadsPage(QWidget): # Clear track state self.current_track_id = None self.current_track_result = None - print("⏹️ Stopped from sidebar") + print("Stopped from sidebar") def handle_sidebar_volume(self, volume): """Handle volume change from sidebar media player""" self.audio_player.audio_output.setVolume(volume) - print(f"🔊 Volume set to {int(volume * 100)}% from sidebar") + print(f"Volume set to {int(volume * 100)}% from sidebar") def clear_stream_folder(self, release_current_file=True): """Clear all files from the Stream folder to prevent playing wrong files @@ -9364,7 +9364,7 @@ class DownloadsPage(QWidget): # Only release file handles if explicitly requested if release_current_file and hasattr(self, 'audio_player') and self.audio_player: self.audio_player.release_file() - print("🔓 Released audio player file handle before clearing") + print("Released audio player file handle before clearing") project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # Go up from ui/pages/ stream_folder = os.path.join(project_root, 'Stream') @@ -9375,12 +9375,12 @@ class DownloadsPage(QWidget): if os.path.isfile(file_path): try: os.remove(file_path) - print(f"🗑️ Cleared old stream file: {filename}") + print(f"Cleared old stream file: {filename}") except Exception as e: - print(f"⚠️ Could not remove stream file {filename}: {e}") + print(f"Could not remove stream file {filename}: {e}") except Exception as e: - print(f"⚠️ Error clearing stream folder: {e}") + print(f"Error clearing stream folder: {e}") def on_download_completed(self, message, download_item): """Handle successful download start (NOT completion)""" @@ -9413,7 +9413,7 @@ class DownloadsPage(QWidget): download_item.progress = 0 # Emit activity signal for download failure - self.download_activity.emit("❌", "Download Failed", f"'{download_item.title}' - {error_msg}", "Now") + self.download_activity.emit("", "Download Failed", f"'{download_item.title}' - {error_msg}", "Now") # Error logged to console for debugging @@ -9553,13 +9553,13 @@ class DownloadsPage(QWidget): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - print("[DEBUG] 🗑️ Clearing all completed/cancelled downloads from slskd backend...") + print("[DEBUG] Clearing all completed/cancelled downloads from slskd backend...") success = loop.run_until_complete(self.soulseek_client.clear_all_completed_downloads()) if success: - print("[DEBUG] ✅ Successfully cleared completed/cancelled downloads from backend") + print("[DEBUG] Successfully cleared completed/cancelled downloads from backend") else: - print("[WARNING] ❌ Backend reported failure, but proceeding with UI clearing anyway") + print("[WARNING] Backend reported failure, but proceeding with UI clearing anyway") print("[WARNING] (Web UI may have cleared successfully despite backend failure report)") except Exception as e: @@ -9604,13 +9604,13 @@ class DownloadsPage(QWidget): def _handle_api_cleanup_completion(self, success, download_id, username): """Handle completion of API cleanup operation on main thread""" if success: - print(f"✓ Successfully signaled completion for download {download_id}") + print(f"Successfully signaled completion for download {download_id}") else: - print(f"⚠️ Failed to signal completion for download {download_id}") + print(f"Failed to signal completion for download {download_id}") def _on_download_completion_finished(self, download_item, organized_path): """Handle successful completion of background download processing""" - print(f"✅ Background processing completed for '{download_item.title}' -> {organized_path}") + print(f"Background processing completed for '{download_item.title}' -> {organized_path}") # Update the download item status and progress on main thread download_item.update_status( @@ -9626,7 +9626,7 @@ class DownloadsPage(QWidget): def _on_download_completion_error(self, download_item, error_message): """Handle error in background download processing""" - print(f"❌ Background processing failed for '{download_item.title}': {error_message}") + print(f"Background processing failed for '{download_item.title}': {error_message}") # Still mark as completed but with original path, move to finished queue download_item.update_status( @@ -9771,9 +9771,9 @@ class DownloadsPage(QWidget): stuck_downloads.append((item.title, f"API missing {item.api_missing_count_v2} cycles")) # Log summary - print(f"📊 Queue Health: {status_counts}") + print(f"Queue Health: {status_counts}") if stuck_downloads: - print(f"⚠️ Potentially stuck downloads: {len(stuck_downloads)}") + print(f"Potentially stuck downloads: {len(stuck_downloads)}") for title, issue in stuck_downloads[:3]: # Show first 3 if isinstance(issue, str): print(f" - {title}: {issue}") @@ -9834,7 +9834,7 @@ class DownloadsPage(QWidget): # Track queue state transitions for progressive timeout if new_status in ['queued', 'initializing'] and download_item.queue_start_time is None: download_item.queue_start_time = time.time() - print(f"🕐 Download entered queue: {download_item.title}") + print(f"Download entered queue: {download_item.title}") elif new_status in ['downloading', 'completed', 'cancelled', 'failed']: if download_item.queue_start_time: queue_duration = time.time() - download_item.queue_start_time @@ -9857,7 +9857,7 @@ class DownloadsPage(QWidget): # Reset API missing count when transfer is found and add enhanced logging if hasattr(download_item, 'api_missing_count_v2') and download_item.api_missing_count_v2 > 0: - print(f"✅ Download reconnected to API after {download_item.api_missing_count_v2} missing cycles: {download_item.title}") + print(f"Download reconnected to API after {download_item.api_missing_count_v2} missing cycles: {download_item.title}") download_item.api_missing_count_v2 = 0 # Update ID mapping if needed @@ -9873,9 +9873,9 @@ class DownloadsPage(QWidget): # Emit specific activity signal for failures if new_status == 'failed': - self.download_activity.emit("❌", "Download Failed", f"'{download_item.title}' by {download_item.artist}", "Now") + self.download_activity.emit("", "Download Failed", f"'{download_item.title}' by {download_item.artist}", "Now") elif new_status == 'cancelled': - self.download_activity.emit("⏹️", "Download Cancelled", f"'{download_item.title}' by {download_item.artist}", "Now") + self.download_activity.emit("", "Download Cancelled", f"'{download_item.title}' by {download_item.artist}", "Now") # Cleanup API if needed if new_status in ['cancelled', 'failed'] and hasattr(download_item, 'download_id'): @@ -9899,7 +9899,7 @@ class DownloadsPage(QWidget): current_status = download_item.status.lower() if current_status in ['queued', 'initializing'] and download_item.queue_start_time is None: download_item.queue_start_time = time.time() - print(f"🕐 Started tracking queue time for: {download_item.title}") + print(f"Started tracking queue time for: {download_item.title}") elif current_status not in ['queued', 'initializing']: download_item.queue_start_time = None # Reset if no longer queued @@ -9917,9 +9917,9 @@ class DownloadsPage(QWidget): # Fail download if API missing for 3 cycles OR queue timeout exceeded if download_item.api_missing_count_v2 >= 3 or queue_timeout_exceeded: if queue_timeout_exceeded: - print(f"⚠️ Download failed due to queue timeout: {download_item.title}") + print(f"Download failed due to queue timeout: {download_item.title}") else: - print(f"⚠️ Download missing from API for {download_item.api_missing_count_v2} cycles: {download_item.title}") + print(f"Download missing from API for {download_item.api_missing_count_v2} cycles: {download_item.title}") self._queue_manager.atomic_state_transition(download_item, 'failed') self.download_queue.move_to_finished(download_item) @@ -9959,14 +9959,14 @@ class DownloadsPage(QWidget): return # Success, exit retry loop else: if attempt == max_retries - 1: - print(f"❌ API cleanup failed after {max_retries} attempts: {download_item.title}") + print(f"API cleanup failed after {max_retries} attempts: {download_item.title}") finally: loop.close() except Exception as e: if attempt == max_retries - 1: - print(f"❌ API cleanup failed after {max_retries} attempts: {e}") + print(f"API cleanup failed after {max_retries} attempts: {e}") # Continue to next retry self._optimized_api_pool.submit(cleanup_task_with_retry) @@ -10016,7 +10016,7 @@ class DownloadsPage(QWidget): ) ) if success: - print(f"✅ Fallback cleanup successful for persistent error") + print(f"Fallback cleanup successful for persistent error") except Exception as e: pass # Silent fallback failures @@ -10041,7 +10041,7 @@ class DownloadsPage(QWidget): def enable_optimized_systems(self): """Enable all v2 optimization systems""" self._use_optimized_systems = True - print("✅ Enabled optimized download systems") + print("Enabled optimized download systems") # Switch to optimized timer callback self.download_status_timer.timeout.disconnect() @@ -10053,7 +10053,7 @@ class DownloadsPage(QWidget): def disable_optimized_systems(self): """Disable v2 optimizations and revert to original behavior""" self._use_optimized_systems = False - print("⚠️ Disabled optimizations, reverted to original system") + print("Disabled optimizations, reverted to original system") # Revert to original timer callback self.download_status_timer.timeout.disconnect() @@ -10222,9 +10222,9 @@ class DownloadsPage(QWidget): if success: success_count += 1 - print(f"[CLEANUP] ✅ Successfully removed {state} download: {username}/{download_id}") + print(f"[CLEANUP] Successfully removed {state} download: {username}/{download_id}") else: - print(f"[CLEANUP] ❌ Failed to remove {state} download: {username}/{download_id}") + print(f"[CLEANUP] Failed to remove {state} download: {username}/{download_id}") except Exception as e: print(f"[ERROR] Error removing individual download {download_info}: {e}") @@ -10396,7 +10396,7 @@ class DownloadsPage(QWidget): controls_title.setStyleSheet("color: #ffffff;") # Pause/Resume button - pause_btn = QPushButton("⏸️ Pause Downloads") + pause_btn = QPushButton("Pause Downloads") pause_btn.setFixedHeight(40) pause_btn.setStyleSheet(""" QPushButton { @@ -10413,7 +10413,7 @@ class DownloadsPage(QWidget): """) # Clear completed button - clear_btn = QPushButton("🗑️ Clear Completed") + clear_btn = QPushButton("Clear Completed") clear_btn.setFixedHeight(35) clear_btn.clicked.connect(self.clear_completed_downloads) # Connect to the clearing method clear_btn.setStyleSheet(""" @@ -10513,7 +10513,7 @@ class DownloadsPage(QWidget): count_label.setFont(QFont("Arial", 11)) count_label.setStyleSheet("color: #b3b3b3;") - download_all_btn = QPushButton("📥 Download All") + download_all_btn = QPushButton("Download All") download_all_btn.setFixedSize(150, 35) download_all_btn.setStyleSheet(""" QPushButton { @@ -10614,7 +10614,7 @@ class DownloadsPage(QWidget): info_layout.addWidget(playlist_label) # Download button - download_btn = QPushButton("📥") + download_btn = QPushButton("") download_btn.setFixedSize(30, 30) download_btn.setStyleSheet(""" QPushButton { @@ -10657,7 +10657,7 @@ class DownloadsPage(QWidget): match = re.match(pattern, title.strip()) if match: track_num = int(match.group(1)) - print(f" 🎵 Found track number in title '{title}': {track_num}") + print(f" Found track number in title '{title}': {track_num}") return track_num # Try extracting from filename @@ -10678,20 +10678,20 @@ class DownloadsPage(QWidget): match = re.match(pattern, base_name.strip()) if match: track_num = int(match.group(1)) - print(f" 🎵 Found track number in filename '{filename}': {track_num}") + print(f" Found track number in filename '{filename}': {track_num}") return track_num - print(f" ❌ No track number found in filename: '{filename}'") + print(f" No track number found in filename: '{filename}'") return None except Exception as e: - print(f"❌ Error extracting track number from filename: {e}") + print(f"Error extracting track number from filename: {e}") return None def _get_spotify_album_tracks(self, selected_album: Album) -> List[dict]: """Fetch all tracks from the selected Spotify album""" try: - print(f"🎵 Fetching tracks from Spotify album: {selected_album.name}") + print(f"Fetching tracks from Spotify album: {selected_album.name}") tracks_data = self.spotify_client.get_album_tracks(selected_album.id) if tracks_data and 'items' in tracks_data: @@ -10703,14 +10703,14 @@ class DownloadsPage(QWidget): 'duration_ms': track_data['duration_ms'], 'id': track_data['id'] }) - print(f"✅ Found {len(tracks)} tracks in Spotify album") + print(f"Found {len(tracks)} tracks in Spotify album") return tracks else: - print(f"❌ No tracks found in Spotify album") + print(f"No tracks found in Spotify album") return [] except Exception as e: - print(f"❌ Error fetching Spotify album tracks: {e}") + print(f"Error fetching Spotify album tracks: {e}") return [] def _match_track_to_spotify_title(self, track, spotify_tracks: List[dict]) -> Optional[str]: @@ -10720,7 +10720,7 @@ class DownloadsPage(QWidget): return None original_title = track.title - print(f"🔍 Matching track: '{original_title}'") + print(f"Matching track: '{original_title}'") # Clean the original title by removing track number prefixes import re @@ -10728,7 +10728,7 @@ class DownloadsPage(QWidget): track_num_match = re.match(r'^(\d+)\s*[\.\-_]\s*(.+)', cleaned_original.strip()) if track_num_match: cleaned_original = track_num_match.group(2).strip() - print(f" 🧹 Cleaned title (removed track number): '{cleaned_original}'") + print(f" Cleaned title (removed track number): '{cleaned_original}'") best_match = None best_score = 0.0 @@ -10737,7 +10737,7 @@ class DownloadsPage(QWidget): if hasattr(track, 'track_number') and track.track_number: for spotify_track in spotify_tracks: if spotify_track['track_number'] == track.track_number: - print(f"✅ Matched by track number {track.track_number}: '{spotify_track['name']}'") + print(f"Matched by track number {track.track_number}: '{spotify_track['name']}'") return spotify_track['name'] # Fallback to title similarity matching using cleaned titles @@ -10746,7 +10746,7 @@ class DownloadsPage(QWidget): normalized_original = self.matching_engine.normalize_string(cleaned_original) normalized_spotify = self.matching_engine.normalize_string(spotify_track['name']) - print(f" 📊 Comparing: '{normalized_original}' vs '{normalized_spotify}'") + print(f" Comparing: '{normalized_original}' vs '{normalized_spotify}'") # Calculate similarity score score = self.matching_engine.similarity_score(normalized_original, normalized_spotify) @@ -10754,18 +10754,18 @@ class DownloadsPage(QWidget): if score > best_score: best_score = score best_match = spotify_track - print(f" ⬆️ New best match ({score:.2f}): '{spotify_track['name']}'") + print(f" New best match ({score:.2f}): '{spotify_track['name']}'") # Only return match if confidence is high enough if best_match and best_score >= 0.6: # 60% similarity threshold - print(f"✅ Matched by title similarity ({best_score:.2f}): '{best_match['name']}'") + print(f"Matched by title similarity ({best_score:.2f}): '{best_match['name']}'") return best_match['name'] else: - print(f"❌ No good title match found (best score: {best_score:.2f})") + print(f"No good title match found (best score: {best_score:.2f})") return None except Exception as e: - print(f"❌ Error matching track to Spotify title: {e}") + print(f"Error matching track to Spotify title: {e}") return None # In downloads.py, add this new method inside the DownloadsPage class (e.g., after load_more_results) @@ -10810,9 +10810,9 @@ class DownloadsPage(QWidget): total_filtered = len(self.current_filtered_results) if self.displayed_results < total_filtered: remaining = total_filtered - self.displayed_results - self.update_search_status(f"✨ Showing {self.displayed_results} of {total_filtered} results (scroll for {remaining} more)", "#1db954") + self.update_search_status(f"Showing {self.displayed_results} of {total_filtered} results (scroll for {remaining} more)", "#1db954") else: - self.update_search_status(f"✨ Showing all {total_filtered} results", "#1db954") + self.update_search_status(f"Showing all {total_filtered} results", "#1db954") # In class DownloadsPage, add this new method @@ -10903,21 +10903,21 @@ class DownloadsPage(QWidget): try: # Check if metadata enhancement is enabled if not config_manager.get('metadata_enhancement.enabled', True): - print("🎵 Metadata enhancement disabled in config") + print("Metadata enhancement disabled in config") return True - print(f"🎵 Enhancing metadata for: {os.path.basename(file_path)}") + print(f"Enhancing metadata for: {os.path.basename(file_path)}") # Load the audio file audio_file = MutagenFile(file_path) if audio_file is None: - print(f"❌ Could not load audio file with Mutagen: {file_path}") + print(f"Could not load audio file with Mutagen: {file_path}") return False # Extract comprehensive metadata from Spotify metadata = self._extract_spotify_metadata(download_item, artist, album_info) if not metadata: - print(f"⚠️ Could not extract Spotify metadata, preserving original tags") + print(f"Could not extract Spotify metadata, preserving original tags") return True # Determine file format and apply appropriate tags @@ -10925,19 +10925,19 @@ class DownloadsPage(QWidget): success = False if file_format == 'mp3': - print(f"🎵 Applying ID3 tags for {file_path}") + print(f"Applying ID3 tags for {file_path}") success = self._apply_id3_tags(audio_file, metadata, file_path) elif file_format == 'flac': - print(f"🎵 Applying FLAC tags for {file_path}") + print(f"Applying FLAC tags for {file_path}") success = self._apply_flac_tags(audio_file, metadata, file_path) elif file_format in ['mp4', 'm4a']: - print(f"🎵 Applying MP4 tags for {file_path}") + print(f"Applying MP4 tags for {file_path}") success = self._apply_mp4_tags(audio_file, metadata, file_path) elif file_format == 'ogg': - print(f"🎵 Applying OGG tags for {file_path}") + print(f"Applying OGG tags for {file_path}") success = self._apply_ogg_tags(audio_file, metadata, file_path) else: - print(f"⚠️ Unsupported audio format for metadata enhancement: {file_format}") + print(f"Unsupported audio format for metadata enhancement: {file_format}") return True if success: @@ -10945,14 +10945,14 @@ class DownloadsPage(QWidget): if config_manager.get('metadata_enhancement.embed_album_art', True): self._embed_album_art_metadata(file_path, audio_file, metadata, file_format) - print(f"✅ Metadata enhanced with Spotify data") + print(f"Metadata enhanced with Spotify data") return True else: - print(f"⚠️ Metadata enhancement failed, original tags preserved") + print(f"Metadata enhancement failed, original tags preserved") return False except Exception as e: - print(f"❌ Error enhancing metadata for {file_path}: {e}") + print(f"Error enhancing metadata for {file_path}: {e}") return False def _extract_spotify_metadata(self, download_item, artist: Artist, album_info: dict) -> dict: @@ -10970,14 +10970,14 @@ class DownloadsPage(QWidget): metadata = {} # Debug: Log what we're working with - print(f"🔍 Extracting metadata for: {download_item.title}") + print(f"Extracting metadata for: {download_item.title}") print(f" - Artist: {artist.name if artist else 'None'}") print(f" - Album info: {album_info}") print(f" - Has _spotify_clean_title: {hasattr(download_item, '_spotify_clean_title')}") print(f" - Has matched_album: {hasattr(download_item, 'matched_album')}") if not artist: - print(f"❌ No artist provided for metadata extraction") + print(f"No artist provided for metadata extraction") return {} # Basic track information @@ -11022,7 +11022,7 @@ class DownloadsPage(QWidget): if hasattr(download_item, 'matched_album') and download_item.matched_album: metadata['spotify_album_id'] = getattr(download_item.matched_album, 'id', None) - print(f"🎯 Extracted metadata summary:") + print(f"Extracted metadata summary:") print(f" - Title: {metadata.get('title')}") print(f" - Artist: {metadata.get('artist')}") print(f" - Album: {metadata.get('album')}") @@ -11034,7 +11034,7 @@ class DownloadsPage(QWidget): # Special debugging for problematic tracks if metadata.get('title') == 'Tell Me What You Want' or metadata.get('track_number') == 13: - print(f"🔍 [DEBUG] Special track detected - Tell Me What You Want:") + print(f"[DEBUG] Special track detected - Tell Me What You Want:") print(f" - Full metadata dict: {metadata}") print(f" - Album info dict: {album_info}") print(f" - Artist object: {artist}") @@ -11046,7 +11046,7 @@ class DownloadsPage(QWidget): return metadata except Exception as e: - print(f"❌ Error extracting Spotify metadata: {e}") + print(f"Error extracting Spotify metadata: {e}") import traceback traceback.print_exc() return {} @@ -11084,7 +11084,7 @@ class DownloadsPage(QWidget): return 'unknown' except Exception as e: - print(f"⚠️ Could not detect audio format: {e}") + print(f"Could not detect audio format: {e}") return 'unknown' def _apply_id3_tags(self, audio_file, metadata: dict, file_path: str) -> bool: @@ -11130,13 +11130,13 @@ class DownloadsPage(QWidget): return True except Exception as e: - print(f"❌ Error applying ID3 tags: {e}") + print(f"Error applying ID3 tags: {e}") return False def _apply_flac_tags(self, audio_file, metadata: dict, file_path: str) -> bool: """Handle FLAC Vorbis comments for lossless files""" try: - print(f"🎵 Applying FLAC tags with {len(metadata)} metadata fields") + print(f"Applying FLAC tags with {len(metadata)} metadata fields") # Read existing tags first to preserve non-empty values existing_title = audio_file.get('TITLE', [''])[0] if audio_file.get('TITLE') else '' @@ -11202,11 +11202,11 @@ class DownloadsPage(QWidget): print(f" - Saving FLAC file with enhanced metadata...") audio_file.save() - print(f" - ✅ FLAC tags saved successfully") + print(f" - FLAC tags saved successfully") return True except Exception as e: - print(f"❌ Error applying FLAC tags: {e}") + print(f"Error applying FLAC tags: {e}") import traceback traceback.print_exc() return False @@ -11249,7 +11249,7 @@ class DownloadsPage(QWidget): return True except Exception as e: - print(f"❌ Error applying MP4 tags: {e}") + print(f"Error applying MP4 tags: {e}") return False def _apply_ogg_tags(self, audio_file, metadata: dict, file_path: str) -> bool: @@ -11287,17 +11287,17 @@ class DownloadsPage(QWidget): return True except Exception as e: - print(f"❌ Error applying OGG tags: {e}") + print(f"Error applying OGG tags: {e}") return False def _embed_album_art_metadata(self, file_path: str, audio_file, metadata: dict, file_format: str) -> bool: """Download and embed high-quality Spotify album art""" try: if not metadata.get('album_art_url'): - print("🎨 No album art URL available for embedding") + print("No album art URL available for embedding") return True - print(f"🎨 Downloading album art for embedding...") + print(f"Downloading album art for embedding...") # Download album art album_art_url = metadata['album_art_url'] @@ -11305,7 +11305,7 @@ class DownloadsPage(QWidget): image_data = response.read() if not image_data: - print("❌ Failed to download album art data") + print("Failed to download album art data") return False # Determine image format @@ -11342,11 +11342,11 @@ class DownloadsPage(QWidget): # Save with embedded art audio_file.save() - print(f"🎨 ✅ Album art successfully embedded") + print(f"Album art successfully embedded") return True except Exception as e: - print(f"❌ Error embedding album art: {e}") + print(f"Error embedding album art: {e}") return False def cleanup_resources(self): @@ -11355,10 +11355,10 @@ class DownloadsPage(QWidget): # Shutdown thread pools if hasattr(self, 'api_thread_pool'): self.api_thread_pool.shutdown(wait=False) - print("🧹 API thread pool shutdown") + print("API thread pool shutdown") if hasattr(self, 'completion_thread_pool'): self.completion_thread_pool.waitForDone(3000) # Wait up to 3 seconds for completion - print("🧹 Completion thread pool shutdown") + print("Completion thread pool shutdown") except Exception as e: - print(f"❌ Error during resource cleanup: {e}") \ No newline at end of file + print(f"Error during resource cleanup: {e}") \ No newline at end of file diff --git a/ui/pages/settings.py b/ui/pages/settings.py index ecb1dc89..c26f9e63 100644 --- a/ui/pages/settings.py +++ b/ui/pages/settings.py @@ -474,7 +474,7 @@ class ServiceTestThread(QThread): # Basic validation first if not self.test_config.get('client_id') or not self.test_config.get('client_secret'): - return False, "✗ Please enter both Client ID and Client Secret" + return False, "Please enter both Client ID and Client Secret" # Save temporarily to test original_client_id = config_manager.get('spotify.client_id') @@ -489,7 +489,7 @@ class ServiceTestThread(QThread): # Check if client was created successfully (has sp object) if client.sp is None: - message = "✗ Failed to create Spotify client.\nCheck your credentials." + message = "Failed to create Spotify client.\nCheck your credentials." success = False else: # Try a simple auth check with timeout @@ -498,17 +498,17 @@ class ServiceTestThread(QThread): if client.is_authenticated(): user_info = client.get_user_info() username = user_info.get('display_name', 'Unknown') if user_info else 'Unknown' - message = f"✓ Spotify connection successful!\nConnected as: {username}" + message = f"Spotify connection successful!\nConnected as: {username}" success = True else: - message = "✗ Spotify authentication failed.\nPlease complete the OAuth flow in your browser." + message = "Spotify authentication failed.\nPlease complete the OAuth flow in your browser." success = False except Exception as auth_e: - message = f"✗ Spotify authentication failed:\n{str(auth_e)}" + message = f"Spotify authentication failed:\n{str(auth_e)}" success = False except Exception as client_e: - message = f"✗ Failed to create Spotify client:\n{str(client_e)}" + message = f"Failed to create Spotify client:\n{str(client_e)}" success = False # Restore original values @@ -524,7 +524,7 @@ class ServiceTestThread(QThread): config_manager.set('spotify.client_secret', original_client_secret) except: pass - return False, f"✗ Spotify test failed:\n{str(e)}" + return False, f"Spotify test failed:\n{str(e)}" def _test_tidal(self): """Test Tidal connection""" @@ -533,7 +533,7 @@ class ServiceTestThread(QThread): # Basic validation first if not self.test_config.get('client_id') or not self.test_config.get('client_secret'): - return False, "✗ Please enter both Client ID and Client Secret" + return False, "Please enter both Client ID and Client Secret" # Save temporarily to test original_client_id = config_manager.get('tidal.client_id') @@ -550,14 +550,14 @@ class ServiceTestThread(QThread): if client.is_authenticated() or client._ensure_valid_token(): user_info = client.get_user_info() username = user_info.get('display_name', 'Tidal User') if user_info else 'Tidal User' - message = f"✓ Tidal connection successful!\nConnected as: {username}\nOAuth flow completed." + message = f"Tidal connection successful!\nConnected as: {username}\nOAuth flow completed." success = True else: - message = "✗ Tidal authentication failed.\nPlease complete the OAuth flow in your browser.\nCheck your credentials and redirect URI." + message = "Tidal authentication failed.\nPlease complete the OAuth flow in your browser.\nCheck your credentials and redirect URI." success = False except Exception as client_e: - message = f"✗ Failed to create Tidal client:\n{str(client_e)}" + message = f"Failed to create Tidal client:\n{str(client_e)}" success = False # Restore original values @@ -573,7 +573,7 @@ class ServiceTestThread(QThread): config_manager.set('tidal.client_secret', original_client_secret) except: pass - return False, f"✗ Tidal test failed:\n{str(e)}" + return False, f"Tidal test failed:\n{str(e)}" def _test_plex(self): """Test Plex connection""" @@ -591,10 +591,10 @@ class ServiceTestThread(QThread): client = PlexClient() if client.is_connected(): server_name = client.server.friendlyName if client.server else 'Unknown' - message = f"✓ Plex connection successful!\nServer: {server_name}" + message = f"Plex connection successful!\nServer: {server_name}" success = True else: - message = "✗ Plex connection failed.\nCheck your server URL and token." + message = "Plex connection failed.\nCheck your server URL and token." success = False # Restore original values @@ -604,7 +604,7 @@ class ServiceTestThread(QThread): return success, message except Exception as e: - return False, f"✗ Plex test failed:\n{str(e)}" + return False, f"Plex test failed:\n{str(e)}" def _test_jellyfin(self): """Test Jellyfin connection""" @@ -634,19 +634,19 @@ class ServiceTestThread(QThread): data = response.json() server_name = data.get('ServerName', 'Unknown') version = data.get('Version', 'Unknown') - message = f"✓ Jellyfin connection successful!\nServer: {server_name}\nVersion: {version}" + message = f"Jellyfin connection successful!\nServer: {server_name}\nVersion: {version}" return True, message elif response.status_code == 401: - return False, "✗ Jellyfin authentication failed.\nCheck your API key." + return False, "Jellyfin authentication failed.\nCheck your API key." else: - return False, f"✗ Jellyfin connection failed.\nHTTP {response.status_code}: {response.text}" + return False, f"Jellyfin connection failed.\nHTTP {response.status_code}: {response.text}" except requests.exceptions.Timeout: - return False, "✗ Jellyfin connection timeout.\nCheck your server URL." + return False, "Jellyfin connection timeout.\nCheck your server URL." except requests.exceptions.ConnectionError: - return False, "✗ Cannot connect to Jellyfin server.\nCheck your server URL and network." + return False, "Cannot connect to Jellyfin server.\nCheck your server URL and network." except Exception as e: - return False, f"✗ Jellyfin test failed:\n{str(e)}" + return False, f"Jellyfin test failed:\n{str(e)}" def _test_navidrome(self): """Test Navidrome connection""" @@ -695,23 +695,23 @@ class ServiceTestThread(QThread): if subsonic_response.get('status') == 'ok': version = subsonic_response.get('version', 'Unknown') - message = f"✓ Navidrome connection successful!\nSubsonic API Version: {version}" + message = f"Navidrome connection successful!\nSubsonic API Version: {version}" return True, message elif subsonic_response.get('status') == 'failed': error = subsonic_response.get('error', {}) error_message = error.get('message', 'Unknown error') - return False, f"✗ Navidrome authentication failed:\n{error_message}" + return False, f"Navidrome authentication failed:\n{error_message}" else: - return False, "✗ Unexpected response from Navidrome server" + return False, "Unexpected response from Navidrome server" else: - return False, f"✗ Navidrome connection failed.\nHTTP {response.status_code}: {response.text}" + return False, f"Navidrome connection failed.\nHTTP {response.status_code}: {response.text}" except requests.exceptions.Timeout: - return False, "✗ Navidrome connection timeout.\nCheck your server URL." + return False, "Navidrome connection timeout.\nCheck your server URL." except requests.exceptions.ConnectionError: - return False, "✗ Cannot connect to Navidrome server.\nCheck your server URL and network." + return False, "Cannot connect to Navidrome server.\nCheck your server URL and network." except Exception as e: - return False, f"✗ Navidrome test failed:\n{str(e)}" + return False, f"Navidrome test failed:\n{str(e)}" def _test_soulseek(self): """Test Soulseek connection""" @@ -734,31 +734,31 @@ class ServiceTestThread(QThread): response = requests.get(f"{slskd_url}/api/v0/session", headers=headers, timeout=5) if response.status_code == 200: - return True, "✓ Soulseek connection successful!\nslskd is responding." + return True, "Soulseek connection successful!\nslskd is responding." elif response.status_code == 401: - return False, ("✗ Invalid API key\n\n" + return False, ("Invalid API key\n\n" "Please check your slskd API key in the configuration.") else: - return False, (f"✗ Soulseek connection failed\nHTTP {response.status_code}\n\n" + return False, (f"Soulseek connection failed\nHTTP {response.status_code}\n\n" "slskd is running but returned an error.") except requests.exceptions.ConnectionError as e: if "refused" in str(e).lower(): - return False, ("✗ Cannot connect to slskd\n\n" + return False, ("Cannot connect to slskd\n\n" "slskd appears to not be running on the specified URL.\n\n" "To fix this:\n" "1. Install slskd from: https://github.com/slskd/slskd\n" "2. Start slskd service\n" "3. Ensure it's running on the correct port (default: 5030)") else: - return False, f"✗ Network error:\n{str(e)}" + return False, f"Network error:\n{str(e)}" except requests.exceptions.Timeout: - return False, ("✗ Connection timed out\n\n" + return False, ("Connection timed out\n\n" "slskd is not responding. Check if it's running and accessible.") except requests.exceptions.RequestException as e: - return False, f"✗ Request failed:\n{str(e)}" + return False, f"Request failed:\n{str(e)}" except Exception as e: - return False, f"✗ Unexpected error:\n{str(e)}" + return False, f"Unexpected error:\n{str(e)}" class JellyfinDetectionThread(QThread): progress_updated = pyqtSignal(int, str) # progress value, current url @@ -1004,7 +1004,7 @@ class NavidromeDetectionThread(QThread): print(f"Response data: {data}") # Check if it's a valid Subsonic API response if 'subsonic-response' in data: - print(f"✓ Found Navidrome server at {url}") + print(f"Found Navidrome server at {url}") return True except Exception as e: print(f"JSON parse error: {e}") @@ -1013,7 +1013,7 @@ class NavidromeDetectionThread(QThread): try: root_response = requests.get(url, timeout=timeout) if root_response.status_code == 200 and 'navidrome' in root_response.text.lower(): - print(f"✓ Found Navidrome web interface at {url}") + print(f"Found Navidrome web interface at {url}") return True except: pass @@ -1166,7 +1166,7 @@ class SettingsPage(QWidget): content_layout.addStretch() # Save button - self.save_btn = QPushButton("💾 Save Settings") + self.save_btn = QPushButton("Save Settings") self.save_btn.setFixedHeight(45) self.save_btn.clicked.connect(self.save_settings) self.save_btn.setStyleSheet(""" @@ -1333,7 +1333,7 @@ class SettingsPage(QWidget): # Update button text temporarily original_text = self.save_btn.text() - self.save_btn.setText("✓ Saved!") + self.save_btn.setText("Saved!") self.save_btn.setStyleSheet(""" QPushButton { background: #1aa34a; @@ -1397,20 +1397,20 @@ class SettingsPage(QWidget): # Create client and authenticate client = TidalClient() - self.tidal_auth_btn.setText("🔐 Authenticating...") + self.tidal_auth_btn.setText("Authenticating...") self.tidal_auth_btn.setEnabled(False) if client.authenticate(): - QMessageBox.information(self, "Success", "✓ Tidal authentication successful!\nYou can now use Tidal playlists.") - self.tidal_auth_btn.setText("✅ Authenticated") + QMessageBox.information(self, "Success", "Tidal authentication successful!\nYou can now use Tidal playlists.") + self.tidal_auth_btn.setText("Authenticated") else: - QMessageBox.warning(self, "Authentication Failed", "✗ Tidal authentication failed.\nPlease check your credentials and try again.") - self.tidal_auth_btn.setText("🔐 Authenticate") + QMessageBox.warning(self, "Authentication Failed", "Tidal authentication failed.\nPlease check your credentials and try again.") + self.tidal_auth_btn.setText("Authenticate") self.tidal_auth_btn.setEnabled(True) except Exception as e: - self.tidal_auth_btn.setText("🔐 Authenticate") + self.tidal_auth_btn.setText("Authenticate") self.tidal_auth_btn.setEnabled(True) QMessageBox.critical(self, "Error", f"Failed to authenticate with Tidal:\n{str(e)}") @@ -1826,7 +1826,7 @@ class SettingsPage(QWidget): # Success message location_type = "locally" if "localhost" in found_url or "127.0.0.1" in found_url else "on network" - success_label = QLabel(f"✓ Found Plex server running {location_type}!") + success_label = QLabel(f"Found Plex server running {location_type}!") success_label.setStyleSheet("color: #e5a00d; font-size: 13px; font-weight: bold;") success_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(success_label) @@ -1937,7 +1937,7 @@ class SettingsPage(QWidget): # Success message location_type = "locally" if "localhost" in found_url or "127.0.0.1" in found_url else "on network" - success_label = QLabel(f"✓ Found Jellyfin server running {location_type}!") + success_label = QLabel(f"Found Jellyfin server running {location_type}!") success_label.setStyleSheet("color: #aa5cc3; font-size: 13px; font-weight: bold;") success_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(success_label) @@ -2048,7 +2048,7 @@ class SettingsPage(QWidget): # Success message location_type = "locally" if "localhost" in found_url or "127.0.0.1" in found_url else "on network" - success_label = QLabel(f"✓ Found slskd running {location_type}!") + success_label = QLabel(f"Found slskd running {location_type}!") success_label.setStyleSheet("color: #1db954; font-size: 13px; font-weight: bold;") success_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(success_label) @@ -2315,7 +2315,7 @@ class SettingsPage(QWidget): tidal_layout.addWidget(oauth_url_label) # Authenticate button - self.tidal_auth_btn = QPushButton("🔐 Authenticate") + self.tidal_auth_btn = QPushButton("Authenticate") self.tidal_auth_btn.setFixedHeight(30) self.tidal_auth_btn.setStyleSheet(""" QPushButton { @@ -2378,7 +2378,7 @@ class SettingsPage(QWidget): server_selection_layout.addLayout(toggle_container) # Restart warning (initially hidden) - self.restart_warning_frame = QLabel("⚠️ Server change requires restart - Save settings then restart SoulSync") + self.restart_warning_frame = QLabel("Server change requires restart - Save settings then restart SoulSync") self.restart_warning_frame.setStyleSheet(""" color: #ffc107; font-size: 11px; @@ -2764,7 +2764,7 @@ class SettingsPage(QWidget): database_layout.addWidget(workers_help) # Metadata Enhancement Settings - metadata_group = SettingsGroup("🎵 Metadata Enhancement") + metadata_group = SettingsGroup("Metadata Enhancement") metadata_layout = QVBoxLayout(metadata_group) metadata_layout.setContentsMargins(16, 20, 16, 16) metadata_layout.setSpacing(12) @@ -2831,13 +2831,13 @@ class SettingsPage(QWidget): metadata_layout.addWidget(help_text) # Playlist Sync Settings - playlist_sync_group = SettingsGroup("🎶 Playlist Sync") + playlist_sync_group = SettingsGroup("Playlist Sync") playlist_sync_layout = QVBoxLayout(playlist_sync_group) playlist_sync_layout.setContentsMargins(16, 20, 16, 16) playlist_sync_layout.setSpacing(12) # Create backup checkbox - self.create_backup_checkbox = QCheckBox("🛡️ Create backup of existing playlists before sync") + self.create_backup_checkbox = QCheckBox("Create backup of existing playlists before sync") self.create_backup_checkbox.setChecked(True) self.create_backup_checkbox.setStyleSheet(""" QCheckBox { @@ -3367,12 +3367,12 @@ class SettingsPage(QWidget): # Show success toast from ui.components.toast_manager import ToastManager toast_manager = ToastManager(self) - toast_manager.show_toast(f"✓ Navidrome server detected: {found_url}", "success", 4000) + toast_manager.show_toast(f"Navidrome server detected: {found_url}", "success", 4000) else: # Show error toast from ui.components.toast_manager import ToastManager toast_manager = ToastManager(self) - toast_manager.show_toast("❌ No Navidrome servers found on the network", "error", 4000) + toast_manager.show_toast("No Navidrome servers found on the network", "error", 4000) def on_jellyfin_detection_completed(self, found_url): """Handle Jellyfin detection completion""" diff --git a/ui/pages/sync.py b/ui/pages/sync.py index 87f45069..9e70c1f1 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -146,7 +146,7 @@ def clean_track_name_for_search(track_name): # Log cleaning if significant changes were made if cleaned_name != track_name: - print(f"🧹 Intelligent track cleaning: '{track_name}' -> '{cleaned_name}'") + print(f"Intelligent track cleaning: '{track_name}' -> '{cleaned_name}'") return cleaned_name @@ -179,7 +179,7 @@ def clean_youtube_track_title(title, artist_name=None): # Debug logging for artist removal if cleaned_title != title: - print(f"🎯 Removed artist from title: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')") + print(f"Removed artist from title: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')") title = cleaned_title @@ -256,7 +256,7 @@ def clean_youtube_track_title(title, artist_name=None): title = original_title if title != original_title: - print(f"🧹 YouTube title cleaned: '{original_title}' → '{title}'") + print(f"YouTube title cleaned: '{original_title}' → '{title}'") return title @@ -321,7 +321,7 @@ def clean_youtube_artist(artist_string): artist_string = original_artist if artist_string != original_artist: - print(f"🧹 YouTube artist cleaned: '{original_artist}' → '{artist_string}'") + print(f"YouTube artist cleaned: '{original_artist}' → '{artist_string}'") return artist_string @@ -359,13 +359,13 @@ def parse_youtube_playlist(url): # Extract playlist title playlist_title = playlist_info.get('title', 'YouTube Playlist') - print(f"🎵 Found {len(entries)} tracks in YouTube playlist: '{playlist_title}'") - print(f"📊 Playlist info keys: {list(playlist_info.keys())}") + print(f"Found {len(entries)} tracks in YouTube playlist: '{playlist_title}'") + print(f"Playlist info keys: {list(playlist_info.keys())}") if 'playlist_count' in playlist_info: - print(f"📊 Reported playlist count: {playlist_info['playlist_count']}") + print(f"Reported playlist count: {playlist_info['playlist_count']}") if 'n_entries' in playlist_info: - print(f"📊 Reported n_entries: {playlist_info['n_entries']}") - print(f"📊 Actual entries length: {len(entries)}") + print(f"Reported n_entries: {playlist_info['n_entries']}") + print(f"Actual entries length: {len(entries)}") # Convert each entry to our Track format for i, entry in enumerate(entries): @@ -449,21 +449,21 @@ def parse_youtube_playlist(url): # Log the parsing result for debugging if track_name != raw_title or artists[0] != raw_uploader: - print(f"🎯 Parsed: '{raw_title}' by '{raw_uploader}' → '{track_name}' by '{artists[0]}'") + print(f"Parsed: '{raw_title}' by '{raw_uploader}' → '{track_name}' by '{artists[0]}'") except Exception as e: - print(f"⚠️ Error processing track {i}: {e}") + print(f"Error processing track {i}: {e}") continue - print(f"✅ Successfully processed {len(tracks)} tracks out of {len(entries)} entries") + print(f"Successfully processed {len(tracks)} tracks out of {len(entries)} entries") if len(tracks) != len(entries): skipped = len(entries) - len(tracks) - print(f"⚠️ Skipped {skipped} tracks due to processing errors") + print(f"Skipped {skipped} tracks due to processing errors") return tracks, playlist_title except Exception as e: - print(f"❌ Error parsing YouTube playlist: {e}") + print(f"Error parsing YouTube playlist: {e}") raise e def create_youtube_playlist_object(tracks_data, playlist_url, playlist_title=None): @@ -514,7 +514,7 @@ def create_youtube_playlist_object(tracks_data, playlist_url, playlist_title=Non return playlist except Exception as e: - print(f"❌ Error creating YouTube playlist object: {e}") + print(f"Error creating YouTube playlist object: {e}") raise e @dataclass @@ -644,7 +644,7 @@ class PlaylistTrackAnalysisWorker(QRunnable): db_track, confidence = db.check_track_exists(query_title, artist_name, confidence_threshold=0.7, server_source=active_server) if db_track and confidence >= 0.7: - print(f"✔️ Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}") + print(f"Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}") # Convert database track to format compatible with existing code # Create a mock Plex track object for compatibility @@ -661,7 +661,7 @@ class PlaylistTrackAnalysisWorker(QRunnable): mock_track = MockPlexTrack(db_track) return mock_track, confidence - print(f"❌ No database match found for '{original_title}' by any of the artists {artists_to_search}") + print(f"No database match found for '{original_title}' by any of the artists {artists_to_search}") return None, 0.0 except Exception as e: @@ -717,7 +717,7 @@ class TrackDownloadWorker(QRunnable): if self._cancelled: return - print(f"🔍 Searching Soulseek: {query}") + print(f"Searching Soulseek: {query}") # Use the async method (need to run in sync context) import asyncio @@ -839,7 +839,7 @@ class SyncStatusProcessingWorker(QRunnable): item_data['api_missing_count'] = item_data.get('api_missing_count', 0) + 1 if item_data['api_missing_count'] >= 3: expected_filename = os.path.basename(item_data['file_path']) - print(f"❌ Download failed (missing from API after 3 checks): {expected_filename}") + print(f"Download failed (missing from API after 3 checks): {expected_filename}") payload = {'widget_id': item_data['widget_id'], 'status': 'failed'} results.append(payload) @@ -1018,14 +1018,14 @@ class SyncWorker(QRunnable): # Set up progress callback for sync service def on_progress(progress): - print(f"⚡ SyncWorker progress callback called! total={progress.total_tracks}, matched={progress.matched_tracks}") + print(f"SyncWorker progress callback called! total={progress.total_tracks}, matched={progress.matched_tracks}") if not self._cancelled: - print(f"⚡ Emitting progress signal to parent page") + print(f"Emitting progress signal to parent page") self.signals.progress.emit(progress) else: - print(f"⚡ Sync was cancelled, not emitting signal") + print(f"Sync was cancelled, not emitting signal") - print(f"⚡ Setting up progress callback for playlist: '{self.playlist.name}'") + print(f"Setting up progress callback for playlist: '{self.playlist.name}'") self.sync_service.set_progress_callback(on_progress, self.playlist.name) # Create new event loop for this thread @@ -1287,17 +1287,17 @@ class PlaylistDetailsModal(QDialog): layout.setSpacing(12) # Total tracks - self.total_tracks_label = QLabel("♪ 0") + self.total_tracks_label = QLabel("0") self.total_tracks_label.setFont(QFont("SF Pro Text", 12, QFont.Weight.Medium)) self.total_tracks_label.setStyleSheet("color: #ffa500; background: transparent; border: none;") # Matched tracks - self.matched_tracks_label = QLabel("✓ 0") + self.matched_tracks_label = QLabel("0") self.matched_tracks_label.setFont(QFont("SF Pro Text", 12, QFont.Weight.Medium)) self.matched_tracks_label.setStyleSheet("color: #1db954; background: transparent; border: none;") # Failed tracks - self.failed_tracks_label = QLabel("✗ 0") + self.failed_tracks_label = QLabel("0") self.failed_tracks_label.setFont(QFont("SF Pro Text", 12, QFont.Weight.Medium)) self.failed_tracks_label.setStyleSheet("color: #e22134; background: transparent; border: none;") @@ -1337,9 +1337,9 @@ class PlaylistDetailsModal(QDialog): def update_sync_status(self, total_tracks=0, matched_tracks=0, failed_tracks=0): """Update sync status display""" if self.sync_status_widget: - self.total_tracks_label.setText(f"♪ {total_tracks}") - self.matched_tracks_label.setText(f"✓ {matched_tracks}") - self.failed_tracks_label.setText(f"✗ {failed_tracks}") + self.total_tracks_label.setText(f"{total_tracks}") + self.matched_tracks_label.setText(f"{matched_tracks}") + self.failed_tracks_label.setText(f"{failed_tracks}") if total_tracks > 0: processed_tracks = matched_tracks + failed_tracks @@ -1647,7 +1647,7 @@ class PlaylistDetailsModal(QDialog): def on_download_missing_tracks_clicked(self): """Handle Download Missing Tracks button click""" - print("🔄 Download Missing Tracks button clicked!") + print("Download Missing Tracks button clicked!") if not self.playlist or not self.playlist.tracks: QMessageBox.warning(self, "Error", "Playlist tracks not loaded") @@ -1658,7 +1658,7 @@ class PlaylistDetailsModal(QDialog): QMessageBox.critical(self, "Error", "Could not find the associated playlist item on the main page.") return - print("🚀 Creating DownloadMissingTracksModal...") + print("Creating DownloadMissingTracksModal...") modal = DownloadMissingTracksModal(self.playlist, playlist_item_widget, self.parent_page, self.parent_page.downloads_page) playlist_item_widget.download_modal = modal @@ -1724,13 +1724,13 @@ class PlaylistDetailsModal(QDialog): # Update Tidal card state to syncing (matches YouTube workflow) if hasattr(self, 'is_tidal_playlist') and self.is_tidal_playlist and hasattr(self, 'playlist_id'): - print(f"🎵 Updating Tidal card state to syncing for playlist_id: {self.playlist_id}") + print(f"Updating Tidal card state to syncing for playlist_id: {self.playlist_id}") if hasattr(self.parent_page, 'update_tidal_card_phase'): self.parent_page.update_tidal_card_phase(self.playlist_id, 'syncing') # Update YouTube card state to syncing (existing logic) if hasattr(self, 'youtube_url'): - print(f"🎬 Updating YouTube card state to syncing for URL: {self.youtube_url}") + print(f"Updating YouTube card state to syncing for URL: {self.youtube_url}") if hasattr(self.parent_page, 'update_youtube_card_phase'): self.parent_page.update_youtube_card_phase(self.youtube_url, 'syncing') @@ -2070,7 +2070,7 @@ class PlaylistDetailsModal(QDialog): self.setWindowTitle(f"{original_title} (Showing {display_limit} of {total_tracks:,} tracks)") # Also show a subtle message at the bottom of the table - print(f"📊 Playlist Details: Showing first {display_limit} of {total_tracks:,} tracks for better performance") + print(f"Playlist Details: Showing first {display_limit} of {total_tracks:,} tracks for better performance") class PlaylistItem(QFrame): view_details_clicked = pyqtSignal(object) # Signal to emit playlist object @@ -2339,15 +2339,15 @@ class PlaylistItem(QFrame): layout.setContentsMargins(8, 6, 8, 6) layout.setSpacing(6) - self.item_total_tracks_label = QLabel("♪ 0") + self.item_total_tracks_label = QLabel("0") self.item_total_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.item_total_tracks_label.setStyleSheet("color: #ffa500; background: transparent; border: none;") - self.item_matched_tracks_label = QLabel("✓ 0") + self.item_matched_tracks_label = QLabel("0") self.item_matched_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.item_matched_tracks_label.setStyleSheet("color: #1db954; background: transparent; border: none;") - self.item_failed_tracks_label = QLabel("✗ 0") + self.item_failed_tracks_label = QLabel("0") self.item_failed_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.item_failed_tracks_label.setStyleSheet("color: #e22134; background: transparent; border: none;") @@ -2387,9 +2387,9 @@ class PlaylistItem(QFrame): self.sync_failed_tracks = failed_tracks if self.sync_status_widget and hasattr(self, 'item_total_tracks_label'): - self.item_total_tracks_label.setText(f"♪ {total_tracks}") - self.item_matched_tracks_label.setText(f"✓ {matched_tracks}") - self.item_failed_tracks_label.setText(f"✗ {failed_tracks}") + self.item_total_tracks_label.setText(f"{total_tracks}") + self.item_matched_tracks_label.setText(f"{matched_tracks}") + self.item_failed_tracks_label.setText(f"{failed_tracks}") if total_tracks > 0: processed_tracks = matched_tracks + failed_tracks @@ -2500,7 +2500,7 @@ class TidalPlaylistCard(QFrame): layout.setSpacing(15) # Tidal icon indicator - tidal_icon = QLabel("🎵") + tidal_icon = QLabel("") tidal_icon.setFixedSize(24, 24) tidal_icon.setStyleSheet(""" QLabel { @@ -2611,15 +2611,15 @@ class TidalPlaylistCard(QFrame): layout.setSpacing(8) # Create labels for progress display - self.total_tracks_label = QLabel(f"♪ {self.progress_data['total']}") + self.total_tracks_label = QLabel(f"{self.progress_data['total']}") self.total_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.total_tracks_label.setStyleSheet("color: #b3b3b3; background: transparent;") - self.matched_tracks_label = QLabel(f"✓ {self.progress_data['matched']}") + self.matched_tracks_label = QLabel(f"{self.progress_data['matched']}") self.matched_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.matched_tracks_label.setStyleSheet("color: #1db954; background: transparent;") - self.failed_tracks_label = QLabel(f"✗ {self.progress_data['failed']}") + self.failed_tracks_label = QLabel(f"{self.progress_data['failed']}") self.failed_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.failed_tracks_label.setStyleSheet("color: #e22134; background: transparent;") @@ -2680,7 +2680,7 @@ class TidalPlaylistCard(QFrame): # Show/hide progress widget based on phase if phase in ['syncing', 'downloading', 'sync_complete']: - print(f"🎵 Tidal card phase set to {phase} - showing progress widget") + print(f"Tidal card phase set to {phase} - showing progress widget") self.progress_widget.show() self.action_btn.hide() # For syncing phase, initialize with current progress data @@ -2689,9 +2689,9 @@ class TidalPlaylistCard(QFrame): if self.progress_data['total'] == 0: # Initialize with track count if available self.progress_data['total'] = self.track_count - self.total_tracks_label.setText(f"♪ {self.progress_data['total']}") - self.matched_tracks_label.setText(f"✓ {self.progress_data['matched']}") - self.failed_tracks_label.setText(f"✗ {self.progress_data['failed']}") + self.total_tracks_label.setText(f"{self.progress_data['total']}") + self.matched_tracks_label.setText(f"{self.progress_data['matched']}") + self.failed_tracks_label.setText(f"{self.progress_data['failed']}") # For sync_complete, hide progress after a delay to show final results elif phase == 'sync_complete': from PyQt6.QtCore import QTimer @@ -2711,9 +2711,9 @@ class TidalPlaylistCard(QFrame): """Update progress data and refresh progress display""" self.progress_data = {'total': total, 'matched': matched, 'failed': failed} if self.progress_widget.isVisible(): - self.total_tracks_label.setText(f"♪ {total}") - self.matched_tracks_label.setText(f"✓ {matched}") - self.failed_tracks_label.setText(f"✗ {failed}") + self.total_tracks_label.setText(f"{total}") + self.matched_tracks_label.setText(f"{matched}") + self.failed_tracks_label.setText(f"{failed}") def on_action_clicked(self): """Handle action button click - emit signal with current phase""" @@ -2771,7 +2771,7 @@ class YouTubePlaylistCard(QFrame): layout.setSpacing(15) # YouTube icon indicator - yt_icon = QLabel("▶") + yt_icon = QLabel("") yt_icon.setFixedSize(24, 24) yt_icon.setStyleSheet(""" QLabel { @@ -2857,7 +2857,7 @@ class YouTubePlaylistCard(QFrame): layout.setSpacing(8) # Create labels for progress display - self.total_tracks_label = QLabel(f"♪ {self.progress_data['total']}") + self.total_tracks_label = QLabel(f"{self.progress_data['total']}") self.total_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.total_tracks_label.setStyleSheet("color: #b3b3b3; background: transparent; border: none;") layout.addWidget(self.total_tracks_label) @@ -2867,7 +2867,7 @@ class YouTubePlaylistCard(QFrame): sep1.setStyleSheet("color: #666666; background: transparent; border: none;") layout.addWidget(sep1) - self.matched_tracks_label = QLabel(f"✓ {self.progress_data['matched']}") + self.matched_tracks_label = QLabel(f"{self.progress_data['matched']}") self.matched_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.matched_tracks_label.setStyleSheet("color: #1db954; background: transparent; border: none;") layout.addWidget(self.matched_tracks_label) @@ -2877,7 +2877,7 @@ class YouTubePlaylistCard(QFrame): sep2.setStyleSheet("color: #666666; background: transparent; border: none;") layout.addWidget(sep2) - self.failed_tracks_label = QLabel(f"✗ {self.progress_data['failed']}") + self.failed_tracks_label = QLabel(f"{self.progress_data['failed']}") self.failed_tracks_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium)) self.failed_tracks_label.setStyleSheet("color: #e22134; background: transparent; border: none;") layout.addWidget(self.failed_tracks_label) @@ -2946,7 +2946,7 @@ class YouTubePlaylistCard(QFrame): # Show/hide progress widget based on phase if phase in ['syncing', 'downloading', 'sync_complete']: - print(f"🎬 Card phase set to {phase} - showing progress widget") + print(f"Card phase set to {phase} - showing progress widget") self.progress_widget.show() self.action_btn.hide() # For syncing phase, initialize with current progress data @@ -2955,22 +2955,22 @@ class YouTubePlaylistCard(QFrame): if self.progress_data['total'] == 0: # Initialize with track count if available self.progress_data['total'] = self.track_count - self.total_tracks_label.setText(f"♪ {self.progress_data['total']}") - self.matched_tracks_label.setText(f"✓ {self.progress_data['matched']}") - self.failed_tracks_label.setText(f"✗ {self.progress_data['failed']}") + self.total_tracks_label.setText(f"{self.progress_data['total']}") + self.matched_tracks_label.setText(f"{self.progress_data['matched']}") + self.failed_tracks_label.setText(f"{self.progress_data['failed']}") # For sync_complete, hide progress after a delay to show final results elif phase == 'sync_complete': from PyQt6.QtCore import QTimer QTimer.singleShot(5000, lambda: self.progress_widget.hide() if self.phase == 'sync_complete' else None) QTimer.singleShot(5000, lambda: self.action_btn.show() if self.phase == 'sync_complete' else None) else: - print(f"🎬 Card phase set to {phase} - hiding progress widget") + print(f"Card phase set to {phase} - hiding progress widget") self.progress_widget.hide() self.action_btn.show() def update_progress(self, total=None, matched=None, failed=None): """Update progress data and display""" - print(f"🎬 Card update_progress called: total={total}, matched={matched}, failed={failed}, phase={self.phase}") + print(f"Card update_progress called: total={total}, matched={matched}, failed={failed}, phase={self.phase}") if total is not None: self.progress_data['total'] = total @@ -2980,18 +2980,18 @@ class YouTubePlaylistCard(QFrame): self.progress_data['failed'] = failed # Update labels - self.total_tracks_label.setText(f"♪ {self.progress_data['total']}") - self.matched_tracks_label.setText(f"✓ {self.progress_data['matched']}") - self.failed_tracks_label.setText(f"✗ {self.progress_data['failed']}") + self.total_tracks_label.setText(f"{self.progress_data['total']}") + self.matched_tracks_label.setText(f"{self.progress_data['matched']}") + self.failed_tracks_label.setText(f"{self.progress_data['failed']}") # Ensure progress widget is visible when progress is being updated # This ensures live status display is always shown during active operations if self.phase in ['syncing', 'downloading']: - print(f"🎬 Card in {self.phase} phase - ensuring progress widget is visible") + print(f"Card in {self.phase} phase - ensuring progress widget is visible") self.progress_widget.show() self.action_btn.hide() else: - print(f"🎬 Card not in active phase ({self.phase}) - progress widget state unchanged") + print(f"Card not in active phase ({self.phase}) - progress widget state unchanged") # Calculate percentage total = self.progress_data['total'] @@ -3134,7 +3134,7 @@ class SyncPage(QWidget): self.scan_manager = MediaScanManager(delay_seconds=60) # Add automatic incremental database update after scan completion self.scan_manager.add_scan_completion_callback(self._on_media_scan_completed) - logger.info("✅ MediaScanManager initialized for SyncPage") + logger.info("MediaScanManager initialized for SyncPage") except Exception as e: logger.error(f"Failed to initialize MediaScanManager: {e}") @@ -3188,7 +3188,7 @@ class SyncPage(QWidget): return # All conditions met - start incremental update - logger.info(f"🎵 Starting automatic incremental database update after {active_server.upper()} scan") + logger.info(f"Starting automatic incremental database update after {active_server.upper()} scan") self._start_automatic_incremental_update() except Exception as e: @@ -3225,13 +3225,13 @@ class SyncPage(QWidget): """Handle completion of automatic database update""" try: if successful > 0: - logger.info(f"✅ Automatic database update completed: {successful} items processed successfully") + logger.info(f"Automatic database update completed: {successful} items processed successfully") else: - logger.info("💡 Automatic database update completed - no new content found") + logger.info("Automatic database update completed - no new content found") # Emit the signal to notify the dashboard to refresh its statistics self.database_updated_externally.emit() - logger.info("📊 Emitted signal to refresh dashboard database statistics after auto update") + logger.info("Emitted signal to refresh dashboard database statistics after auto update") # Clean up the worker if hasattr(self, '_auto_database_worker'): @@ -3314,7 +3314,7 @@ class SyncPage(QWidget): self.active_sync_workers[playlist.id] = sync_worker # Emit activity signal for sync start - self.sync_activity.emit("🔄", "Sync Started", f"Syncing playlist '{playlist.name}'", "Now") + self.sync_activity.emit("", "Sync Started", f"Syncing playlist '{playlist.name}'", "Now") # Show toast notification for sync start if hasattr(self, 'toast_manager') and self.toast_manager: @@ -3333,7 +3333,7 @@ class SyncPage(QWidget): # Log start if hasattr(self, 'log_area'): - self.log_area.append(f"🔄 Starting sync for playlist: {playlist.name}") + self.log_area.append(f"Starting sync for playlist: {playlist.name}") # Update refresh button state since we now have an active sync self.update_refresh_button_state() @@ -3378,7 +3378,7 @@ class SyncPage(QWidget): # Log start if hasattr(self, 'log_area'): - self.log_area.append(f"🔄 Starting sequential sync for playlist: {playlist.name}") + self.log_area.append(f"Starting sequential sync for playlist: {playlist.name}") # Show toast notification for sequential sync start if hasattr(self, 'toast_manager') and self.toast_manager: @@ -3519,7 +3519,7 @@ class SyncPage(QWidget): # Log completion if hasattr(self, 'log_area'): success_rate = result.success_rate - msg = f"✅ Sequential sync complete: {result.synced_tracks}/{result.total_tracks} tracks synced ({success_rate:.1f}%)" + msg = f"Sequential sync complete: {result.synced_tracks}/{result.total_tracks} tracks synced ({success_rate:.1f}%)" if result.failed_tracks > 0: msg += f", {result.failed_tracks} failed" self.log_area.append(msg) @@ -3558,7 +3558,7 @@ class SyncPage(QWidget): # Log error if hasattr(self, 'log_area'): - self.log_area.append(f"❌ Sequential sync failed: {error_msg}") + self.log_area.append(f"Sequential sync failed: {error_msg}") # Show toast notification for sequential sync error if hasattr(self, 'toast_manager') and self.toast_manager: @@ -3599,38 +3599,38 @@ class SyncPage(QWidget): # Log cancellation if hasattr(self, 'log_area'): - self.log_area.append(f"🚫 Sync cancelled for playlist") + self.log_area.append(f"Sync cancelled for playlist") return True return False def on_sync_progress(self, playlist_id, progress): """Handle sync progress updates""" - print(f"🚀 PARENT PAGE on_sync_progress called! playlist_id={playlist_id}") - print(f"🚀 Progress: total={progress.total_tracks}, matched={progress.matched_tracks}, failed={progress.failed_tracks}") + print(f"PARENT PAGE on_sync_progress called! playlist_id={playlist_id}") + print(f"Progress: total={progress.total_tracks}, matched={progress.matched_tracks}, failed={progress.failed_tracks}") # Update playlist item status (for Spotify playlists) playlist_item = self.find_playlist_item_widget(playlist_id) if playlist_item: - print(f"🚀 Found playlist item widget, updating status") + print(f"Found playlist item widget, updating status") playlist_item.update_sync_status( progress.total_tracks, progress.matched_tracks, progress.failed_tracks ) else: - print(f"🚀 No playlist item widget found for playlist_id: {playlist_id}") + print(f"No playlist item widget found for playlist_id: {playlist_id}") # Update YouTube card progress (for YouTube playlists) # Find the YouTube card by matching playlist IDs youtube_card_updated = False - print(f"🎬 Searching for YouTube card with playlist_id: {playlist_id}") + print(f"Searching for YouTube card with playlist_id: {playlist_id}") for url, state in self.youtube_playlist_states.items(): playlist_data = state.get('playlist_data') if playlist_data and hasattr(playlist_data, 'id'): - print(f"🎬 Checking YouTube card: URL={url}, stored playlist_id={playlist_data.id}") + print(f"Checking YouTube card: URL={url}, stored playlist_id={playlist_data.id}") if playlist_data.id == playlist_id: - print(f"🎬 ✅ Found matching YouTube card for playlist_id: {playlist_id}, updating progress") + print(f"Found matching YouTube card for playlist_id: {playlist_id}, updating progress") self.update_youtube_card_progress( url, total=progress.total_tracks, @@ -3640,23 +3640,23 @@ class SyncPage(QWidget): youtube_card_updated = True break else: - print(f"🎬 ❌ Playlist ID mismatch: {playlist_data.id} != {playlist_id}") + print(f"Playlist ID mismatch: {playlist_data.id} != {playlist_id}") else: - print(f"🎬 YouTube card state missing playlist_data or id: URL={url}") + print(f"YouTube card state missing playlist_data or id: URL={url}") if not youtube_card_updated: - print(f"🎬 ❌ No matching YouTube card found for playlist_id: {playlist_id}") + print(f"No matching YouTube card found for playlist_id: {playlist_id}") # Update Tidal card progress (for Tidal playlists) # Find the Tidal card by matching playlist IDs tidal_card_updated = False - print(f"🎵 Searching for Tidal card with playlist_id: {playlist_id}") + print(f"Searching for Tidal card with playlist_id: {playlist_id}") for tidal_playlist_id, state in self.tidal_playlist_states.items(): playlist_data = state.get('playlist_data') if playlist_data and hasattr(playlist_data, 'id'): - print(f"🎵 Checking Tidal card: tidal_playlist_id={tidal_playlist_id}, stored playlist_id={playlist_data.id}") + print(f"Checking Tidal card: tidal_playlist_id={tidal_playlist_id}, stored playlist_id={playlist_data.id}") if playlist_data.id == playlist_id: - print(f"🎵 ✅ Found matching Tidal card for playlist_id: {playlist_id}, updating progress") + print(f"Found matching Tidal card for playlist_id: {playlist_id}, updating progress") # Update card progress display if tidal_playlist_id in self.tidal_cards: card = self.tidal_cards[tidal_playlist_id] @@ -3668,18 +3668,18 @@ class SyncPage(QWidget): tidal_card_updated = True break else: - print(f"🎵 ❌ Playlist ID mismatch: {playlist_data.id} != {playlist_id}") + print(f"Playlist ID mismatch: {playlist_data.id} != {playlist_id}") else: - print(f"🎵 Tidal card state missing playlist_data or id: tidal_playlist_id={tidal_playlist_id}") + print(f"Tidal card state missing playlist_data or id: tidal_playlist_id={tidal_playlist_id}") if not tidal_card_updated: - print(f"🎵 ❌ No matching Tidal card found for playlist_id: {playlist_id}") + print(f"No matching Tidal card found for playlist_id: {playlist_id}") if not playlist_item and not youtube_card_updated and not tidal_card_updated: - print(f"🚀 No playlist widget, YouTube card, OR Tidal card found for playlist_id: {playlist_id}") + print(f"No playlist widget, YouTube card, OR Tidal card found for playlist_id: {playlist_id}") # Update any open modal for this playlist - print(f"🚀 About to call update_open_modals_progress") + print(f"About to call update_open_modals_progress") self.update_open_modals_progress(playlist_id, progress) def on_sync_finished(self, playlist_id, result, snapshot_id): @@ -3708,7 +3708,7 @@ class SyncPage(QWidget): for url, state in self.youtube_playlist_states.items(): playlist_data = state.get('playlist_data') if playlist_data and hasattr(playlist_data, 'id') and playlist_data.id == playlist_id: - print(f"🎬 YouTube sync finished for playlist_id: {playlist_id}, updating card to sync_complete") + print(f"YouTube sync finished for playlist_id: {playlist_id}, updating card to sync_complete") self.update_youtube_card_phase(url, 'sync_complete') self.update_youtube_card_progress( url, @@ -3725,7 +3725,7 @@ class SyncPage(QWidget): for tidal_playlist_id, state in self.tidal_playlist_states.items(): playlist_data = state.get('playlist_data') if playlist_data and hasattr(playlist_data, 'id') and playlist_data.id == playlist_id: - print(f"🎵 Tidal sync finished for playlist_id: {playlist_id}, updating card to sync_complete") + print(f"Tidal sync finished for playlist_id: {playlist_id}, updating card to sync_complete") self.update_tidal_card_phase(tidal_playlist_id, 'sync_complete') # Also update card progress display if tidal_playlist_id in self.tidal_cards: @@ -3747,7 +3747,7 @@ class SyncPage(QWidget): # Emit activity signal for sync completion success_msg = f"Completed: {result.matched_tracks}/{result.total_tracks} tracks" - self.sync_activity.emit("✅", "Sync Complete", f"'{playlist_name}' - {success_msg}", "Now") + self.sync_activity.emit("", "Sync Complete", f"'{playlist_name}' - {success_msg}", "Now") # Show toast notification for sync completion if hasattr(self, 'toast_manager') and self.toast_manager: @@ -3777,7 +3777,7 @@ class SyncPage(QWidget): # Log completion if hasattr(self, 'log_area'): success_rate = result.success_rate - msg = f"✅ Sync complete: {result.synced_tracks}/{result.total_tracks} tracks synced ({success_rate:.1f}%)" + msg = f"Sync complete: {result.synced_tracks}/{result.total_tracks} tracks synced ({success_rate:.1f}%)" if result.failed_tracks > 0: msg += f", {result.failed_tracks} failed" self.log_area.append(msg) @@ -3800,7 +3800,7 @@ class SyncPage(QWidget): # Emit activity signal for sync error playlist_name = playlist_item.name if playlist_item else "Unknown Playlist" - self.sync_activity.emit("❌", "Sync Failed", f"'{playlist_name}' - {error_msg}", "Now") + self.sync_activity.emit("", "Sync Failed", f"'{playlist_name}' - {error_msg}", "Now") # Show toast notification for sync error if hasattr(self, 'toast_manager') and self.toast_manager: @@ -3815,12 +3815,12 @@ class SyncPage(QWidget): # Log error if hasattr(self, 'log_area'): - self.log_area.append(f"❌ Sync failed: {error_msg}") + self.log_area.append(f"Sync failed: {error_msg}") def update_open_modals_progress(self, playlist_id, progress): """Update any open modals for this playlist with sync progress""" - print(f"🔍 Looking for modals to update progress for playlist_id: {playlist_id}") - print(f"🔍 Progress data: total={progress.total_tracks}, matched={progress.matched_tracks}, failed={progress.failed_tracks}") + print(f"Looking for modals to update progress for playlist_id: {playlist_id}") + print(f"Progress data: total={progress.total_tracks}, matched={progress.matched_tracks}, failed={progress.failed_tracks}") # Find all open modal instances for this playlist from PyQt6.QtWidgets import QApplication @@ -3829,21 +3829,21 @@ class SyncPage(QWidget): for widget in QApplication.topLevelWidgets(): widget_name = type(widget).__name__ - print(f"🔍 Checking widget: {widget_name}") + print(f"Checking widget: {widget_name}") # Handle PlaylistDetailsModal if isinstance(widget, PlaylistDetailsModal): if hasattr(widget, 'playlist'): widget_playlist_id = getattr(widget.playlist, 'id', 'NO_ID') is_visible = widget.isVisible() - print(f"🔍 Spotify modal: playlist_id={widget_playlist_id}, visible={is_visible}, target={playlist_id}") + print(f"Spotify modal: playlist_id={widget_playlist_id}, visible={is_visible}, target={playlist_id}") if widget_playlist_id == playlist_id and is_visible: - print(f"📊 Updating Spotify modal progress: {playlist_id}") + print(f"Updating Spotify modal progress: {playlist_id}") spotify_modals_found += 1 widget.on_sync_progress(playlist_id, progress) else: - print(f"🔍 Spotify modal without playlist attribute") + print(f"Spotify modal without playlist attribute") # Handle YouTubeDownloadMissingTracksModal elif isinstance(widget, YouTubeDownloadMissingTracksModal): @@ -3851,18 +3851,18 @@ class SyncPage(QWidget): if hasattr(widget, 'playlist'): widget_playlist_id = getattr(widget.playlist, 'id', 'NO_ID') is_visible = widget.isVisible() - print(f"🔍 YouTube modal #{youtube_modals_found}: playlist_id={widget_playlist_id}, visible={is_visible}, target={playlist_id}") + print(f"YouTube modal #{youtube_modals_found}: playlist_id={widget_playlist_id}, visible={is_visible}, target={playlist_id}") if widget_playlist_id == playlist_id: - print(f"📊 ✅ Found matching YouTube modal for playlist_id: {playlist_id}, calling on_sync_progress") + print(f"Found matching YouTube modal for playlist_id: {playlist_id}, calling on_sync_progress") # Update the YouTube modal's progress display (even if hidden) widget.on_sync_progress(playlist_id, progress) else: - print(f"📊 ❌ YouTube modal playlist_id mismatch: {widget_playlist_id} vs {playlist_id}") + print(f"YouTube modal playlist_id mismatch: {widget_playlist_id} vs {playlist_id}") else: - print(f"🔍 YouTube modal #{youtube_modals_found} without playlist attribute") + print(f"YouTube modal #{youtube_modals_found} without playlist attribute") - print(f"🔍 Summary: Found {spotify_modals_found} Spotify modals, {youtube_modals_found} YouTube modals total") + print(f"Summary: Found {spotify_modals_found} Spotify modals, {youtube_modals_found} YouTube modals total") def update_open_modals_completion(self, playlist_id, result): """Update any open modals for this playlist with sync completion""" @@ -3997,7 +3997,7 @@ class SyncPage(QWidget): """) # Add load button - load_btn = QPushButton("🎵 Load Playlists") + load_btn = QPushButton("Load Playlists") load_btn.setFixedSize(200, 50) load_btn.clicked.connect(self.load_playlists_async) load_btn.setStyleSheet(""" @@ -4091,7 +4091,7 @@ class SyncPage(QWidget): section_title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) section_title.setStyleSheet("color: #ffffff;") - self.refresh_btn = QPushButton("🔄 Refresh") + self.refresh_btn = QPushButton("Refresh") self.refresh_btn.setFixedSize(100, 35) self.refresh_btn.clicked.connect(self.load_playlists_async) self.refresh_btn.setStyleSheet(""" @@ -4223,7 +4223,7 @@ class SyncPage(QWidget): section_title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) section_title.setStyleSheet("color: #ffffff;") - self.refresh_btn = QPushButton("🔄 Refresh") + self.refresh_btn = QPushButton("Refresh") self.refresh_btn.setFixedSize(100, 35) self.refresh_btn.clicked.connect(self.load_playlists_async) self.refresh_btn.setStyleSheet(""" @@ -4294,7 +4294,7 @@ class SyncPage(QWidget): section_title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) section_title.setStyleSheet("color: #ffffff;") - self.tidal_refresh_btn = QPushButton("🔄 Refresh") + self.tidal_refresh_btn = QPushButton("Refresh") self.tidal_refresh_btn.setFixedSize(100, 35) self.tidal_refresh_btn.clicked.connect(self.load_tidal_playlists_async) self.tidal_refresh_btn.setStyleSheet(""" @@ -4478,14 +4478,14 @@ class SyncPage(QWidget): def show_youtube_download_status(self, playlist_name, track_count, playlist_id=None): """Show download status widget in YouTube tab - styled like PlaylistItem""" - print(f"📋 show_youtube_download_status called with playlist_id: {playlist_id}") + print(f"show_youtube_download_status called with playlist_id: {playlist_id}") if playlist_id is None: playlist_id = f"youtube_{hash(playlist_name)}" # If a status widget for this playlist already exists, do nothing. if playlist_id in self.youtube_status_widgets: - print(f"📋 Status widget for {playlist_id} already exists. No action taken.") + print(f"Status widget for {playlist_id} already exists. No action taken.") return # --- THE FIX --- @@ -4515,7 +4515,7 @@ class SyncPage(QWidget): layout.setSpacing(15) # Status icon (instead of checkbox) - status_icon = QLabel("🎵") + status_icon = QLabel("") status_icon.setFont(QFont("Arial", 18)) status_icon.setFixedSize(22, 22) status_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -4577,17 +4577,17 @@ class SyncPage(QWidget): def open_youtube_download_modal(self, playlist_id): """Open the YouTube download modal when View Progress button is clicked""" - print(f"🔍 Attempting to open modal for playlist_id: {playlist_id}") - print(f"🔍 Available modals: {list(self.active_youtube_download_modals.keys())}") + print(f"Attempting to open modal for playlist_id: {playlist_id}") + print(f"Available modals: {list(self.active_youtube_download_modals.keys())}") if playlist_id in self.active_youtube_download_modals: modal = self.active_youtube_download_modals[playlist_id] - print(f"✅ Found modal, opening...") + print(f"Found modal, opening...") modal.show() modal.raise_() modal.activateWindow() else: - print(f"❌ No modal found for playlist_id: {playlist_id}") + print(f"No modal found for playlist_id: {playlist_id}") def create_right_sidebar(self): section = QWidget() @@ -4746,7 +4746,7 @@ class SyncPage(QWidget): self.update_selection_ui() # Add loading placeholder - loading_label = QLabel("🔄 Loading playlists...") + loading_label = QLabel("Loading playlists...") loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_label.setStyleSheet(""" QLabel { @@ -4761,7 +4761,7 @@ class SyncPage(QWidget): self.playlist_layout.insertWidget(0, loading_label) # Show loading state - self.refresh_btn.setText("🔄 Loading...") + self.refresh_btn.setText("Loading...") self.refresh_btn.setEnabled(False) self.log_area.append("Starting playlist loading...") @@ -4860,9 +4860,9 @@ class SyncPage(QWidget): item.widget().deleteLater() break - self.refresh_btn.setText("🔄 Refresh") + self.refresh_btn.setText("Refresh") self.refresh_btn.setEnabled(True) - self.log_area.append(f"✓ Loaded {count} Spotify playlists successfully") + self.log_area.append(f"Loaded {count} Spotify playlists successfully") # Start background preloading of tracks for smaller playlists self.start_background_preloading() @@ -4910,9 +4910,9 @@ class SyncPage(QWidget): item.widget().deleteLater() break - self.refresh_btn.setText("🔄 Refresh") + self.refresh_btn.setText("Refresh") self.refresh_btn.setEnabled(True) - self.log_area.append(f"✗ Failed to load playlists: {error_msg}") + self.log_area.append(f"Failed to load playlists: {error_msg}") QMessageBox.critical(self, "Error", f"Failed to load playlists: {error_msg}") def update_progress(self, message): @@ -4931,7 +4931,7 @@ class SyncPage(QWidget): self.clear_tidal_playlists() # Add loading placeholder - loading_label = QLabel("🔄 Loading Tidal playlists...") + loading_label = QLabel("Loading Tidal playlists...") loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_label.setStyleSheet(""" QLabel { @@ -4946,7 +4946,7 @@ class SyncPage(QWidget): self.tidal_playlist_layout.insertWidget(0, loading_label) # Show loading state - self.tidal_refresh_btn.setText("🔄 Loading...") + self.tidal_refresh_btn.setText("Loading...") self.tidal_refresh_btn.setEnabled(False) self.log_area.append("Starting Tidal playlist loading...") @@ -4995,9 +4995,9 @@ class SyncPage(QWidget): item.widget().deleteLater() break - self.tidal_refresh_btn.setText("🔄 Refresh") + self.tidal_refresh_btn.setText("Refresh") self.tidal_refresh_btn.setEnabled(True) - self.log_area.append(f"✓ Loaded {count} Tidal playlists successfully") + self.log_area.append(f"Loaded {count} Tidal playlists successfully") def on_tidal_loading_failed(self, error_msg): """Handle Tidal playlist loading failure""" @@ -5009,21 +5009,21 @@ class SyncPage(QWidget): item.widget().deleteLater() break - self.tidal_refresh_btn.setText("🔄 Refresh") + self.tidal_refresh_btn.setText("Refresh") self.tidal_refresh_btn.setEnabled(True) - self.log_area.append(f"✗ Failed to load Tidal playlists: {error_msg}") + self.log_area.append(f"Failed to load Tidal playlists: {error_msg}") QMessageBox.critical(self, "Error", f"Failed to load Tidal playlists: {error_msg}") def cleanup_all_tidal_operations(self): """Complete cleanup of all Tidal operations - stop workers, close modals, cancel syncs""" - print("🧹 Starting complete Tidal cleanup for refresh...") + print("Starting complete Tidal cleanup for refresh...") # Close and cleanup all active Tidal modals for playlist_id, state in list(self.tidal_playlist_states.items()): # Close discovery modals discovery_modal = state.get('discovery_modal') if discovery_modal: - print(f"🔍 Closing Tidal discovery modal for playlist_id: {playlist_id}") + print(f"Closing Tidal discovery modal for playlist_id: {playlist_id}") try: # Cancel any active workers in the discovery modal if hasattr(discovery_modal, 'spotify_worker') and discovery_modal.spotify_worker: @@ -5038,12 +5038,12 @@ class SyncPage(QWidget): # Force close the modal discovery_modal.close() except Exception as e: - print(f"⚠️ Error closing discovery modal: {e}") + print(f"Error closing discovery modal: {e}") # Close download modals download_modal = state.get('download_modal') if download_modal: - print(f"📥 Closing Tidal download modal for playlist_id: {playlist_id}") + print(f"Closing Tidal download modal for playlist_id: {playlist_id}") try: # Cancel all operations (downloads, searches, etc.) download_modal.cancel_operations() @@ -5063,7 +5063,7 @@ class SyncPage(QWidget): # Force close the modal download_modal.close() except Exception as e: - print(f"⚠️ Error closing download modal: {e}") + print(f"Error closing download modal: {e}") # Cancel any active sync workers for Tidal playlists tidal_playlist_ids = set() @@ -5075,24 +5075,24 @@ class SyncPage(QWidget): # Cancel sync workers for playlist_id in tidal_playlist_ids: if playlist_id in self.active_sync_workers: - print(f"🔄 Cancelling sync worker for Tidal playlist_id: {playlist_id}") + print(f"Cancelling sync worker for Tidal playlist_id: {playlist_id}") try: worker = self.active_sync_workers[playlist_id] if hasattr(worker, 'cancel'): worker.cancel() del self.active_sync_workers[playlist_id] except Exception as e: - print(f"⚠️ Error cancelling sync worker: {e}") + print(f"Error cancelling sync worker: {e}") # Remove from active download modals (shared with YouTube) for playlist_id in list(self.active_youtube_download_modals.keys()): modal = self.active_youtube_download_modals[playlist_id] if hasattr(modal, 'is_tidal_playlist') and modal.is_tidal_playlist: - print(f"📥 Removing Tidal download modal from active list: {playlist_id}") + print(f"Removing Tidal download modal from active list: {playlist_id}") try: del self.active_youtube_download_modals[playlist_id] except Exception as e: - print(f"⚠️ Error removing download modal: {e}") + print(f"Error removing download modal: {e}") # Force cleanup of any remaining thread pool operations # This ensures any lingering search workers are properly terminated @@ -5100,18 +5100,18 @@ class SyncPage(QWidget): thread_pool = QThreadPool.globalInstance() # Note: QThreadPool doesn't have a direct "cancel all" method, # but setting cancel_requested=True in the modals should make workers exit gracefully - print(f"🔧 Thread pool active count: {thread_pool.activeThreadCount()}") + print(f"Thread pool active count: {thread_pool.activeThreadCount()}") if thread_pool.activeThreadCount() > 0: - print("⏳ Waiting briefly for thread pool workers to finish gracefully...") + print("Waiting briefly for thread pool workers to finish gracefully...") # Give workers a moment to see the cancel_requested flag and exit from PyQt6.QtCore import QTimer, QEventLoop loop = QEventLoop() QTimer.singleShot(500, loop.quit) # 500ms timeout loop.exec() except Exception as e: - print(f"⚠️ Error during thread pool cleanup: {e}") + print(f"Error during thread pool cleanup: {e}") - print("✅ Tidal cleanup complete") + print("Tidal cleanup complete") def clear_tidal_playlists(self): """Clear all Tidal playlist items from UI""" @@ -5132,11 +5132,11 @@ class SyncPage(QWidget): def on_tidal_card_clicked(self, playlist_id: str, phase: str): """Handle Tidal playlist card clicks - route to appropriate modal (matches YouTube workflow)""" - print(f"🎵 Tidal card clicked: playlist_id={playlist_id}, Phase={phase}") + print(f"Tidal card clicked: playlist_id={playlist_id}, Phase={phase}") state = self.get_tidal_playlist_state(playlist_id) if not state: - print(f"⚠️ No state found for playlist_id: {playlist_id}") + print(f"No state found for playlist_id: {playlist_id}") return # Route to appropriate modal based on current phase @@ -5153,17 +5153,17 @@ class SyncPage(QWidget): download_modal = state.get('download_modal') if download_modal and not download_modal.isVisible(): # Modal exists but is hidden - show it - print(f"📍 Reopening hidden Tidal download modal for playlist_id: {playlist_id}") + print(f"Reopening hidden Tidal download modal for playlist_id: {playlist_id}") download_modal.show() download_modal.activateWindow() download_modal.raise_() elif download_modal and download_modal.isVisible(): # Modal is already visible - bring to front - print(f"📍 Bringing visible Tidal download modal to front for playlist_id: {playlist_id}") + print(f"Bringing visible Tidal download modal to front for playlist_id: {playlist_id}") download_modal.activateWindow() download_modal.raise_() else: - print(f"📍 No download modal found, routing to discovery modal instead") + print(f"No download modal found, routing to discovery modal instead") self.open_or_create_tidal_discovery_modal(playlist_id, state) elif phase == 'syncing': # Show sync progress - route to discovery modal @@ -5179,7 +5179,7 @@ class SyncPage(QWidget): # Check if modal exists but is hidden - reopen it if state.get('discovery_modal') and not state['discovery_modal'].isVisible(): - print(f"🔍 Reopening existing hidden discovery modal for playlist_id: {playlist_id}") + print(f"Reopening existing hidden discovery modal for playlist_id: {playlist_id}") state['discovery_modal'].show() state['discovery_modal'].activateWindow() state['discovery_modal'].raise_() @@ -5187,7 +5187,7 @@ class SyncPage(QWidget): # Check if we have playlist data already (discovery_complete state) if state.get('playlist_data') and state['phase'] == 'discovery_complete': - print(f"🔍 Opening existing discovery modal with data for playlist_id: {playlist_id}") + print(f"Opening existing discovery modal with data for playlist_id: {playlist_id}") # Create a new modal with the existing data dummy_playlist_item = type('DummyPlaylistItem', (), { @@ -5221,7 +5221,7 @@ class SyncPage(QWidget): return # Need to discover playlist data first - print(f"🔍 Need to discover playlist data for playlist_id: {playlist_id}") + print(f"Need to discover playlist data for playlist_id: {playlist_id}") # Get playlist data if not cached playlist_data = state.get('playlist_data') @@ -5234,7 +5234,7 @@ class SyncPage(QWidget): break if not playlist_data: - print(f"❌ Could not find playlist data for playlist_id: {playlist_id}") + print(f"Could not find playlist data for playlist_id: {playlist_id}") return # Get full playlist data with tracks if not already loaded @@ -5244,11 +5244,11 @@ class SyncPage(QWidget): if full_playlist and full_playlist.tracks: playlist_data = full_playlist else: - print(f"❌ Failed to load tracks for Tidal playlist {playlist_id}") + print(f"Failed to load tracks for Tidal playlist {playlist_id}") QMessageBox.warning(self, "Error", f"Failed to load tracks for playlist") return except Exception as e: - print(f"❌ Error loading Tidal playlist tracks: {e}") + print(f"Error loading Tidal playlist tracks: {e}") QMessageBox.warning(self, "Error", f"Error loading playlist tracks: {str(e)}") return @@ -5283,13 +5283,13 @@ class SyncPage(QWidget): modal.activateWindow() modal.raise_() - print(f"✅ Opened discovery modal for Tidal playlist '{playlist_data.name}' with {len(playlist_data.tracks)} tracks") + print(f"Opened discovery modal for Tidal playlist '{playlist_data.name}' with {len(playlist_data.tracks)} tracks") def open_or_create_tidal_download_modal(self, playlist_id: str, state: dict): """Open or create the download modal for a Tidal playlist""" playlist_data = state.get('playlist_data') if not playlist_data: - print(f"⚠️ No playlist data found for download modal") + print(f"No playlist data found for download modal") return # Check if download modal already exists @@ -5307,12 +5307,12 @@ class SyncPage(QWidget): return # Need to create new download modal - route back to discovery modal for now - print(f"📍 No download modal found, routing to discovery modal") + print(f"No download modal found, routing to discovery modal") self.open_or_create_tidal_discovery_modal(playlist_id, state) def on_tidal_playlist_clicked(self, playlist): """Legacy method for old TidalPlaylistItem - route to card system""" - print(f"🎵 Legacy Tidal playlist clicked: {playlist.name} - routing to card system") + print(f"Legacy Tidal playlist clicked: {playlist.name} - routing to card system") # For now, create a temporary discovery modal (this should be replaced when cards are fully integrated) # Get full playlist data with tracks if not already loaded @@ -5322,11 +5322,11 @@ class SyncPage(QWidget): if full_playlist and full_playlist.tracks: playlist = full_playlist else: - print(f"❌ Failed to load tracks for Tidal playlist {playlist.name}") + print(f"Failed to load tracks for Tidal playlist {playlist.name}") QMessageBox.warning(self, "Error", f"Failed to load tracks for playlist '{playlist.name}'") return except Exception as e: - print(f"❌ Error loading Tidal playlist tracks: {e}") + print(f"Error loading Tidal playlist tracks: {e}") QMessageBox.warning(self, "Error", f"Error loading playlist tracks: {str(e)}") return @@ -5357,17 +5357,17 @@ class SyncPage(QWidget): modal.activateWindow() modal.raise_() - print(f"✅ Opened discovery modal for Tidal playlist '{playlist.name}' with {len(playlist.tracks)} tracks") + print(f"Opened discovery modal for Tidal playlist '{playlist.name}' with {len(playlist.tracks)} tracks") def disable_refresh_button(self, operation_name="Operation"): """Disable refresh button during sync/download operations""" self.refresh_btn.setEnabled(False) - self.refresh_btn.setText(f"🔄 {operation_name}...") + self.refresh_btn.setText(f"{operation_name}...") def enable_refresh_button(self): """Re-enable refresh button after operations complete""" self.refresh_btn.setEnabled(True) - self.refresh_btn.setText("🔄 Refresh") + self.refresh_btn.setText("Refresh") def has_active_operations(self): """Check if any sync or download operations are currently active""" @@ -5472,7 +5472,7 @@ class SyncPage(QWidget): else: # Card exists but no data yet - this means parsing was cancelled/failed # Reset the card state and continue with new parsing - print(f"🔄 Resetting existing card state for URL: {url}") + print(f"Resetting existing card state for URL: {url}") self.reset_youtube_playlist_state(url) # Check if this URL is already being processed (legacy check) @@ -5533,7 +5533,7 @@ class SyncPage(QWidget): })() # Open the modal in loading state - print("🚀 Opening YouTubeDownloadMissingTracksModal in loading state...") + print("Opening YouTubeDownloadMissingTracksModal in loading state...") self.current_youtube_modal = YouTubeDownloadMissingTracksModal( empty_playlist, dummy_playlist_item, @@ -5558,8 +5558,8 @@ class SyncPage(QWidget): def on_youtube_parsing_finished(self, playlist): """Handle successful YouTube playlist parsing""" try: - print(f"✅ Successfully parsed YouTube playlist: {playlist.name}") - print(f"🔍 Playlist ID: {playlist.id}") + print(f"Successfully parsed YouTube playlist: {playlist.name}") + print(f"Playlist ID: {playlist.id}") # Reset button state self.parse_btn.setEnabled(True) @@ -5579,9 +5579,9 @@ class SyncPage(QWidget): self.youtube_playlist_states[url]['discovery_modal'] = self.current_youtube_modal # Update the existing modal with the parsed playlist data - print(f"🔍 Has current_youtube_modal: {hasattr(self, 'current_youtube_modal') and self.current_youtube_modal is not None}") + print(f"Has current_youtube_modal: {hasattr(self, 'current_youtube_modal') and self.current_youtube_modal is not None}") if hasattr(self, 'current_youtube_modal') and self.current_youtube_modal: - print(f"🔍 Calling populate_with_playlist_data...") + print(f"Calling populate_with_playlist_data...") self.current_youtube_modal.populate_with_playlist_data(playlist) else: # Fallback: create new modal if loading modal wasn't created @@ -5611,12 +5611,12 @@ class SyncPage(QWidget): self.youtube_url_input.clear() except Exception as e: - print(f"❌ Error handling YouTube parsing result: {e}") + print(f"Error handling YouTube parsing result: {e}") self.on_youtube_parsing_error(str(e)) def on_youtube_parsing_error(self, error_message): """Handle YouTube playlist parsing error""" - print(f"❌ YouTube parsing error: {error_message}") + print(f"YouTube parsing error: {error_message}") # Update card state on error (remove the card since parsing failed) if hasattr(self, 'current_youtube_url'): @@ -5625,7 +5625,7 @@ class SyncPage(QWidget): # Clean up URL tracking on error if hasattr(self, 'current_youtube_url') and self.current_youtube_url in self.active_youtube_processes: - print(f"🧹 Cleaning up URL tracking on error for: {self.current_youtube_url}") + print(f"Cleaning up URL tracking on error for: {self.current_youtube_url}") del self.active_youtube_processes[self.current_youtube_url] # Reset button state @@ -5641,7 +5641,7 @@ class SyncPage(QWidget): if hasattr(self, 'toast_manager') and self.toast_manager: self.toast_manager.show_toast(message, ToastType.ERROR) else: - print(f"⚠️ YouTube Error: {message}") + print(f"YouTube Error: {message}") # Fallback to a simple message box msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Icon.Warning) @@ -5734,7 +5734,7 @@ class SyncPage(QWidget): if status_widget: status_widget.setParent(None) status_widget.deleteLater() - print(f"🧹 Cleaned up status widget for phase change to: {phase}") + print(f"Cleaned up status widget for phase change to: {phase}") # Ensure card is always visible - it manages its own appearance card.show() @@ -5824,7 +5824,7 @@ class SyncPage(QWidget): if status_widget: status_widget.setParent(None) status_widget.deleteLater() - print(f"🧹 Cleaned up status widget for Tidal phase change to: {phase}") + print(f"Cleaned up status widget for Tidal phase change to: {phase}") def update_tidal_card_playlist_info(self, playlist_id: str, name: str, track_count: int): """Update Tidal card playlist information""" @@ -5881,11 +5881,11 @@ class SyncPage(QWidget): def on_youtube_card_clicked(self, url: str, phase: str): """Handle YouTube playlist card clicks - route to appropriate modal""" - print(f"🎬 YouTube card clicked: URL={url}, Phase={phase}") + print(f"YouTube card clicked: URL={url}, Phase={phase}") state = self.get_youtube_playlist_state(url) if not state: - print(f"⚠️ No state found for URL: {url}") + print(f"No state found for URL: {url}") return # Route to appropriate modal based on current phase @@ -5899,7 +5899,7 @@ class SyncPage(QWidget): playlist_data.id in self.active_youtube_download_modals): self.open_or_create_download_modal(url, state) else: - print(f"📍 Download modal not found, routing to discovery modal instead") + print(f"Download modal not found, routing to discovery modal instead") self.open_or_create_discovery_modal(url, state) elif phase == 'syncing': # Show sync progress - could be same as discovery modal or separate @@ -5915,7 +5915,7 @@ class SyncPage(QWidget): # Check if modal exists but is hidden - reopen it if state.get('discovery_modal') and not state['discovery_modal'].isVisible(): - print(f"🔍 Reopening existing hidden discovery modal for URL: {url}") + print(f"Reopening existing hidden discovery modal for URL: {url}") state['discovery_modal'].show() state['discovery_modal'].activateWindow() state['discovery_modal'].raise_() @@ -5923,7 +5923,7 @@ class SyncPage(QWidget): # Check if we have playlist data already (discovery_complete state) if state.get('playlist_data') and state['phase'] == 'discovery_complete': - print(f"🔍 Opening existing discovery modal with data for URL: {url}") + print(f"Opening existing discovery modal with data for URL: {url}") # Create a new modal with the existing data dummy_playlist_item = type('DummyPlaylistItem', (), { @@ -5952,7 +5952,7 @@ class SyncPage(QWidget): else: # No existing data - start new discovery process - print(f"🔍 Starting new discovery for URL: {url}") + print(f"Starting new discovery for URL: {url}") # Store URL in input field self.youtube_url_input.setText(url) @@ -5983,7 +5983,7 @@ class SyncPage(QWidget): """Open or create the download modal for a YouTube playlist""" playlist_data = state.get('playlist_data') if not playlist_data: - print(f"⚠️ No playlist data available for URL: {url}") + print(f"No playlist data available for URL: {url}") return # Check if modal already exists @@ -5993,7 +5993,7 @@ class SyncPage(QWidget): return # Create new download modal - print(f"📥 Opening download modal for URL: {url}") + print(f"Opening download modal for URL: {url}") # Check existing modal system first if hasattr(playlist_data, 'id') and playlist_data.id in self.active_youtube_download_modals: @@ -6042,19 +6042,19 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): query = youtube_track.name # Debug logging for search queries - print(f"🔍 Spotify search query: '{query}' (track: '{youtube_track.name}', artist: '{youtube_track.artists[0] if youtube_track.artists else 'None'}')") + print(f"Spotify search query: '{query}' (track: '{youtube_track.name}', artist: '{youtube_track.artists[0] if youtube_track.artists else 'None'}')") # Search Spotify - get more results for validation spotify_results = self.spotify_client.search_tracks(query, limit=10) # Debug logging for search results if spotify_results: - print(f"📊 Found {len(spotify_results)} Spotify results:") + print(f"Found {len(spotify_results)} Spotify results:") for idx, result in enumerate(spotify_results[:3]): # Show first 3 album_name = result.album if isinstance(result.album, str) else getattr(result.album, 'name', 'Unknown') print(f" {idx+1}. '{result.name}' by '{result.artists[0] if result.artists else 'Unknown'}' from '{album_name}'") else: - print(f"❌ No Spotify results for query: '{query}'") + print(f"No Spotify results for query: '{query}'") if spotify_results: # Use matching engine to find the best validated match @@ -6066,21 +6066,21 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): # Try swapping artist and track name (sometimes YouTube data is swapped) best_track = self.retry_with_swapped_fields(youtube_track) if best_track: - print(f"🔄 Found match after swapping artist/track for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") + print(f"Found match after swapping artist/track for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") self.signals.track_discovered.emit(i, best_track, "found") successful_discoveries += 1 else: # Third resort: try with uncleaned original data best_track = self.retry_with_uncleaned_data(youtube_track) if best_track: - print(f"🔍 Found match with uncleaned data for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") + print(f"Found match with uncleaned data for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") self.signals.track_discovered.emit(i, best_track, "found") successful_discoveries += 1 else: # Final resort: try with raw title + raw artist combined best_track = self.retry_with_raw_title_and_artist(youtube_track) if best_track: - print(f"🎯 Found match with title+artist fallback for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") + print(f"Found match with title+artist fallback for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") self.signals.track_discovered.emit(i, best_track, "found") successful_discoveries += 1 else: @@ -6090,28 +6090,28 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): # No Spotify search results found - try swapping before giving up best_track = self.retry_with_swapped_fields(youtube_track) if best_track: - print(f"🔄 Found match after swapping artist/track for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") + print(f"Found match after swapping artist/track for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") self.signals.track_discovered.emit(i, best_track, "found") successful_discoveries += 1 else: # Third resort: try with uncleaned original data best_track = self.retry_with_uncleaned_data(youtube_track) if best_track: - print(f"🔍 Found match with uncleaned data for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") + print(f"Found match with uncleaned data for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") self.signals.track_discovered.emit(i, best_track, "found") successful_discoveries += 1 else: # Final resort: try with raw title + raw artist combined best_track = self.retry_with_raw_title_and_artist(youtube_track) if best_track: - print(f"🎯 Found match with title+artist fallback for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") + print(f"Found match with title+artist fallback for: '{youtube_track.name}' by '{youtube_track.artists[0] if youtube_track.artists else 'Unknown'}'") self.signals.track_discovered.emit(i, best_track, "found") successful_discoveries += 1 else: self.signals.track_discovered.emit(i, None, "not_found") except Exception as e: - print(f"❌ Error searching Spotify for track {i}: {e}") + print(f"Error searching Spotify for track {i}: {e}") self.signals.track_discovered.emit(i, None, f"error: {str(e)}") # Update progress @@ -6152,7 +6152,7 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): # Debug logging for track cleaning if cleaned_spotify_track.name != spotify_track.name: - print(f"🧹 Cleaned Spotify track: '{spotify_track.name}' -> '{cleaned_spotify_track.name}'") + print(f"Cleaned Spotify track: '{spotify_track.name}' -> '{cleaned_spotify_track.name}'") # Use your matching engine to calculate confidence confidence, match_type = self.matching_engine.calculate_match_confidence( @@ -6170,17 +6170,17 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): best_match_type = match_type except Exception as e: - print(f"⚠️ Error calculating match confidence: {e}") + print(f"Error calculating match confidence: {e}") continue # Apply your matching engine's confidence threshold (0.8 for high confidence) confidence_threshold = 0.75 # Slightly lower for YouTube discovery if best_confidence >= confidence_threshold: - print(f"✅ Validated match: '{best_match.name}' by '{best_match.artists[0]}' (confidence: {best_confidence:.3f}, type: {best_match_type})") + print(f"Validated match: '{best_match.name}' by '{best_match.artists[0]}' (confidence: {best_confidence:.3f}, type: {best_match_type})") return best_match else: - print(f"❌ No high-confidence match found. Best was {best_confidence:.3f} < {confidence_threshold}") + print(f"No high-confidence match found. Best was {best_confidence:.3f} < {confidence_threshold}") if best_match: print(f" Best candidate was: '{best_match.name}' by '{best_match.artists[0] if best_match.artists else 'Unknown'}'") return None @@ -6253,7 +6253,7 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): else: score += 30 # No album info gets low score except Exception as e: - print(f"⚠️ Error accessing album type: {e}") + print(f"Error accessing album type: {e}") score += 30 # Error case gets low score # 2. Prefer tracks with more total tracks in album (indicates full album) @@ -6270,7 +6270,7 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): score += 10 # Multi-track release # Singles (1 track) get no bonus except Exception as e: - print(f"⚠️ Error accessing total_tracks: {e}") + print(f"Error accessing total_tracks: {e}") pass # 3. Consider popularity as tiebreaker @@ -6297,7 +6297,7 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): if len(self.youtube_tracks) <= 5 or len(scored_tracks) > 1: try: album_name = best_track.album if isinstance(best_track.album, str) else getattr(best_track.album, 'name', 'Unknown Album') - print(f"🎯 Chose: '{best_track.name}' from '{album_name}' (score: {scored_tracks[0][0]:.1f})") + print(f"Chose: '{best_track.name}' from '{album_name}' (score: {scored_tracks[0][0]:.1f})") if len(scored_tracks) > 1: alt_album = scored_tracks[1][1].album if isinstance(scored_tracks[1][1].album, str) else getattr(scored_tracks[1][1].album, 'name', 'Unknown Album') print(f" vs. '{scored_tracks[1][1].name}' from '{alt_album}' (score: {scored_tracks[1][0]:.1f})") @@ -6358,7 +6358,7 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): swapped_query = f"{swapped_artist_clean} {swapped_track_clean}" - print(f"🔄 Retrying with swapped fields: '{swapped_query}' (was '{youtube_track.artists[0]} {youtube_track.name}')") + print(f"Retrying with swapped fields: '{swapped_query}' (was '{youtube_track.artists[0]} {youtube_track.name}')") # Search Spotify with swapped query spotify_results = self.spotify_client.search_tracks(swapped_query, limit=10) @@ -6379,14 +6379,14 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): return None except Exception as e: - print(f"❌ Error in retry with swapped fields: {e}") + print(f"Error in retry with swapped fields: {e}") return None def retry_with_uncleaned_data(self, youtube_track): """Last resort: retry search with original uncleaned YouTube data""" # Check if we have raw uncleaned data if not hasattr(youtube_track, 'raw_title') or not hasattr(youtube_track, 'raw_uploader'): - print("🔍 No raw data available for uncleaned fallback search") + print("No raw data available for uncleaned fallback search") return None try: @@ -6397,13 +6397,13 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): # Create query with minimal cleaning - just basic text normalization uncleaned_query = f"{raw_uploader} {raw_title}".strip() - print(f"🔍 Last resort: Trying uncleaned data: '{uncleaned_query}' (was '{youtube_track.artists[0]} {youtube_track.name}')") + print(f"Last resort: Trying uncleaned data: '{uncleaned_query}' (was '{youtube_track.artists[0]} {youtube_track.name}')") # Search Spotify with uncleaned query spotify_results = self.spotify_client.search_tracks(uncleaned_query, limit=10) if spotify_results: - print(f"📊 Found {len(spotify_results)} results with uncleaned data") + print(f"Found {len(spotify_results)} results with uncleaned data") # Create an uncleaned YouTube track for comparison uncleaned_youtube_track = type('Track', (), { @@ -6430,29 +6430,29 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): best_confidence = confidence best_match = spotify_track except Exception as e: - print(f"⚠️ Error calculating confidence for uncleaned fallback: {e}") + print(f"Error calculating confidence for uncleaned fallback: {e}") continue # Use lower confidence threshold for uncleaned fallback (0.6 instead of 0.75) confidence_threshold = 0.6 if best_confidence >= confidence_threshold: - print(f"✅ Uncleaned fallback match: '{best_match.name}' by '{best_match.artists[0]}' (confidence: {best_confidence:.3f})") + print(f"Uncleaned fallback match: '{best_match.name}' by '{best_match.artists[0]}' (confidence: {best_confidence:.3f})") return best_match else: - print(f"❌ Uncleaned fallback: Best confidence {best_confidence:.3f} < {confidence_threshold}") + print(f"Uncleaned fallback: Best confidence {best_confidence:.3f} < {confidence_threshold}") return None except Exception as e: - print(f"❌ Error in retry with uncleaned data: {e}") + print(f"Error in retry with uncleaned data: {e}") return None def retry_with_raw_title_and_artist(self, youtube_track): """Final fallback: search with raw title + raw artist as combined query""" # Check if we have raw uncleaned data if not hasattr(youtube_track, 'raw_title') or not hasattr(youtube_track, 'raw_uploader'): - print("🔍 No raw data available for title+artist fallback search") + print("No raw data available for title+artist fallback search") return None try: @@ -6464,13 +6464,13 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): # This uses "title artist" order which sometimes works better combined_query = f"{raw_title} {raw_uploader}".strip() - print(f"🔍 Final fallback: Trying raw title+artist: '{combined_query}'") + print(f"Final fallback: Trying raw title+artist: '{combined_query}'") # Search Spotify with the combined query spotify_results = self.spotify_client.search_tracks(combined_query, limit=10) if spotify_results: - print(f"📊 Found {len(spotify_results)} results with title+artist search") + print(f"Found {len(spotify_results)} results with title+artist search") # Create a track object for matching with raw data in title+artist order combined_youtube_track = type('Track', (), { @@ -6495,24 +6495,24 @@ class OptimizedSpotifyDiscoveryWorker(QRunnable): best_confidence = confidence best_match = spotify_track except Exception as e: - print(f"⚠️ Error calculating confidence for title+artist fallback: {e}") + print(f"Error calculating confidence for title+artist fallback: {e}") continue # Use very low confidence threshold for this final attempt (0.5 instead of 0.6) confidence_threshold = 0.5 if best_confidence >= confidence_threshold: - print(f"✅ Title+artist fallback match: '{best_match.name}' by '{best_match.artists[0]}' (confidence: {best_confidence:.3f})") + print(f"Title+artist fallback match: '{best_match.name}' by '{best_match.artists[0]}' (confidence: {best_confidence:.3f})") return best_match else: - print(f"❌ Title+artist fallback: Best confidence {best_confidence:.3f} < {confidence_threshold}") + print(f"Title+artist fallback: Best confidence {best_confidence:.3f} < {confidence_threshold}") else: - print(f"❌ No results found for title+artist query: '{combined_query}'") + print(f"No results found for title+artist query: '{combined_query}'") return None except Exception as e: - print(f"❌ Error in retry with title+artist: {e}") + print(f"Error in retry with title+artist: {e}") return None class TidalSpotifyDiscoveryWorkerSignals(QObject): @@ -6548,14 +6548,14 @@ class TidalSpotifyDiscoveryWorker(QRunnable): query = tidal_track.name # Debug logging for search queries - print(f"🔍 Spotify search query for Tidal track: '{query}' (track: '{tidal_track.name}', artist: '{tidal_track.artists[0] if tidal_track.artists else 'None'}')") + print(f"Spotify search query for Tidal track: '{query}' (track: '{tidal_track.name}', artist: '{tidal_track.artists[0] if tidal_track.artists else 'None'}')") # Search Spotify - get more results for validation spotify_results = self.spotify_client.search_tracks(query, limit=10) # Progress tracking if spotify_results: - print(f"📊 Found {len(spotify_results)} Spotify results for Tidal track:") + print(f"Found {len(spotify_results)} Spotify results for Tidal track:") for idx, result in enumerate(spotify_results[:3]): # Show first 3 print(f" {idx+1}. '{result.name}' by {', '.join(result.artists)}") @@ -6565,37 +6565,37 @@ class TidalSpotifyDiscoveryWorker(QRunnable): if not best_track: # Try with swapped fields if no match found - print(f"🔄 No direct match found, trying swapped fields for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"No direct match found, trying swapped fields for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") best_track = self.retry_with_swapped_fields(tidal_track) if best_track: - print(f"🔄 Found match after swapping artist/track for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"Found match after swapping artist/track for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") else: # Final fallback: try with cleaned data best_track = self.retry_with_uncleaned_data(tidal_track) if best_track: - print(f"🔍 Found match with uncleaned data for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"Found match with uncleaned data for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") else: # Last resort: try title+artist combo best_track = self.retry_with_raw_title_and_artist(tidal_track) if best_track: - print(f"🎯 Found match with title+artist fallback for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"Found match with title+artist fallback for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") if best_track: successful_discoveries += 1 self.signals.track_discovered.emit(i, best_track, "found") - print(f"✅ Matched Tidal track '{tidal_track.name}' to Spotify track '{best_track.name}' by {', '.join(best_track.artists)}") + print(f"Matched Tidal track '{tidal_track.name}' to Spotify track '{best_track.name}' by {', '.join(best_track.artists)}") else: self.signals.track_discovered.emit(i, None, "not_found") - print(f"❌ No Spotify match found for Tidal track '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"No Spotify match found for Tidal track '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") else: # No search results - try fallback approaches best_track = self.retry_with_swapped_fields(tidal_track) if best_track: - print(f"🔄 Found match after swapping artist/track for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"Found match after swapping artist/track for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") successful_discoveries += 1 self.signals.track_discovered.emit(i, best_track, "found") else: @@ -6603,7 +6603,7 @@ class TidalSpotifyDiscoveryWorker(QRunnable): best_track = self.retry_with_uncleaned_data(tidal_track) if best_track: - print(f"🔍 Found match with uncleaned data for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"Found match with uncleaned data for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") successful_discoveries += 1 self.signals.track_discovered.emit(i, best_track, "found") else: @@ -6611,12 +6611,12 @@ class TidalSpotifyDiscoveryWorker(QRunnable): best_track = self.retry_with_raw_title_and_artist(tidal_track) if best_track: - print(f"🎯 Found match with title+artist fallback for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") + print(f"Found match with title+artist fallback for: '{tidal_track.name}' by '{tidal_track.artists[0] if tidal_track.artists else 'Unknown'}'") successful_discoveries += 1 self.signals.track_discovered.emit(i, best_track, "found") else: self.signals.track_discovered.emit(i, None, "not_found") - print(f"❌ No Spotify match found for Tidal track '{tidal_track.name}'") + print(f"No Spotify match found for Tidal track '{tidal_track.name}'") # Update progress self.signals.progress_updated.emit(i + 1) @@ -6625,11 +6625,11 @@ class TidalSpotifyDiscoveryWorker(QRunnable): time.sleep(0.25) # 250ms delay except Exception as e: - print(f"❌ Error processing Tidal track '{tidal_track.name}': {str(e)}") + print(f"Error processing Tidal track '{tidal_track.name}': {str(e)}") self.signals.track_discovered.emit(i, None, "error") continue - print(f"🎵 Tidal discovery completed: {successful_discoveries} successful discoveries") + print(f"Tidal discovery completed: {successful_discoveries} successful discoveries") self.signals.finished.emit(successful_discoveries) def find_best_validated_match(self, tidal_track, spotify_results): @@ -6665,17 +6665,17 @@ class TidalSpotifyDiscoveryWorker(QRunnable): best_confidence = confidence best_track = spotify_track - print(f"🎯 Tidal->Spotify match confidence: {confidence:.3f} for '{spotify_track.name}' by {', '.join(spotify_track.artists)}") + print(f"Tidal->Spotify match confidence: {confidence:.3f} for '{spotify_track.name}' by {', '.join(spotify_track.artists)}") except Exception as e: - print(f"❌ Error validating match: {e}") + print(f"Error validating match: {e}") continue if best_confidence >= confidence_threshold: - print(f"✅ Best validated Tidal->Spotify match: '{best_track.name}' (confidence: {best_confidence:.3f})") + print(f"Best validated Tidal->Spotify match: '{best_track.name}' (confidence: {best_confidence:.3f})") return best_track else: - print(f"❌ Best Tidal->Spotify confidence {best_confidence:.3f} < {confidence_threshold}") + print(f"Best Tidal->Spotify confidence {best_confidence:.3f} < {confidence_threshold}") return None def clean_for_tidal_matching(self, title): @@ -6701,14 +6701,14 @@ class TidalSpotifyDiscoveryWorker(QRunnable): # Swap: use track name as artist and artist name as track swapped_query = f"{tidal_track.name} {tidal_track.artists[0]}" - print(f"🔄 Trying swapped Tidal fields: '{swapped_query}'") + print(f"Trying swapped Tidal fields: '{swapped_query}'") spotify_results = self.spotify_client.search_tracks(swapped_query, limit=5) if spotify_results: return self.find_best_validated_match(tidal_track, spotify_results) return None except Exception as e: - print(f"❌ Error in swapped fields retry for Tidal track: {e}") + print(f"Error in swapped fields retry for Tidal track: {e}") return None def retry_with_uncleaned_data(self, tidal_track): @@ -6720,14 +6720,14 @@ class TidalSpotifyDiscoveryWorker(QRunnable): else: raw_query = tidal_track.name - print(f"🔍 Trying uncleaned Tidal data: '{raw_query}'") + print(f"Trying uncleaned Tidal data: '{raw_query}'") spotify_results = self.spotify_client.search_tracks(raw_query, limit=5) if spotify_results: return self.find_best_validated_match(tidal_track, spotify_results) return None except Exception as e: - print(f"❌ Error in uncleaned data retry for Tidal track: {e}") + print(f"Error in uncleaned data retry for Tidal track: {e}") return None def retry_with_raw_title_and_artist(self, tidal_track): @@ -6738,7 +6738,7 @@ class TidalSpotifyDiscoveryWorker(QRunnable): # Combine everything into one search term combined_query = f"{tidal_track.name} {tidal_track.artists[0]}" - print(f"🎯 Trying combined Tidal query: '{combined_query}'") + print(f"Trying combined Tidal query: '{combined_query}'") spotify_results = self.spotify_client.search_tracks(combined_query, limit=5) if spotify_results: @@ -6761,17 +6761,17 @@ class TidalSpotifyDiscoveryWorker(QRunnable): continue if best_confidence >= confidence_threshold: - print(f"🎯 Title+artist fallback found match: confidence {best_confidence:.3f}") + print(f"Title+artist fallback found match: confidence {best_confidence:.3f}") return best_track else: - print(f"❌ Title+artist fallback: Best confidence {best_confidence:.3f} < {confidence_threshold}") + print(f"Title+artist fallback: Best confidence {best_confidence:.3f} < {confidence_threshold}") else: - print(f"❌ No results found for title+artist query: '{combined_query}'") + print(f"No results found for title+artist query: '{combined_query}'") return None except Exception as e: - print(f"❌ Error in retry with title+artist for Tidal track: {e}") + print(f"Error in retry with title+artist for Tidal track: {e}") return None def basic_string_similarity(self, s1, s2): @@ -6833,7 +6833,7 @@ class SpotifyDiscoveryWorker(QRunnable): self.signals.track_discovered.emit(track_index, None, "not_found") except Exception as e: - print(f"❌ Worker {self.worker_id} error searching Spotify for track {track_index}: {e}") + print(f"Worker {self.worker_id} error searching Spotify for track {track_index}: {e}") self.signals.track_discovered.emit(track_index, None, f"error: {str(e)}") # Update progress @@ -6843,7 +6843,7 @@ class SpotifyDiscoveryWorker(QRunnable): if not self.is_cancelled: time.sleep(0.5) # 500ms delay with 3 workers = ~6 requests/second total - print(f"🎵 Worker {self.worker_id} completed: {successful_discoveries} discoveries") + print(f"Worker {self.worker_id} completed: {successful_discoveries} discoveries") class SpotifyDiscoveryManager: def __init__(self, youtube_tracks, spotify_client, num_workers=3): @@ -6858,7 +6858,7 @@ class SpotifyDiscoveryManager: def start_discovery(self): """Start concurrent Spotify discovery with multiple workers""" - print(f"🚀 Starting Spotify discovery with {self.num_workers} concurrent workers") + print(f"Starting Spotify discovery with {self.num_workers} concurrent workers") # Divide tracks among workers track_batches = self.distribute_tracks() @@ -6892,7 +6892,7 @@ class SpotifyDiscoveryManager: batch = [(i, self.youtube_tracks[i]) for i in range(start_idx, end_idx)] batches.append(batch) - print(f"📦 Worker {worker_id}: tracks {start_idx}-{end_idx-1} ({len(batch)} tracks)") + print(f"Worker {worker_id}: tracks {start_idx}-{end_idx-1} ({len(batch)} tracks)") start_idx = end_idx return batches @@ -6946,7 +6946,7 @@ class YouTubeParsingWorker(QThread): def run(self): """Parse the YouTube playlist in a separate thread""" try: - print(f"🎵 Starting YouTube playlist parsing for: {self.url}") + print(f"Starting YouTube playlist parsing for: {self.url}") # Parse tracks using yt-dlp tracks_data, playlist_title = parse_youtube_playlist(self.url) @@ -6958,12 +6958,12 @@ class YouTubeParsingWorker(QThread): # Create playlist object with actual title playlist = create_youtube_playlist_object(tracks_data, self.url, playlist_title) - print(f"✅ Successfully created playlist with {len(playlist.tracks)} tracks") + print(f"Successfully created playlist with {len(playlist.tracks)} tracks") self.finished.emit(playlist) except Exception as e: error_message = str(e) - print(f"❌ YouTube parsing worker error: {error_message}") + print(f"YouTube parsing worker error: {error_message}") self.error.emit(error_message) @@ -7184,10 +7184,10 @@ class ManualMatchModal(QDialog): self.failed_tracks = list(live_failed_tracks) new_count = len(self.failed_tracks) - print(f"🔄 Track list sync: {old_count} → {new_count} failed tracks, current_track_id={current_track_id}") + print(f"Track list sync: {old_count} → {new_count} failed tracks, current_track_id={current_track_id}") if not self.failed_tracks: - print("⚠️ No failed tracks remaining") + print("No failed tracks remaining") return new_index = -1 @@ -7210,7 +7210,7 @@ class ManualMatchModal(QDialog): self.current_track_index = 0 if old_index != self.current_track_index: - print(f"📍 Index changed: {old_index} → {self.current_track_index}") + print(f"Index changed: {old_index} → {self.current_track_index}") def load_current_track(self): """Loads the current failed track's info and intelligently triggers a search.""" @@ -7239,7 +7239,7 @@ class ManualMatchModal(QDialog): spotify_track = self.current_track_info['spotify_track'] artist = spotify_track.artists[0] if spotify_track.artists else "Unknown" - print(f"📍 Loading track at index {self.current_track_index}: {spotify_track.name} by {artist}") + print(f"Loading track at index {self.current_track_index}: {spotify_track.name} by {artist}") # Use the original track name for the info label self.info_label.setText(f"Could not find: {spotify_track.name}
    by {artist}") @@ -7254,14 +7254,14 @@ class ManualMatchModal(QDialog): # Sync the track list first to handle any resolved tracks self._update_track_list() - print(f"🔄 Next clicked: current_index={self.current_track_index}, failed_tracks_count={len(self.failed_tracks)}") + print(f"Next clicked: current_index={self.current_track_index}, failed_tracks_count={len(self.failed_tracks)}") if self.current_track_index < len(self.failed_tracks) - 1: self.current_track_index += 1 - print(f"✅ Moving to next track: new_index={self.current_track_index}") + print(f"Moving to next track: new_index={self.current_track_index}") self.load_current_track() else: - print(f"⚠️ Already at last track (index {self.current_track_index} of {len(self.failed_tracks)})") + print(f"Already at last track (index {self.current_track_index} of {len(self.failed_tracks)})") def load_previous_track(self): """Navigate to the previous failed track.""" @@ -7413,7 +7413,7 @@ class ManualMatchModal(QDialog): if not self.failed_tracks: # No more failed tracks - show success and close - QMessageBox.information(self, "Complete", "All failed tracks have been resolved! 🎉") + QMessageBox.information(self, "Complete", "All failed tracks have been resolved! ") self.accept() return @@ -7422,7 +7422,7 @@ class ManualMatchModal(QDialog): self.current_track_index = len(self.failed_tracks) - 1 # Load the next track (which might be at the same index if current was removed) - print(f"🔄 Auto-advancing after resolution: index {self.current_track_index} of {len(self.failed_tracks)} remaining") + print(f"Auto-advancing after resolution: index {self.current_track_index} of {len(self.failed_tracks)} remaining") self.load_current_track() def clear_results(self): @@ -7513,7 +7513,7 @@ class DownloadMissingTracksModal(QDialog): self.permanently_failed_tracks = [] self.cancelled_tracks = set() # Track indices of cancelled tracks - print(f"📊 Total tracks: {self.total_tracks}") + print(f"Total tracks: {self.total_tracks}") # Track analysis results self.analysis_results = [] @@ -7534,9 +7534,9 @@ class DownloadMissingTracksModal(QDialog): self.active_downloads = [] - print("🎨 Setting up UI...") + print("Setting up UI...") self.setup_ui() - print("✅ Modal initialization complete") + print("Modal initialization complete") def generate_smart_search_queries(self, artist_name, track_name): """ @@ -7596,7 +7596,7 @@ class DownloadMissingTracksModal(QDialog): unique_queries.append(query) seen.add(query.lower()) - print(f"🧠 Generated {len(unique_queries)} smart queries for '{track_name}' (enhanced with album detection)") + print(f"Generated {len(unique_queries)} smart queries for '{track_name}' (enhanced with album detection)") for i, query in enumerate(unique_queries): print(f" {i+1}. '{query}'") @@ -7677,10 +7677,10 @@ class DownloadMissingTracksModal(QDialog): dashboard_layout = QHBoxLayout() dashboard_layout.setSpacing(20) - self.total_card = self.create_compact_counter_card("📀 Total", str(self.total_tracks), "#1db954") - self.matched_card = self.create_compact_counter_card("✅ Found", "0", "#4CAF50") + self.total_card = self.create_compact_counter_card("Total", str(self.total_tracks), "#1db954") + self.matched_card = self.create_compact_counter_card("Found", "0", "#4CAF50") self.download_card = self.create_compact_counter_card("⬇️ Missing", "0", "#ff6b6b") - self.downloaded_card = self.create_compact_counter_card("✅ Downloaded", "0", "#4CAF50") + self.downloaded_card = self.create_compact_counter_card("Downloaded", "0", "#4CAF50") dashboard_layout.addWidget(self.total_card) dashboard_layout.addWidget(self.matched_card) @@ -7745,7 +7745,7 @@ class DownloadMissingTracksModal(QDialog): analysis_container = QVBoxLayout() analysis_container.setSpacing(4) - analysis_label = QLabel("🔍 Plex Analysis") + analysis_label = QLabel("Plex Analysis") analysis_label.setFont(QFont("Arial", 11, QFont.Weight.Bold)) analysis_label.setStyleSheet("color: #cccccc;") @@ -7803,7 +7803,7 @@ class DownloadMissingTracksModal(QDialog): layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) - header_label = QLabel("📋 Track Analysis") + header_label = QLabel("Track Analysis") header_label.setFont(QFont("Arial", 13, QFont.Weight.Bold)) header_label.setStyleSheet("color: #ffffff; padding: 5px;") @@ -7855,7 +7855,7 @@ class DownloadMissingTracksModal(QDialog): duration_item = QTableWidgetItem(duration) duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.track_table.setItem(i, 2, duration_item) - matched_item = QTableWidgetItem("⏳ Pending") + matched_item = QTableWidgetItem("Pending") matched_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.track_table.setItem(i, 3, matched_item) status_item = QTableWidgetItem("—") @@ -7943,10 +7943,10 @@ class DownloadMissingTracksModal(QDialog): cancel_button = layout.itemAt(0).widget() if cancel_button: cancel_button.setEnabled(False) - cancel_button.setText("✓") + cancel_button.setText("") # Update status to cancelled - self.track_table.setItem(row, 4, QTableWidgetItem("🚫 Cancelled")) + self.track_table.setItem(row, 4, QTableWidgetItem("Cancelled")) # Add to cancelled tracks set if not hasattr(self, 'cancelled_tracks'): @@ -7954,7 +7954,7 @@ class DownloadMissingTracksModal(QDialog): self.cancelled_tracks.add(row) track = self.playlist.tracks[row] - print(f"🚫 Track cancelled: {track.name} (row {row})") + print(f"Track cancelled: {track.name} (row {row})") # If downloads are active, also handle active download cancellation download_index = None @@ -7964,7 +7964,7 @@ class DownloadMissingTracksModal(QDialog): for download in self.active_downloads: if download.get('table_index') == row: download_index = download.get('download_index', row) - print(f"🚫 Found active download {download_index} for cancelled track") + print(f"Found active download {download_index} for cancelled track") break # Check parallel_search_tracking for download index @@ -7972,12 +7972,12 @@ class DownloadMissingTracksModal(QDialog): for idx, track_info in self.parallel_search_tracking.items(): if track_info.get('table_index') == row: download_index = idx - print(f"🚫 Found parallel tracking {download_index} for cancelled track") + print(f"Found parallel tracking {download_index} for cancelled track") break # If we found an active download, trigger completion to free up the worker if download_index is not None and hasattr(self, 'on_parallel_track_completed'): - print(f"🚫 Triggering completion for active download {download_index}") + print(f"Triggering completion for active download {download_index}") self.on_parallel_track_completed(download_index, success=False) def create_buttons(self): @@ -7987,7 +7987,7 @@ class DownloadMissingTracksModal(QDialog): layout.setSpacing(15) layout.setContentsMargins(0, 10, 0, 0) - self.correct_failed_btn = QPushButton("🔧 Correct Failed Matches") + self.correct_failed_btn = QPushButton("Correct Failed Matches") self.correct_failed_btn.setFixedWidth(220) self.correct_failed_btn.setStyleSheet(""" QPushButton { background-color: #ffc107; color: #000000; border-radius: 20px; font-weight: bold; } @@ -8070,18 +8070,18 @@ class DownloadMissingTracksModal(QDialog): QThreadPool.globalInstance().start(worker) def on_analysis_started(self, total_tracks): - print(f"🔍 Analysis started for {total_tracks} tracks") + print(f"Analysis started for {total_tracks} tracks") def on_track_analyzed(self, track_index, result): """Handle individual track analysis completion with live UI updates""" self.analysis_progress.setValue(track_index) row_index = track_index - 1 if result.exists_in_plex: - matched_text = f"✅ Found ({result.confidence:.1f})" + matched_text = f"Found ({result.confidence:.1f})" self.matched_tracks_count += 1 self.matched_count_label.setText(str(self.matched_tracks_count)) else: - matched_text = "❌ Missing" + matched_text = "Missing" self.tracks_to_download_count += 1 self.download_count_label.setText(str(self.tracks_to_download_count)) # Add cancel button for missing tracks only @@ -8093,7 +8093,7 @@ class DownloadMissingTracksModal(QDialog): self.analysis_complete = True self.analysis_results = results self.missing_tracks = [r for r in results if not r.exists_in_plex] - print(f"✅ Analysis complete: {len(self.missing_tracks)} to download") + print(f"Analysis complete: {len(self.missing_tracks)} to download") if self.missing_tracks: # --- FIX: This line was missing, which prevented downloads from starting. --- self.start_download_progress() @@ -8122,7 +8122,7 @@ class DownloadMissingTracksModal(QDialog): QMessageBox.information(self, "Analysis Complete", f"All tracks already exist in {server_name}! No downloads needed.") def on_analysis_failed(self, error_message): - print(f"❌ Analysis failed: {error_message}") + print(f"Analysis failed: {error_message}") QMessageBox.critical(self, "Analysis Failed", f"Failed to analyze tracks: {error_message}") self.cancel_btn.hide() self.begin_search_btn.show() @@ -8153,12 +8153,12 @@ class DownloadMissingTracksModal(QDialog): # Skip if track was cancelled if hasattr(self, 'cancelled_tracks') and track_index in self.cancelled_tracks: - print(f"🚫 Skipping cancelled track at index {track_index}: {track.name}") + print(f"Skipping cancelled track at index {track_index}: {track.name}") self.download_queue_index += 1 self.completed_downloads += 1 continue - self.track_table.setItem(track_index, 4, QTableWidgetItem("🔍 Searching...")) + self.track_table.setItem(track_index, 4, QTableWidgetItem("Searching...")) self.search_and_download_track_parallel(track, self.download_queue_index, track_index) self.active_parallel_downloads += 1 self.download_queue_index += 1 @@ -8235,7 +8235,7 @@ class DownloadMissingTracksModal(QDialog): # If this track was previously marked as 'completed' (e.g., from a failure), # we need to reset its state to allow the new download attempt to be tracked correctly. if track_info.get('completed', False): - print(f"🔄 Resetting state for manually retried track (index: {download_index}).") + print(f"Resetting state for manually retried track (index: {download_index}).") track_info['completed'] = False # Decrement the failed count since we are retrying it. @@ -8258,7 +8258,7 @@ class DownloadMissingTracksModal(QDialog): # Update UI to show the new download has been queued spotify_based_result = self.create_spotify_based_search_result_from_validation(slskd_result, spotify_metadata) - print(f"🔧 Updating table at index {table_index} to '... Queued' for manual retry") + print(f"Updating table at index {table_index} to '... Queued' for manual retry") self.track_table.setItem(table_index, 4, QTableWidgetItem("... Queued")) # Start the actual download process @@ -8375,7 +8375,7 @@ class DownloadMissingTracksModal(QDialog): download_info['downloading_start_time'] = time.time() # 90-second timeout for being stuck at 0% elif time.time() - download_info['downloading_start_time'] > 90: - print(f"⚠️ Download for '{download_info['slskd_result'].filename}' is stuck at 0%. Cancelling and retrying.") + print(f"Download for '{download_info['slskd_result'].filename}' is stuck at 0%. Cancelling and retrying.") # Cancel the old download before retry self.cancel_download_before_retry(download_info) if download_info in self.active_downloads: @@ -8393,7 +8393,7 @@ class DownloadMissingTracksModal(QDialog): if 'queued_start_time' not in download_info: download_info['queued_start_time'] = time.time() elif time.time() - download_info['queued_start_time'] > 90: # 90-second timeout - print(f"⚠️ Download for '{download_info['slskd_result'].filename}' is stuck in queue. Cancelling and retrying.") + print(f"Download for '{download_info['slskd_result'].filename}' is stuck in queue. Cancelling and retrying.") # Cancel the old download before retry self.cancel_download_before_retry(download_info) if download_info in self.active_downloads: @@ -8407,7 +8407,7 @@ class DownloadMissingTracksModal(QDialog): try: slskd_result = download_info.get('slskd_result') if not slskd_result: - print("⚠️ No slskd_result found in download_info for cancellation") + print("No slskd_result found in download_info for cancellation") return # Extract download details for cancellation @@ -8415,7 +8415,7 @@ class DownloadMissingTracksModal(QDialog): username = getattr(slskd_result, 'username', None) if download_id and username: - print(f"🚫 Cancelling timed-out download: {download_id} from {username}") + print(f"Cancelling timed-out download: {download_id} from {username}") # Use asyncio to call the async cancel method import asyncio @@ -8426,16 +8426,16 @@ class DownloadMissingTracksModal(QDialog): self.soulseek_client.cancel_download(download_id, username, remove=False) ) if success: - print(f"✅ Successfully cancelled download {download_id}") + print(f"Successfully cancelled download {download_id}") else: - print(f"⚠️ Failed to cancel download {download_id}") + print(f"Failed to cancel download {download_id}") finally: loop.close() else: - print(f"⚠️ Missing download_id ({download_id}) or username ({username}) for cancellation") + print(f"Missing download_id ({download_id}) or username ({username}) for cancellation") except Exception as e: - print(f"❌ Error cancelling download: {e}") + print(f"Error cancelling download: {e}") def retry_parallel_download_with_fallback(self, failed_download_info): """Retries a failed download by selecting the next-best cached candidate.""" @@ -8461,8 +8461,8 @@ class DownloadMissingTracksModal(QDialog): self.on_parallel_track_failed(download_index, "No alternative sources in cache") return - print(f"🔄 Retrying download {download_index + 1} with next candidate: {next_candidate.filename}") - self.track_table.setItem(failed_download_info['table_index'], 4, QTableWidgetItem(f"🔄 Retrying ({track_info['retry_count']})...")) + print(f"Retrying download {download_index + 1} with next candidate: {next_candidate.filename}") + self.track_table.setItem(failed_download_info['table_index'], 4, QTableWidgetItem(f"Retrying ({track_info['retry_count']})...")) self.start_validated_download_parallel( next_candidate, track_info['spotify_track'], track_info['track_index'], @@ -8472,15 +8472,15 @@ class DownloadMissingTracksModal(QDialog): def on_parallel_track_completed(self, download_index, success): """Handle completion of a parallel track download""" if not hasattr(self, 'parallel_search_tracking'): - print(f"⚠️ parallel_search_tracking not initialized yet, skipping completion for download {download_index}") + print(f"parallel_search_tracking not initialized yet, skipping completion for download {download_index}") return track_info = self.parallel_search_tracking.get(download_index) if not track_info or track_info.get('completed', False): return track_info['completed'] = True if success: - print(f"🔧 Track {download_index} completed successfully - updating table index {track_info['table_index']} to '✅ Downloaded'") - self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("✅ Downloaded")) + print(f"Track {download_index} completed successfully - updating table index {track_info['table_index']} to 'Downloaded'") + self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("Downloaded")) # Hide cancel button since track is now downloaded self.hide_cancel_button_for_row(track_info['table_index']) self.downloaded_tracks_count += 1 @@ -8501,11 +8501,11 @@ class DownloadMissingTracksModal(QDialog): # Check if track was cancelled (don't overwrite cancelled status) table_index = track_info['table_index'] current_status = self.track_table.item(table_index, 4) - if current_status and "🚫 Cancelled" in current_status.text(): - print(f"🔧 Track {download_index} was cancelled - preserving cancelled status") + if current_status and "Cancelled" in current_status.text(): + print(f"Track {download_index} was cancelled - preserving cancelled status") else: - print(f"🔧 Track {download_index} failed - updating table index {table_index} to '❌ Failed'") - self.track_table.setItem(table_index, 4, QTableWidgetItem("❌ Failed")) + print(f"Track {download_index} failed - updating table index {table_index} to 'Failed'") + self.track_table.setItem(table_index, 4, QTableWidgetItem("Failed")) if track_info not in self.permanently_failed_tracks: self.permanently_failed_tracks.append(track_info) self.update_failed_matches_button() @@ -8527,14 +8527,14 @@ class DownloadMissingTracksModal(QDialog): def on_parallel_track_failed(self, download_index, reason): """Handle failure of a parallel track download""" - print(f"❌ Parallel download {download_index + 1} failed: {reason}") + print(f"Parallel download {download_index + 1} failed: {reason}") self.on_parallel_track_completed(download_index, False) def update_failed_matches_button(self): """Shows, hides, and updates the counter on the 'Correct Failed Matches' button.""" count = len(self.permanently_failed_tracks) if count > 0: - self.correct_failed_btn.setText(f"🔧 Correct {count} Failed Match{'es' if count > 1 else ''}") + self.correct_failed_btn.setText(f"Correct {count} Failed Match{'es' if count > 1 else ''}") self.correct_failed_btn.show() else: self.correct_failed_btn.hide() @@ -8548,11 +8548,11 @@ class DownloadMissingTracksModal(QDialog): def on_manual_match_resolved(self, resolved_track_info): """Handles a track being successfully resolved by the ManualMatchModal.""" - print(f"🔧 Manual match resolved - download_index: {resolved_track_info.get('download_index')}, table_index: {resolved_track_info.get('table_index')}") + print(f"Manual match resolved - download_index: {resolved_track_info.get('download_index')}, table_index: {resolved_track_info.get('table_index')}") original_failed_track = next((t for t in self.permanently_failed_tracks if t['download_index'] == resolved_track_info['download_index']), None) if original_failed_track: self.permanently_failed_tracks.remove(original_failed_track) - print(f"✅ Removed track from permanently_failed_tracks - remaining: {len(self.permanently_failed_tracks)}") + print(f"Removed track from permanently_failed_tracks - remaining: {len(self.permanently_failed_tracks)}") # Update progress bar to account for manually resolved track # The track was manually resolved, so we need to count it as "completed" @@ -8564,9 +8564,9 @@ class DownloadMissingTracksModal(QDialog): # Recalculate progress: completed work / total original work progress_value = self.completed_downloads self.download_progress.setValue(progress_value) - print(f"📊 Updated progress: {progress_value}/{self.download_progress.maximum()} (manual fix)") + print(f"Updated progress: {progress_value}/{self.download_progress.maximum()} (manual fix)") else: - print("⚠️ Could not find original failed track to remove") + print("Could not find original failed track to remove") self.update_failed_matches_button() def find_track_index_in_playlist(self, spotify_track): @@ -8579,7 +8579,7 @@ class DownloadMissingTracksModal(QDialog): def on_all_downloads_complete(self): """Handle completion of all downloads""" self.download_in_progress = False - print("🎉 All downloads completed!") + print("All downloads completed!") self.cancel_btn.hide() # If this is a YouTube workflow, update card and clean up @@ -8636,8 +8636,8 @@ class DownloadMissingTracksModal(QDialog): status_item = self.track_table.item(cancelled_row, 4) current_status = status_item.text() if status_item else "" - if "✅ Downloaded" in current_status: - print(f"🚫 Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist addition") + if "Downloaded" in current_status: + print(f"Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist addition") else: cancelled_track_info = { 'download_index': cancelled_row, @@ -8651,9 +8651,9 @@ class DownloadMissingTracksModal(QDialog): # Check if not already in permanently_failed_tracks if not any(t.get('table_index') == cancelled_row for t in self.permanently_failed_tracks): self.permanently_failed_tracks.append(cancelled_track_info) - print(f"🚫 Added cancelled missing track {cancelled_track.name} to failed list for wishlist") + print(f"Added cancelled missing track {cancelled_track.name} to failed list for wishlist") else: - print(f"🚫 Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist addition") + print(f"Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist addition") # Add permanently failed tracks to wishlist before showing completion message failed_count = len(self.permanently_failed_tracks) @@ -8692,7 +8692,7 @@ class DownloadMissingTracksModal(QDialog): final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing tracks!\n\n" if wishlist_added_count > 0: - final_message += f"✨ Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n" + final_message += f"Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n" final_message += "You can also manually correct failed downloads or check the wishlist on the dashboard." @@ -8708,7 +8708,7 @@ class DownloadMissingTracksModal(QDialog): def on_cancel_clicked(self): """Handle Cancel button - cancels operations, resets state, and closes modal.""" - print("🛑 Cancel button clicked - cancelling all operations and cleaning up") + print("Cancel button clicked - cancelling all operations and cleaning up") self.cancel_operations() self.download_in_progress = False # CRITICAL: Reset the state flag. @@ -8716,13 +8716,13 @@ class DownloadMissingTracksModal(QDialog): if self.is_youtube_workflow: # Revert the main card to the discovery phase. if hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'update_youtube_card_phase'): - print("🔄 Returning YouTube playlist to discovery_complete state") + print("Returning YouTube playlist to discovery_complete state") self.parent_page.update_youtube_card_phase(self.youtube_url, 'discovery_complete') # Handle Tidal playlist cancel - revert to discovery_complete phase if hasattr(self, 'is_tidal_playlist') and self.is_tidal_playlist and hasattr(self, 'playlist_id'): if hasattr(self.parent_page, 'update_tidal_card_phase'): - print("🔄 Returning Tidal playlist to discovery_complete state") + print("Returning Tidal playlist to discovery_complete state") self.parent_page.update_tidal_card_phase(self.playlist_id, 'discovery_complete') # Clean up download modal reference from Tidal state @@ -8741,7 +8741,7 @@ class DownloadMissingTracksModal(QDialog): # on subsequent cancellations. if (hasattr(self.parent_page, 'youtube_status_widgets') and self.playlist.id in self.parent_page.youtube_status_widgets): - print(f"🧹 Cleaning up YouTube status widget on cancel for playlist: {self.playlist.id}") + print(f"Cleaning up YouTube status widget on cancel for playlist: {self.playlist.id}") status_widget = self.parent_page.youtube_status_widgets.pop(self.playlist.id, None) if status_widget: status_widget.setParent(None) @@ -8756,7 +8756,7 @@ class DownloadMissingTracksModal(QDialog): def cancel_operations(self): """Cancel any ongoing operations, including active slskd downloads.""" - print("🛑 Cancelling all operations for this playlist...") + print("Cancelling all operations for this playlist...") self.cancel_requested = True # Flag to stop any new workers from starting. # --- FIX: Actively cancel downloads on the slskd server --- @@ -8804,7 +8804,7 @@ class DownloadMissingTracksModal(QDialog): # Stop the status polling timer to prevent further checks self.download_status_timer.stop() - print("🛑 Modal operations cancelled successfully.") + print("Modal operations cancelled successfully.") def closeEvent(self, event): """Override the window's close event to provide custom logic.""" @@ -8882,10 +8882,10 @@ class DownloadMissingTracksModal(QDialog): initial_candidates = self.matching_engine.find_best_slskd_matches_enhanced(spotify_track, results) if not initial_candidates: - print(f"⚠️ No initial candidates found for '{spotify_track.name}' from query '{query}'.") + print(f"No initial candidates found for '{spotify_track.name}' from query '{query}'.") return [] - print(f"✅ Found {len(initial_candidates)} initial candidates for '{spotify_track.name}'. Now verifying artist...") + print(f"Found {len(initial_candidates)} initial candidates for '{spotify_track.name}'. Now verifying artist...") # Step 2: Perform strict artist verification on the initial candidates. verified_candidates = [] @@ -8906,11 +8906,11 @@ class DownloadMissingTracksModal(QDialog): # **THE CRITICAL CHECK**: See if the cleaned artist's name is in the cleaned folder path. if normalized_spotify_artist in normalized_slskd_path: # Artist name was found in the path, this is a valid candidate. - print(f"✔️ Artist '{spotify_artist_name}' VERIFIED in path: '{slskd_full_path}'") + print(f"Artist '{spotify_artist_name}' VERIFIED in path: '{slskd_full_path}'") verified_candidates.append(candidate) else: # Artist name was NOT found. Discard this candidate. - print(f"❌ Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.") + print(f"Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.") if verified_candidates: # Apply quality profile filtering before returning @@ -8921,14 +8921,14 @@ class DownloadMissingTracksModal(QDialog): if quality_filtered: verified_candidates = quality_filtered - print(f"🎯 Applied quality profile filtering: {len(verified_candidates)} candidates remain") + print(f"Applied quality profile filtering: {len(verified_candidates)} candidates remain") else: - print(f"⚠️ Quality profile filtering removed all candidates, keeping originals") + print(f"Quality profile filtering removed all candidates, keeping originals") best_confidence = verified_candidates[0].confidence best_version = getattr(verified_candidates[0], 'version_type', 'unknown') best_quality = getattr(verified_candidates[0], 'quality', 'unknown') - print(f"✅ Found {len(verified_candidates)} VERIFIED matches for '{spotify_track.name}'. Best: {best_confidence:.2f} ({best_version}, {best_quality.upper()})") + print(f"Found {len(verified_candidates)} VERIFIED matches for '{spotify_track.name}'. Best: {best_confidence:.2f} ({best_version}, {best_quality.upper()})") # Log version breakdown for debugging for candidate in verified_candidates[:3]: # Show top 3 @@ -8936,10 +8936,10 @@ class DownloadMissingTracksModal(QDialog): penalty = getattr(candidate, 'version_penalty', 0.0) quality = getattr(candidate, 'quality', 'unknown') bitrate_info = f" {candidate.bitrate}kbps" if hasattr(candidate, 'bitrate') and candidate.bitrate else "" - print(f" 🎵 {candidate.confidence:.2f} - {version} ({quality.upper()}{bitrate_info}) (penalty: {penalty:.2f}) - {candidate.filename[:80]}...") + print(f" {candidate.confidence:.2f} - {version} ({quality.upper()}{bitrate_info}) (penalty: {penalty:.2f}) - {candidate.filename[:80]}...") else: - print(f"⚠️ No verified matches found for '{spotify_track.name}' after checking file paths.") + print(f"No verified matches found for '{spotify_track.name}' after checking file paths.") return verified_candidates def create_spotify_based_search_result_from_validation(self, slskd_result, spotify_metadata): @@ -9043,7 +9043,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): layout = QVBoxLayout(header_frame) - title = QLabel("🎵 YouTube Playlist Discovery") + title = QLabel("YouTube Playlist Discovery") title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) title.setStyleSheet("color: #1db954;") @@ -9074,7 +9074,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): layout = QVBoxLayout(progress_frame) # Spotify discovery progress - spotify_label = QLabel("🔍 Spotify Discovery Progress") + spotify_label = QLabel("Spotify Discovery Progress") spotify_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) self.spotify_progress = QProgressBar() @@ -9090,7 +9090,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): """) # Plex analysis progress (hidden initially) - analysis_label = QLabel("📊 Plex Analysis Progress") + analysis_label = QLabel("Plex Analysis Progress") analysis_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) self.analysis_progress = QProgressBar() @@ -9126,7 +9126,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) - header_label = QLabel("📋 Track Discovery & Analysis") + header_label = QLabel("Track Discovery & Analysis") header_label.setFont(QFont("Arial", 13, QFont.Weight.Bold)) header_label.setStyleSheet("color: #ffffff; padding: 5px;") @@ -9176,7 +9176,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): layout.addWidget(self.sync_status_widget) # Sync button - appears to the left of Begin Search - self.sync_btn = QPushButton("🔄 Sync This Playlist") + self.sync_btn = QPushButton("Sync This Playlist") self.sync_btn.setEnabled(False) # Disabled until Spotify discovery completes self.sync_btn.clicked.connect(self.on_sync_clicked) self.sync_btn.setStyleSheet(""" @@ -9189,15 +9189,15 @@ class YouTubeDownloadMissingTracksModal(QDialog): QPushButton:disabled { background-color: #404040; color: #888888; } """) - self.begin_search_btn = QPushButton("🔍 Download Missing Tracks") + self.begin_search_btn = QPushButton("Download Missing Tracks") self.begin_search_btn.setEnabled(False) # Disabled until Spotify discovery completes self.begin_search_btn.clicked.connect(self.on_begin_plex_analysis) - self.cancel_btn = QPushButton("❌ Cancel") + self.cancel_btn = QPushButton("Cancel") self.cancel_btn.clicked.connect(self.on_cancel_clicked) # Close button - hides modal without clearing data - self.close_btn = QPushButton("🏠 Close") + self.close_btn = QPushButton("Close") self.close_btn.clicked.connect(self.on_close_clicked) self.close_btn.setStyleSheet(""" QPushButton { @@ -9233,17 +9233,17 @@ class YouTubeDownloadMissingTracksModal(QDialog): layout.setSpacing(12) # Total tracks - self.total_tracks_label = QLabel("♪ 0") + self.total_tracks_label = QLabel("0") self.total_tracks_label.setFont(QFont("SF Pro Text", 12, QFont.Weight.Medium)) self.total_tracks_label.setStyleSheet("color: #ffa500; background: transparent; border: none;") # Matched tracks - self.matched_tracks_label = QLabel("✓ 0") + self.matched_tracks_label = QLabel("0") self.matched_tracks_label.setFont(QFont("SF Pro Text", 12, QFont.Weight.Medium)) self.matched_tracks_label.setStyleSheet("color: #1db954; background: transparent; border: none;") # Failed tracks - self.failed_tracks_label = QLabel("✗ 0") + self.failed_tracks_label = QLabel("0") self.failed_tracks_label.setFont(QFont("SF Pro Text", 12, QFont.Weight.Medium)) self.failed_tracks_label.setStyleSheet("color: #e22134; background: transparent; border: none;") @@ -9283,9 +9283,9 @@ class YouTubeDownloadMissingTracksModal(QDialog): def update_sync_status(self, total_tracks=0, matched_tracks=0, failed_tracks=0): """Update sync status display""" if self.sync_status_widget: - self.total_tracks_label.setText(f"♪ {total_tracks}") - self.matched_tracks_label.setText(f"✓ {matched_tracks}") - self.failed_tracks_label.setText(f"✗ {failed_tracks}") + self.total_tracks_label.setText(f"{total_tracks}") + self.matched_tracks_label.setText(f"{matched_tracks}") + self.failed_tracks_label.setText(f"{failed_tracks}") if total_tracks > 0: processed_tracks = matched_tracks + failed_tracks @@ -9323,13 +9323,13 @@ class YouTubeDownloadMissingTracksModal(QDialog): def start_spotify_discovery(self): """Start the Spotify discovery process using background worker""" - print(f"🔍 Starting Spotify discovery for {self.total_tracks} tracks...") + print(f"Starting Spotify discovery for {self.total_tracks} tracks...") # Update all rows to show "Searching..." status for row in range(self.total_tracks): status_item = self.track_table.item(row, 2) if status_item: - status_item.setText("🔍 Pending...") + status_item.setText("Pending...") # Create and start a single optimized Spotify discovery worker # Import matching engine for validation @@ -9338,14 +9338,14 @@ class YouTubeDownloadMissingTracksModal(QDialog): # Use TidalSpotifyDiscoveryWorker if this is a Tidal playlist if hasattr(self, 'is_tidal_playlist') and self.is_tidal_playlist: - print("🎵 Using Tidal discovery worker for Tidal playlist") + print("Using Tidal discovery worker for Tidal playlist") self.spotify_worker = TidalSpotifyDiscoveryWorker( self.playlist.tracks, self.parent_page.spotify_client, matching_engine ) else: - print("🎥 Using YouTube discovery worker for YouTube playlist") + print("Using YouTube discovery worker for YouTube playlist") self.spotify_worker = OptimizedSpotifyDiscoveryWorker( self.playlist.tracks, self.parent_page.spotify_client, @@ -9373,7 +9373,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): else: # error self.update_table_with_error(row, status.replace("error: ", "")) except Exception as e: - print(f"❌ Error updating UI for track {row}: {e}") + print(f"Error updating UI for track {row}: {e}") def on_discovery_progress(self, current): """Update the discovery progress""" @@ -9382,13 +9382,13 @@ class YouTubeDownloadMissingTracksModal(QDialog): def on_spotify_discovery_finished(self, successful_discoveries): """Handle Spotify discovery completion""" self.spotify_discovery_completed() - print(f"🎵 Spotify discovery completed: {successful_discoveries}/{self.total_tracks} tracks found") + print(f"Spotify discovery completed: {successful_discoveries}/{self.total_tracks} tracks found") def update_table_with_spotify_match(self, row, spotify_track): """Update table row with successful Spotify match""" # Spotify Match Status status_item = self.track_table.item(row, 2) - status_item.setText("✅ Found") + status_item.setText("Found") status_item.setForeground(QBrush(QColor("#4CAF50"))) # Spotify Track @@ -9416,7 +9416,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): """Update table row when no Spotify match found""" # Spotify Match Status status_item = self.track_table.item(row, 2) - status_item.setText("❌ Not Found") + status_item.setText("Not Found") status_item.setForeground(QBrush(QColor("#ff6b6b"))) # Status @@ -9428,7 +9428,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): """Update table row when Spotify matches were found but confidence too low""" # Spotify Match Status status_item = self.track_table.item(row, 2) - status_item.setText("⚠️ Low Confidence") + status_item.setText("Low Confidence") status_item.setForeground(QBrush(QColor("#FFA500"))) # Status @@ -9440,7 +9440,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): """Update table row when search error occurred""" # Spotify Match Status status_item = self.track_table.item(row, 2) - status_item.setText("⚠️ Error") + status_item.setText("Error") status_item.setForeground(QBrush(QColor("#FFA500"))) # Status @@ -9455,11 +9455,11 @@ class YouTubeDownloadMissingTracksModal(QDialog): # Count successful discoveries successful_discoveries = sum(1 for track in self.spotify_discovered_tracks if track is not None) - print(f"🎵 Spotify discovery completed: {successful_discoveries}/{self.total_tracks} tracks found") + print(f"Spotify discovery completed: {successful_discoveries}/{self.total_tracks} tracks found") # Update card state for Tidal playlists (matches YouTube workflow) if hasattr(self, 'is_tidal_playlist') and self.is_tidal_playlist and hasattr(self, 'playlist_id'): - print(f"🎵 Updating Tidal card state to discovery_complete for playlist_id: {self.playlist_id}") + print(f"Updating Tidal card state to discovery_complete for playlist_id: {self.playlist_id}") if hasattr(self.parent_page, 'update_tidal_card_phase'): self.parent_page.update_tidal_card_phase(self.playlist_id, 'discovery_complete') @@ -9469,13 +9469,13 @@ class YouTubeDownloadMissingTracksModal(QDialog): # Update card state for YouTube playlists (existing logic) if hasattr(self, 'youtube_url'): - print(f"🎬 Updating YouTube card state to discovery_complete for URL: {self.youtube_url}") + print(f"Updating YouTube card state to discovery_complete for URL: {self.youtube_url}") if hasattr(self.parent_page, 'update_youtube_card_phase'): self.parent_page.update_youtube_card_phase(self.youtube_url, 'discovery_complete') # Enable the Plex analysis and sync buttons self.begin_search_btn.setEnabled(True) - self.begin_search_btn.setText(f"🔍 Download Missing Tracks ({successful_discoveries} tracks)") + self.begin_search_btn.setText(f"Download Missing Tracks ({successful_discoveries} tracks)") self.sync_btn.setEnabled(True) @@ -9488,7 +9488,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): QMessageBox.warning(self, "No Tracks", "No tracks were successfully discovered on Spotify.") return - print(f"🎵 Creating discovered playlist with {len(valid_spotify_tracks)} Spotify tracks...") + print(f"Creating discovered playlist with {len(valid_spotify_tracks)} Spotify tracks...") # Create a Spotify-compatible playlist from discovered tracks discovered_playlist = self.create_discovered_playlist(valid_spotify_tracks) @@ -9509,7 +9509,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): })() # Open the regular DownloadMissingTracksModal with the discovered playlist - print("🚀 Opening regular DownloadMissingTracksModal with discovered tracks...") + print("Opening regular DownloadMissingTracksModal with discovered tracks...") modal = DownloadMissingTracksModal( discovered_playlist, dummy_playlist_item, @@ -9522,7 +9522,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): if hasattr(self, 'youtube_url'): modal.youtube_url = self.youtube_url self.parent_page.active_youtube_processes[self.youtube_url] = modal - print(f"🔄 Transferred URL tracking to download modal: {self.youtube_url}") + print(f"Transferred URL tracking to download modal: {self.youtube_url}") # Update card to downloading phase if hasattr(self.parent_page, 'update_youtube_card_phase'): @@ -9533,7 +9533,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): modal.playlist_id = self.playlist_id modal.is_tidal_playlist = True modal.tidal_playlist = discovered_playlist - print(f"🔄 Transferred Tidal playlist tracking to download modal: {self.playlist_id}") + print(f"Transferred Tidal playlist tracking to download modal: {self.playlist_id}") # Update Tidal card to downloading phase if hasattr(self.parent_page, 'update_tidal_card_phase'): @@ -9545,7 +9545,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): state['download_modal'] = modal # Store the modal reference using the ID of the NEWLY created playlist object. - print(f"📝 Storing modal with CORRECT discovered_playlist.id: {discovered_playlist.id}") + print(f"Storing modal with CORRECT discovered_playlist.id: {discovered_playlist.id}") self.parent_page.active_youtube_download_modals[discovered_playlist.id] = modal modal.exec() @@ -9554,14 +9554,14 @@ class YouTubeDownloadMissingTracksModal(QDialog): """Handle Sync This Playlist button click""" if self.sync_in_progress: # Cancel ongoing sync - print(f"🛑 Cancelling sync for playlist: {self.playlist.name}") + print(f"Cancelling sync for playlist: {self.playlist.name}") if hasattr(self.parent_page, 'cancel_playlist_sync'): self.parent_page.cancel_playlist_sync(self.playlist.id) # Reset sync state immediately (don't wait for callback) self.sync_in_progress = False - self.sync_btn.setText("🔄 Sync This Playlist") + self.sync_btn.setText("Sync This Playlist") self.sync_btn.setStyleSheet(""" QPushButton { background-color: #ff6b6b; color: #ffffff; border: none; @@ -9573,14 +9573,14 @@ class YouTubeDownloadMissingTracksModal(QDialog): """) # Status widgets are no longer used for sync - cards handle their own state - print("🔄 Sync cancelled - card will update its own state") + print("Sync cancelled - card will update its own state") else: # Start sync using the parent page's sync infrastructure - print(f"🔄 Starting sync for playlist: {self.playlist.name}") + print(f"Starting sync for playlist: {self.playlist.name}") if hasattr(self.parent_page, 'start_playlist_sync') and self.parent_page.start_playlist_sync(self.playlist): - print(f"✅ Sync started successfully for: {self.playlist.name}") + print(f"Sync started successfully for: {self.playlist.name}") # Update UI to show sync is active self.sync_in_progress = True @@ -9601,36 +9601,36 @@ class YouTubeDownloadMissingTracksModal(QDialog): # Update card to syncing phase if hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'update_youtube_card_phase'): - print(f"🎬 Discovery modal: Setting card to syncing phase for URL: {self.youtube_url}") - print(f"🎬 Discovery modal: Using playlist.id: {self.playlist.id}") + print(f"Discovery modal: Setting card to syncing phase for URL: {self.youtube_url}") + print(f"Discovery modal: Using playlist.id: {self.playlist.id}") self.parent_page.update_youtube_card_phase(self.youtube_url, 'syncing') # The card itself will show sync progress - no need for separate status widget else: - print(f"❌ Failed to start sync for: {self.playlist.name}") + print(f"Failed to start sync for: {self.playlist.name}") QMessageBox.warning(self, "Sync Failed", "Failed to start playlist sync. Please try again.") def on_cancel_clicked(self): """Handle cancel button click - cancel sync or close modal""" - print("🛑 Cancel button clicked") + print("Cancel button clicked") # Cancel any running Spotify discovery worker if self.spotify_worker: - print("🛑 Cancelling Spotify discovery worker") + print("Cancelling Spotify discovery worker") self.spotify_worker.cancel() self.spotify_worker = None if self.sync_in_progress: # Cancel sync operation - print("🛑 Cancelling sync operation") + print("Cancelling sync operation") if hasattr(self.parent_page, 'cancel_playlist_sync'): self.parent_page.cancel_playlist_sync(self.playlist.id) # Reset sync state self.sync_in_progress = False - self.sync_btn.setText("🔄 Sync This Playlist") + self.sync_btn.setText("Sync This Playlist") self.sync_btn.setStyleSheet(""" QPushButton { background-color: #ff6b6b; color: #ffffff; border: none; @@ -9642,17 +9642,17 @@ class YouTubeDownloadMissingTracksModal(QDialog): """) # Status widgets are no longer used for sync - cards handle their own state - print("🔄 Sync cancelled - card will update its own state") + print("Sync cancelled - card will update its own state") # Clean up URL tracking before closing (but not during download transition) if (hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'active_youtube_processes') and not getattr(self, 'transitioning_to_download', False)): if self.youtube_url in self.parent_page.active_youtube_processes: - print(f"🧹 Cleaning up URL tracking on cancel for: {self.youtube_url}") + print(f"Cleaning up URL tracking on cancel for: {self.youtube_url}") del self.parent_page.active_youtube_processes[self.youtube_url] # Always close/hide the modal when cancel is clicked - print("🛑 Closing modal") + print("Closing modal") # Update card state - reset to initial discovering state for Cancel if hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'reset_youtube_playlist_state'): @@ -9661,27 +9661,27 @@ class YouTubeDownloadMissingTracksModal(QDialog): # Update Tidal card state - reset to initial discovering state for Cancel if hasattr(self, 'is_tidal_playlist') and self.is_tidal_playlist and hasattr(self, 'playlist_id'): if hasattr(self.parent_page, 'reset_tidal_playlist_state'): - print(f"🧹 Resetting Tidal playlist state to discovering on cancel for playlist_id: {self.playlist_id}") + print(f"Resetting Tidal playlist state to discovering on cancel for playlist_id: {self.playlist_id}") self.parent_page.reset_tidal_playlist_state(self.playlist_id) self.reject() def on_close_clicked(self): """Handle Close button click - hide modal but preserve discovery data""" - print("🏠 Close button clicked - preserving discovery data") + print("Close button clicked - preserving discovery data") # Check if sync is currently in progress - if so, preserve the syncing state if hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'update_youtube_card_phase'): if self.sync_in_progress: # Sync is running - keep the card in syncing state - print("🔄 Sync in progress - preserving syncing state") + print("Sync in progress - preserving syncing state") # Don't change the card phase - it should stay as 'syncing' elif self.spotify_search_completed: # No sync running, discovery complete - safe to set to discovery_complete self.parent_page.update_youtube_card_phase(self.youtube_url, 'discovery_complete') else: # Discovery still running - keep as discovering but hide modal - print("🔄 Discovery still in progress - keeping discovering state") + print("Discovery still in progress - keeping discovering state") # Just hide the modal, don't reset any data self.hide() @@ -9689,25 +9689,25 @@ class YouTubeDownloadMissingTracksModal(QDialog): def on_sync_progress(self, playlist_id, progress): """Handle sync progress updates (called from parent page)""" try: - print(f"🔍 YouTube modal sync progress called: playlist_id={playlist_id}, my_id={self.playlist.id}") - print(f"🔍 YouTube modal sync_in_progress={self.sync_in_progress}") - print(f"🔍 YouTube modal progress data: total={progress.total_tracks}, matched={progress.matched_tracks}, failed={progress.failed_tracks}") + print(f"YouTube modal sync progress called: playlist_id={playlist_id}, my_id={self.playlist.id}") + print(f"YouTube modal sync_in_progress={self.sync_in_progress}") + print(f"YouTube modal progress data: total={progress.total_tracks}, matched={progress.matched_tracks}, failed={progress.failed_tracks}") if playlist_id == self.playlist.id: - print(f"🔄 ✅ Playlist ID matches - processing sync progress for YouTube playlist") + print(f"Playlist ID matches - processing sync progress for YouTube playlist") if self.sync_in_progress: - print(f"🔄 ✅ Sync in progress - updating status widget") + print(f"Sync in progress - updating status widget") # Show and update the sync status widget (same as Spotify modal) if self.sync_status_widget: - print(f"📊 ✅ Status widget exists - showing and updating") + print(f"Status widget exists - showing and updating") self.sync_status_widget.show() self.update_sync_status( progress.total_tracks, progress.matched_tracks, progress.failed_tracks ) - print(f"📊 ✅ Status widget updated successfully") + print(f"Status widget updated successfully") # Update card progress as well if hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'update_youtube_card_progress'): @@ -9718,16 +9718,16 @@ class YouTubeDownloadMissingTracksModal(QDialog): failed=progress.failed_tracks ) else: - print("❌ sync_status_widget is None!") + print("sync_status_widget is None!") else: - print(f"🔄 ❌ Sync not in progress (sync_in_progress={self.sync_in_progress})") + print(f"Sync not in progress (sync_in_progress={self.sync_in_progress})") else: - print(f"🔄 ❌ Playlist ID mismatch: {playlist_id} != {self.playlist.id}") + print(f"Playlist ID mismatch: {playlist_id} != {self.playlist.id}") except Exception as e: - print(f"💥 EXCEPTION in YouTube modal on_sync_progress: {e}") + print(f"EXCEPTION in YouTube modal on_sync_progress: {e}") import traceback - print(f"💥 Traceback: {traceback.format_exc()}") + print(f"Traceback: {traceback.format_exc()}") # Update the card progress display instead of creating status widgets if hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'update_youtube_card_progress'): @@ -9741,7 +9741,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): def on_sync_finished(self, playlist_id, result): """Handle sync completion (called from parent page)""" if playlist_id == self.playlist.id: - print(f"🎉 Sync completed for YouTube playlist: {self.playlist.name}") + print(f"Sync completed for YouTube playlist: {self.playlist.name}") # Reset sync state self.sync_in_progress = False @@ -9751,7 +9751,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): self.sync_status_widget.hide() # Reset sync button to original state - self.sync_btn.setText("🔄 Sync This Playlist") + self.sync_btn.setText("Sync This Playlist") self.sync_btn.setStyleSheet(""" QPushButton { background-color: #ff6b6b; color: #ffffff; border: none; @@ -9769,7 +9769,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): def on_sync_error(self, playlist_id, error_msg): """Handle sync error (called from parent page)""" if playlist_id == self.playlist.id: - print(f"❌ Sync error for YouTube playlist: {self.playlist.name} - {error_msg}") + print(f"Sync error for YouTube playlist: {self.playlist.name} - {error_msg}") # Reset sync state self.sync_in_progress = False @@ -9779,7 +9779,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): self.sync_status_widget.hide() # Reset sync button to original state - self.sync_btn.setText("🔄 Sync This Playlist") + self.sync_btn.setText("Sync This Playlist") self.sync_btn.setStyleSheet(""" QPushButton { background-color: #ff6b6b; color: #ffffff; border: none; @@ -9794,7 +9794,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): """Create a playlist object from discovered Spotify tracks, reusing the original ID and name.""" playlist_id = self.playlist.id - print(f"🎵 Creating discovered playlist with consistent ID: {playlist_id}") + print(f"Creating discovered playlist with consistent ID: {playlist_id}") discovered_playlist = type('Playlist', (), { 'id': playlist_id, @@ -9823,7 +9823,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): self.track_table.setRowCount(0) # Show loading message - loading_item = QTableWidgetItem("🔄 Parsing YouTube playlist...") + loading_item = QTableWidgetItem("Parsing YouTube playlist...") loading_item.setFlags(loading_item.flags() & ~Qt.ItemFlag.ItemIsEditable) self.track_table.setRowCount(1) @@ -9836,7 +9836,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): def populate_with_playlist_data(self, playlist): """Populate the modal with actual playlist data""" - print(f"📊 Populating modal with {len(playlist.tracks)} tracks") + print(f"Populating modal with {len(playlist.tracks)} tracks") # Update modal properties self.playlist = playlist @@ -9870,18 +9870,18 @@ class YouTubeDownloadMissingTracksModal(QDialog): def closeEvent(self, event): """Handle modal closing - hide when sync is active, otherwise close""" - print(f"🔍 DEBUG: YouTube modal closeEvent - sync_in_progress: {self.sync_in_progress}") + print(f"DEBUG: YouTube modal closeEvent - sync_in_progress: {self.sync_in_progress}") # If sync is in progress, just hide the modal (don't close) if self.sync_in_progress: - print("🔍 DEBUG: Sync in progress - hiding modal instead of closing") + print("DEBUG: Sync in progress - hiding modal instead of closing") event.ignore() # Prevent actual closing self.hide() return # Normal close behavior - cancel any running workers if self.spotify_worker: - print("🛑 closeEvent: Cancelling Spotify discovery worker") + print("closeEvent: Cancelling Spotify discovery worker") self.spotify_worker.cancel() self.spotify_worker = None @@ -9890,7 +9890,7 @@ class YouTubeDownloadMissingTracksModal(QDialog): if (hasattr(self, 'youtube_url') and hasattr(self.parent_page, 'active_youtube_processes') and not getattr(self, 'transitioning_to_download', False)): if self.youtube_url in self.parent_page.active_youtube_processes: - print(f"🧹 Cleaning up URL tracking for: {self.youtube_url}") + print(f"Cleaning up URL tracking for: {self.youtube_url}") del self.parent_page.active_youtube_processes[self.youtube_url] # Clean up Tidal playlist state when modal is actually closed (not just hidden) @@ -9906,12 +9906,12 @@ class YouTubeDownloadMissingTracksModal(QDialog): # Only clear the modal reference, don't reset the entire state # This preserves discovery data for when user reopens the modal if state.get('discovery_modal') == self: - print(f"🧹 Cleaning up Tidal discovery modal reference for playlist_id: {playlist_id}") + print(f"Cleaning up Tidal discovery modal reference for playlist_id: {playlist_id}") state['discovery_modal'] = None # If discovery was completed, keep the state, otherwise reset it if state.get('phase') == 'discovering': - print(f"🧹 Discovery incomplete, resetting Tidal state for playlist_id: {playlist_id}") + print(f"Discovery incomplete, resetting Tidal state for playlist_id: {playlist_id}") self.parent_page.reset_tidal_playlist_state(playlist_id) super().closeEvent(event) diff --git a/ui/sidebar.py b/ui/sidebar.py index ac8bbb28..4b202488 100644 --- a/ui/sidebar.py +++ b/ui/sidebar.py @@ -892,7 +892,7 @@ class MediaPlayer(QWidget): volume_layout = QHBoxLayout() volume_layout.setSpacing(10) - volume_icon = QLabel("🔊") + volume_icon = QLabel("") volume_icon.setStyleSheet(""" QLabel { color: #b3b3b3; @@ -933,7 +933,7 @@ class MediaPlayer(QWidget): self.volume_slider.valueChanged.connect(self.on_volume_changed) # Stop button - more visible Spotify style - self.stop_btn = QPushButton("⏹") + self.stop_btn = QPushButton("") self.stop_btn.setFixedSize(32, 32) self.stop_btn.setStyleSheet(""" QPushButton { @@ -1029,7 +1029,7 @@ class MediaPlayer(QWidget): """Update play/pause button state""" self.is_playing = playing if playing: - self.play_pause_btn.setText("⏸︎") + self.play_pause_btn.setText("") # Start scrolling animation when playing if self.track_info.should_scroll and not self.track_info.is_scrolling: self.track_info.start_scroll_animation() @@ -1207,11 +1207,11 @@ class ModernSidebar(QWidget): # Navigation buttons nav_items = [ - ("dashboard", "Dashboard", "📊"), - ("sync", "Sync", "🔄"), - ("downloads", "Search", "📥"), - ("artists", "Artists", "🎵"), - ("settings", "Settings", "⚙️") + ("dashboard", "Dashboard", ""), + ("sync", "Sync", ""), + ("downloads", "Search", ""), + ("artists", "Artists", ""), + ("settings", "Settings", "") ] for page_id, title, icon in nav_items: diff --git a/web_server.py b/web_server.py index 260a4f0e..f3621afd 100644 --- a/web_server.py +++ b/web_server.py @@ -78,7 +78,7 @@ from services.sync_service import PlaylistSyncService # 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("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("") @@ -114,7 +114,7 @@ project_root = os.path.dirname(base_dir) # Go up one level to the project root 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}") + print(f"Using config path from environment: {config_path}") else: config_path = os.path.join(project_root, 'config', 'config.json') @@ -128,7 +128,7 @@ 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}") + print(f"Web server configuration already loaded from: {config_path}") else: print(f"Found config file at: {config_path}") # Load configuration into the existing singleton instance @@ -136,12 +136,12 @@ if os.path.exists(config_path): config_manager.load_config(config_path) else: # Fallback for older settings.py in Docker volumes - print("⚠️ Legacy configuration detected: using fallback loading method") + print("Legacy configuration detected: using fallback loading method") config_manager.config_path = Path(config_path) config_manager._load_config() - print("✅ Web server configuration loaded successfully.") + print("Web server configuration loaded successfully.") else: - print(f"🔴 WARNING: config.json not found at {config_path}. Using default settings.") + print(f"WARNING: config.json not found at {config_path}. Using default settings.") # Correctly point to the 'webui' directory for templates and static files app = Flask( __name__, @@ -329,71 +329,71 @@ 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...") +print("Initializing SoulSync services for Web UI...") spotify_client = plex_client = jellyfin_client = navidrome_client = soulseek_client = tidal_client = matching_engine = sync_service = web_scan_manager = None try: spotify_client = SpotifyClient() - print(" ✅ Spotify client initialized") + print(" Spotify client initialized") except Exception as e: - print(f" ⚠️ Spotify client failed to initialize: {e}") + print(f" Spotify client failed to initialize: {e}") try: plex_client = PlexClient() - print(" ✅ Plex client initialized") + print(" Plex client initialized") except Exception as e: - print(f" ⚠️ Plex client failed to initialize: {e}") + print(f" Plex client failed to initialize: {e}") try: jellyfin_client = JellyfinClient() - print(" ✅ Jellyfin client initialized") + print(" Jellyfin client initialized") except Exception as e: - print(f" ⚠️ Jellyfin client failed to initialize: {e}") + print(f" Jellyfin client failed to initialize: {e}") try: navidrome_client = NavidromeClient() - print(" ✅ Navidrome client initialized") + print(" Navidrome client initialized") except Exception as e: - print(f" ⚠️ Navidrome client failed to initialize: {e}") + print(f" Navidrome client failed to initialize: {e}") try: soulseek_client = DownloadOrchestrator() - print(" ✅ Download orchestrator initialized") + print(" Download orchestrator initialized") except Exception as e: - print(f" ⚠️ Download orchestrator failed to initialize: {e}") + print(f" Download orchestrator failed to initialize: {e}") try: tidal_client = TidalClient() - print(" ✅ Tidal client initialized") + print(" Tidal client initialized") except Exception as e: - print(f" ⚠️ Tidal client failed to initialize: {e}") + print(f" Tidal client failed to initialize: {e}") try: matching_engine = MusicMatchingEngine() - print(" ✅ Matching engine initialized") + print(" Matching engine initialized") except Exception as e: - print(f" ⚠️ Matching engine failed to initialize: {e}") + print(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") + print(" Playlist sync service initialized") except Exception as e: - print(f" ⚠️ Playlist sync service failed to initialize: {e}") + print(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") + print(" 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") + print(" 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") + print(" 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") + print(" Configured HiFi client shutdown callback") # Initialize web scan manager for automatic post-download scanning try: @@ -403,18 +403,18 @@ try: 'navidrome_client': navidrome_client } web_scan_manager = WebScanManager(media_clients, delay_seconds=60) - print(" ✅ Web scan manager initialized") + print(" Web scan manager initialized") except Exception as e: - print(f" ⚠️ Web scan manager failed to initialize: {e}") + print(f" Web scan manager failed to initialize: {e}") -print("✅ Core service initialization complete.") +print("Core service initialization complete.") # --- Automation Engine --- try: automation_engine = AutomationEngine(get_database()) - print("✅ Automation engine initialized.") + print("Automation engine initialized.") except Exception as e: - print(f"⚠️ Automation engine failed to initialize: {e}") + print(f"Automation engine failed to initialize: {e}") automation_engine = None def _register_automation_handlers(): @@ -781,7 +781,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)})") + print(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: @@ -797,7 +797,7 @@ def _register_automation_handlers(): except Exception: pass else: - print(f"⏭️ [AUTOMATION] No changes: '{pl.get('name', '')}' (tracks={len(old_ids)})") + print(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: @@ -1925,7 +1925,7 @@ def _register_automation_handlers(): }) web_scan_manager.add_scan_completion_callback(_on_library_scan_completed) - print("✅ Automation action handlers registered") + print("Automation action handlers registered") def _emit_track_downloaded(context): @@ -2109,9 +2109,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") + print("Public REST API v1 registered at /api/v1") except Exception as e: - print(f"⚠️ Public REST API v1 failed to register: {e}") + print(f"Public REST API v1 failed to register: {e}") # --- Global Streaming State Management --- # Thread-safe state tracking for streaming functionality @@ -2382,14 +2382,14 @@ def get_cached_transfer_data(): 'averageSpeed': download.speed, } except Exception as e: - print(f"⚠️ Could not fetch streaming source downloads: {e}") + print(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}") + print(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 {} @@ -2448,14 +2448,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)") + print(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)") return None except Exception as e: - print(f"⚠️ Cache lookup error for {section_type}/{data_key}: {e}") + print(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): @@ -2476,7 +2476,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)") + print(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']: @@ -2490,10 +2490,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}") + print(f"Cached {section_type}/{genre_slug}/{data_key}") except Exception as e: - print(f"⚠️ Cache storage error for {section_type}/{data_key}: {e}") + print(f"Cache storage error for {section_type}/{data_key}: {e}") def add_cache_headers(response, cache_duration=300): """ @@ -2525,14 +2525,14 @@ 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}") + print(f"Started download monitor for batch {batch_id}") def stop_monitoring(self, batch_id): """Stop monitoring a specific batch""" self.monitored_batches.discard(batch_id) if not self.monitored_batches: self.monitoring = False - print(f"🛑 Stopped download monitor (no active batches)") + print(f"Stopped download monitor (no active batches)") def _monitor_loop(self): """Main monitoring loop - checks downloads every 1 second for responsive web UX""" @@ -2543,12 +2543,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") + print(f"Monitor detected shutdown, stopping gracefully") self.monitoring = False break - print(f"❌ Download monitor error: {e}") + print(f"Download monitor error: {e}") - print(f"🔍 Download monitor loop ended") + print(f"Download monitor loop ended") def _check_all_downloads(self): """Check all active downloads for timeouts and failures""" @@ -2606,7 +2606,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") + print(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': @@ -2618,7 +2618,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") + print(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)) @@ -2630,21 +2630,21 @@ class WebUIDownloadMonitor: try: if op[0] == 'cancel_download': _, download_id, username = op - print(f"🚫 [Deferred] Cancelling download: {download_id} from {username}") + print(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}") + print(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}") + print(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}") + print(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}") + print(f"[Deferred] Successfully restarted worker for task {task_id}") except Exception as e: - print(f"⚠️ [Deferred] Error executing deferred operation {op[0]}: {e}") + print(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) @@ -2652,19 +2652,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}") + print(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}") + print(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}") + print(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}") + print(f"[Monitor] Error handling exhausted task {task_id}: {e}") # ENHANCED: Add worker count validation to detect ghost workers self._validate_worker_counts() @@ -2709,7 +2709,7 @@ class WebUIDownloadMonitor: 'averageSpeed': download.speed, } except Exception as yt_error: - print(f"⚠️ Monitor: Could not fetch streaming source downloads: {yt_error}") + print(f"Monitor: Could not fetch streaming source downloads: {yt_error}") return live_transfers except Exception as e: @@ -2717,11 +2717,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") + print(f"Monitor detected shutdown, stopping immediately") self.monitoring = False return {} else: - print(f"⚠️ Monitor: Could not fetch live transfers: {e}") + print(f"Monitor: Could not fetch live transfers: {e}") return {} def _should_retry_task(self, task_id, task, live_transfers_lookup, current_time, deferred_ops): @@ -2751,7 +2751,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") + print(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 @@ -2767,7 +2767,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}") + print(f"Marked missing-transfer source as used: {source_key}") # Defer orphan cleanup if task_username and task_filename: @@ -2782,7 +2782,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") + print(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: @@ -2794,7 +2794,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)") + print(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' @@ -2814,7 +2814,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") + print(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 @@ -2834,7 +2834,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}") + print(f"Marked errored source as used: {source_key}") # Defer orphan cleanup to outside the lock (needs matched_context_lock) if username and filename: @@ -2852,7 +2852,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") + print(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') @@ -2867,14 +2867,14 @@ 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") + print(f"Task failed after 3 error retry attempts") task['status'] = 'failed' task['error_message'] = f'Soulseek transfer errored 3 times for "{track_label}"{sources_str} — all sources failed or became unavailable' # 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}") + print(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 @@ -2899,7 +2899,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") + print(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 @@ -2920,7 +2920,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}") + print(f"Marked timeout source as used: {source_key}") # Defer orphan cleanup to outside the lock (needs matched_context_lock) if username and filename: @@ -2938,7 +2938,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") + print(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') @@ -2953,7 +2953,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)") + print(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 @@ -2963,7 +2963,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}") + print(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 @@ -2987,7 +2987,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") + print(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 @@ -3008,7 +3008,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}") + print(f"Marked 0% progress source as used: {source_key}") # Defer orphan cleanup to outside the lock (needs matched_context_lock) if username and filename: @@ -3026,7 +3026,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") + print(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') @@ -3040,7 +3040,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)") + print(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 @@ -3050,7 +3050,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}") + print(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: @@ -3077,7 +3077,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") + print(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 @@ -3094,7 +3094,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}") + print(f"Marked unknown-state source as used: {source_key}") if username and filename: old_context_key = _make_context_key(username, filename) @@ -3117,7 +3117,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}')") + print(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) @@ -3171,16 +3171,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)}") + print(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") + print(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}") + print(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): @@ -3189,13 +3189,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}") + print(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}") + print(f"[Worker Validation] Error starting workers for {batch_id}: {e}") except Exception as validation_error: - print(f"❌ Error in worker count validation: {validation_error}") + print(f"Error in worker count validation: {validation_error}") # Global download monitor instance download_monitor = WebUIDownloadMonitor() @@ -3233,7 +3233,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)") + print(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 @@ -3257,7 +3257,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}") + print(f"[Batch Healing] {batch_id}: fixing active count {active_count} → {actually_active}") batch_data['active_count'] = actually_active healed_batches.append(batch_id) @@ -3269,7 +3269,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}") + print(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) @@ -3282,31 +3282,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") + print(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}") + print(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}") + print(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}") + print(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") + print(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}") + print(f"[Batch Healing] Error checking completion for {batch_id}: {e}") except Exception as healing_error: - print(f"❌ [Batch Healing] Error during validation: {healing_error}") + print(f"[Batch Healing] Error during validation: {healing_error}") # Start periodic batch healing (every 30 seconds) import threading @@ -3315,7 +3315,7 @@ def start_batch_healing_timer(): try: validate_and_heal_batch_states() except Exception as e: - print(f"❌ [Batch Healing Timer] Error: {e}") + print(f"[Batch Healing Timer] Error: {e}") finally: # Schedule next healing cycle threading.Timer(30.0, start_batch_healing_timer).start() @@ -3331,7 +3331,7 @@ import sys def cleanup_monitor(): """Clean up background monitor on shutdown""" if download_monitor.monitoring: - print("🛑 Flask shutdown detected, stopping download monitor...") + print("Flask shutdown detected, stopping download monitor...") download_monitor.monitoring = False download_monitor.monitored_batches.clear() # Give the thread a moment to exit cleanly @@ -3340,7 +3340,7 @@ def cleanup_monitor(): # Clean up batch locks to prevent memory leaks with tasks_lock: batch_locks.clear() - print("🧹 Cleaned up batch locks") + print("Cleaned up batch locks") # Global shutdown flag IS_SHUTTING_DOWN = False @@ -3348,32 +3348,32 @@ IS_SHUTTING_DOWN = False def signal_handler(signum, frame): """Handle SIGINT (Ctrl+C) and SIGTERM""" global IS_SHUTTING_DOWN - print(f"🛑 Signal {signum} received, cleaning up...") + print(f"Signal {signum} received, cleaning up...") IS_SHUTTING_DOWN = True cleanup_monitor() # Stop automation engine try: if automation_engine: - print("🛑 Stopping automation engine...") + print("Stopping automation engine...") automation_engine.stop() except Exception as e: - print(f"⚠️ Error stopping automation engine: {e}") + print(f"Error stopping automation engine: {e}") # Persist API call history try: from core.api_call_tracker import api_call_tracker api_call_tracker.save() - print("💾 API call history saved") + print("API call history saved") except Exception as e: - print(f"⚠️ Error saving API call history: {e}") + print(f"Error saving API call history: {e}") # Shutdown executor to prevent new tasks try: - print("🛑 Shutting down missing_download_executor...") + print("Shutting down missing_download_executor...") missing_download_executor.shutdown(wait=False, cancel_futures=True) except Exception as e: - print(f"⚠️ Error shutting down executor: {e}") + print(f"Error shutting down executor: {e}") sys.exit(0) @@ -3402,20 +3402,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") + print(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}") + print(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}") + print(f"Error handling failed download {task_id}: {e}") task_status['status'] = 'failed' task['status'] = 'failed' @@ -3484,7 +3484,7 @@ def _prepare_stream_task(track_data): last_progress_sent = 0.0 try: - print(f"🎵 Starting stream preparation for: {track_data.get('filename')}") + print(f"Starting stream preparation for: {track_data.get('filename')}") # Update state to loading with stream_lock: @@ -3511,9 +3511,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}") + print(f"Cleared old stream file: {existing_file}") except Exception as e: - print(f"⚠️ Could not remove existing stream file: {e}") + print(f"Could not remove existing stream file: {e}") # Start the download using the same mechanism as regular downloads loop = asyncio.new_event_loop() @@ -3534,7 +3534,7 @@ def _prepare_stream_task(track_data): }) return - print(f"✓ Download initiated for streaming") + print(f"Download initiated for streaming") # Enhanced monitoring with queue timeout detection (matching GUI) max_wait_time = 60 # Increased timeout @@ -3574,13 +3574,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}") + print(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}") + print(f"Download started actively downloading: {original_state}") with stream_lock: stream_state["status"] = "loading" @@ -3604,7 +3604,7 @@ def _prepare_stream_task(track_data): # Check if download is complete if is_completed: - print(f"✓ Download completed via API status: {original_state}") + print(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) @@ -3634,7 +3634,7 @@ def _prepare_stream_task(track_data): found_file = _find_downloaded_file(download_path, track_data) if found_file: - print(f"✓ Found downloaded file: {found_file}") + print(f"Found downloaded file: {found_file}") # Move file to Stream folder original_filename = extract_filename(found_file) @@ -3642,7 +3642,7 @@ def _prepare_stream_task(track_data): try: shutil.move(found_file, stream_path) - print(f"✓ Moved file to stream folder: {stream_path}") + print(f"Moved file to stream folder: {stream_path}") # Clean up empty directories (matching GUI) _cleanup_empty_directories(download_path, found_file) @@ -3664,15 +3664,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") + print(f"Cleaned up download {download_id} from API") except Exception as e: - print(f"⚠️ Error cleaning up download: {e}") + print(f"Error cleaning up download: {e}") - print(f"✅ Stream file ready for playback: {stream_path}") + print(f"Stream file ready for playback: {stream_path}") return # Success! except Exception as e: - print(f"❌ Error moving file to stream folder: {e}") + print(f"Error moving file to stream folder: {e}") with stream_lock: stream_state.update({ "status": "error", @@ -3680,7 +3680,7 @@ def _prepare_stream_task(track_data): }) return else: - print("❌ Could not find downloaded file after completion") + print("Could not find downloaded file after completion") with stream_lock: stream_state.update({ "status": "error", @@ -3692,14 +3692,14 @@ def _prepare_stream_task(track_data): print(f"No transfer found in API yet... (elapsed: {wait_count * poll_interval}s)") except Exception as e: - print(f"⚠️ Error checking download progress: {e}") + print(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") + print(f"Download timed out after {max_wait_time}s") with stream_lock: stream_state.update({ "status": "error", @@ -3707,7 +3707,7 @@ def _prepare_stream_task(track_data): }) except asyncio.CancelledError: - print("🛑 Stream task cancelled") + print("Stream task cancelled") with stream_lock: stream_state.update({ "status": "stopped", @@ -3722,10 +3722,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}") + print(f"Error cleaning up streaming event loop: {e}") except Exception as e: - print(f"❌ Stream preparation failed: {e}") + print(f"Stream preparation failed: {e}") with stream_lock: stream_state.update({ "status": "error", @@ -3788,15 +3788,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}") + print(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}") + print(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}") + print(f"[Stream] Using direct filename: {target_filename_youtube}") try: # Walk through the downloads directory to find the file @@ -3830,7 +3830,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}") + print(f"[{source_label} Stream] Comparing: '{file}' vs '{target_filename_youtube}' = {similarity:.2f}") # Keep track of best match if similarity > best_similarity: @@ -3839,21 +3839,21 @@ 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}") + print(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}") + print(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}") + print(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}") + print(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}") @@ -4132,7 +4132,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.") + print(f"Restored original config for '{service}' after test.") def run_detection(server_type): @@ -4716,7 +4716,7 @@ def save_playlist_m3u(): with open(m3u_path, 'w', encoding='utf-8') as f: f.write(m3u_content) - logger.info(f"✅ Saved M3U file: {m3u_path}") + logger.info(f"Saved M3U file: {m3u_path}") return jsonify({ "status": "success", "message": f"M3U file saved: {m3u_filename}", @@ -4724,7 +4724,7 @@ def save_playlist_m3u(): }) except Exception as e: - logger.error(f"❌ Error saving M3U file: {e}") + logger.error(f"Error saving M3U file: {e}") return jsonify({"status": "error", "message": str(e)}), 500 def _build_system_stats(): @@ -5134,7 +5134,7 @@ def add_activity_item(icon: str, title: str, subtitle: str, time_ago: str = "Now except Exception: pass - print(f"📝 Activity: {icon} {title} - {subtitle}") + print(f"Activity: {icon} {title} - {subtitle}") except Exception as e: print(f"Error adding activity item: {e}") @@ -5204,14 +5204,14 @@ 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.") + print("Settings saved successfully via Web UI.") # Add activity for settings save changed_services = list(new_settings.keys()) services_text = ", ".join(changed_services) - add_activity_item("⚙️", "Settings Updated", f"{services_text} configuration saved", "Now") + add_activity_item("", "Settings Updated", f"{services_text} configuration saved", "Now") - add_activity_item("⚙️", "Settings Updated", f"{services_text} configuration saved", "Now") + add_activity_item("", "Settings Updated", f"{services_text} configuration saved", "Now") # Reload service clients with new settings (guard against None from partial init) if spotify_client: @@ -5242,7 +5242,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.") + print("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 @@ -5267,7 +5267,7 @@ def handle_dev_mode(): data = request.get_json() if data.get('password') == 'hydratest': dev_mode_enabled = True - print("🔧 Dev mode activated") + print("Dev mode activated") return jsonify({"success": True, "enabled": True}) return jsonify({"success": False, "error": "Invalid password"}), 401 return jsonify({"enabled": dev_mode_enabled}) @@ -5396,10 +5396,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}") + print(f"[Hydrabase] Connected to {url}") return jsonify({"success": True, "message": "Connected"}) except Exception as e: - print(f"⚠️ [Hydrabase] Connection failed: {e}") + print(f"[Hydrabase] Connection failed: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/hydrabase/disconnect', methods=['POST']) @@ -5417,7 +5417,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") + print("[Hydrabase] Disconnected") return jsonify({"success": True}) @app.route('/api/hydrabase/status') @@ -5475,10 +5475,10 @@ def hydrabase_send(): result = json.loads(response) except json.JSONDecodeError: result = response - print(f"🧪 [Hydrabase] Sent payload — got response") + print(f"[Hydrabase] Sent payload — got response") return jsonify({"success": True, "data": result}) except Exception as e: - print(f"⚠️ [Hydrabase] Send failed: {e}") + print(f"[Hydrabase] Send failed: {e}") with _hydrabase_lock: try: _hydrabase_ws.close() @@ -5510,7 +5510,7 @@ def handle_log_level(): db.set_preference('log_level', level.upper()) logger.info(f"Log level changed to {level.upper()} via Web UI") - add_activity_item("🔍", "Log Level Changed", f"Set to {level.upper()}", "Now") + add_activity_item("", "Log Level Changed", f"Set to {level.upper()}", "Now") return jsonify({"success": True, "level": level.upper()}) else: @@ -6141,24 +6141,24 @@ 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") + print("Updated Spotify status cache after successful test") elif service in ['plex', 'jellyfin', 'navidrome']: _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") + print(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") + print("Updated Soulseek status cache after successful test") elif service == 'listenbrainz': - print("✅ ListenBrainz test successful") + print("ListenBrainz test successful") # Add activity for connection test if success: - add_activity_item("✅", "Connection Test", message, "Now") + add_activity_item("", "Connection Test", message, "Now") else: - add_activity_item("❌", "Connection Test", f"{service.title()} connection failed", "Now") + add_activity_item("", "Connection Test", f"{service.title()} connection failed", "Now") return jsonify({"success": success, "error": "" if success else message, "message": message if success else ""}) @@ -6191,22 +6191,22 @@ 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") + print("Updated Spotify status cache after successful dashboard test") elif service in ['plex', 'jellyfin', 'navidrome']: _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") + print(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") + print("Updated Soulseek status cache after successful dashboard test") # Add activity for dashboard connection test (different from settings test) if success: - add_activity_item("🎛️", "Dashboard Test", message, "Now") + add_activity_item("", "Dashboard Test", message, "Now") else: - add_activity_item("⚠️", "Dashboard Test", f"{service.title()} service check failed", "Now") + add_activity_item("", "Dashboard Test", f"{service.title()} service check failed", "Now") return jsonify({"success": success, "error": "" if success else message, "message": message if success else ""}) @@ -6217,14 +6217,14 @@ def detect_media_server_endpoint(): print(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") + add_activity_item("", "Auto-Detect Started", f"Searching for {server_type} server", "Now") found_url = run_detection(server_type) if found_url: - add_activity_item("✅", "Auto-Detect Complete", f"{server_type} found at {found_url}", "Now") + add_activity_item("", "Auto-Detect Complete", f"{server_type} found at {found_url}", "Now") return jsonify({"success": True, "found_url": found_url}) else: - add_activity_item("❌", "Auto-Detect Failed", f"No {server_type} server found", "Now") + add_activity_item("", "Auto-Detect Failed", f"No {server_type} server found", "Now") return jsonify({"success": False, "error": f"No {server_type} server found on common local addresses."}) @app.route('/api/plex/music-libraries', methods=['GET']) @@ -6266,7 +6266,7 @@ def select_plex_music_library(): success = plex_client.set_music_library_by_name(library_name) if success: - add_activity_item("📚", "Library Selected", f"Plex music library set to: {library_name}", "Now") + add_activity_item("", "Library Selected", f"Plex music library set to: {library_name}", "Now") return jsonify({"success": True, "message": f"Music library set to: {library_name}"}) else: return jsonify({"success": False, "error": f"Library '{library_name}' not found"}), 404 @@ -6317,7 +6317,7 @@ def select_jellyfin_user(): success = jellyfin_client.set_user_by_name(username) if success: - add_activity_item("👤", "User Selected", f"Jellyfin user set to: {username}", "Now") + add_activity_item("", "User Selected", f"Jellyfin user set to: {username}", "Now") return jsonify({"success": True, "message": f"User set to: {username}"}) else: return jsonify({"success": False, "error": f"User '{username}' not found or has no music library"}), 404 @@ -6369,7 +6369,7 @@ def select_jellyfin_music_library(): success = jellyfin_client.set_music_library_by_name(library_name) if success: - add_activity_item("📚", "Library Selected", f"Jellyfin music library set to: {library_name}", "Now") + add_activity_item("", "Library Selected", f"Jellyfin music library set to: {library_name}", "Now") return jsonify({"success": True, "message": f"Music library set to: {library_name}"}) else: return jsonify({"success": False, "error": f"Library '{library_name}' not found"}), 404 @@ -6419,10 +6419,10 @@ def select_navidrome_music_folder(): if success: if folder_name: - add_activity_item("📚", "Library Selected", f"Navidrome music folder set to: {folder_name}", "Now") + add_activity_item("", "Library Selected", f"Navidrome music folder set to: {folder_name}", "Now") return jsonify({"success": True, "message": f"Music folder set to: {folder_name}"}) else: - add_activity_item("📚", "Library Selection Cleared", "Navidrome will use all libraries", "Now") + add_activity_item("", "Library Selection Cleared", "Navidrome will use all libraries", "Now") return jsonify({"success": True, "message": "Music folder selection cleared — using all libraries"}) else: return jsonify({"success": False, "error": f"Folder '{folder_name}' not found"}), 404 @@ -6465,7 +6465,7 @@ def save_quality_profile(): success = db.set_quality_profile(data) if success: - add_activity_item("🎵", "Quality Profile Updated", f"Preset: {data.get('preset', 'custom')}", "Now") + add_activity_item("", "Quality Profile Updated", f"Preset: {data.get('preset', 'custom')}", "Now") return jsonify({"success": True, "message": "Quality profile saved successfully"}) else: return jsonify({"success": False, "error": "Failed to save quality profile"}), 500 @@ -6506,7 +6506,7 @@ def apply_quality_preset(preset_name): success = db.set_quality_profile(preset) if success: - add_activity_item("🎵", "Quality Preset Applied", f"Applied '{preset_name}' preset", "Now") + add_activity_item("", "Quality Preset Applied", f"Applied '{preset_name}' preset", "Now") return jsonify({ "success": True, "message": f"Applied '{preset_name}' preset", @@ -6528,13 +6528,13 @@ def detect_soulseek_endpoint(): print("Received auto-detect request for slskd") # Add activity for soulseek auto-detect start - add_activity_item("🔍", "Auto-Detect Started", "Searching for slskd server", "Now") + add_activity_item("", "Auto-Detect Started", "Searching for slskd server", "Now") found_url = run_detection('slskd') if found_url: - add_activity_item("✅", "Auto-Detect Complete", f"slskd found at {found_url}", "Now") + add_activity_item("", "Auto-Detect Complete", f"slskd found at {found_url}", "Now") return jsonify({"success": True, "found_url": found_url}) else: - add_activity_item("❌", "Auto-Detect Failed", "No slskd server found", "Now") + add_activity_item("", "Auto-Detect Failed", "No slskd server found", "Now") return jsonify({"success": False, "error": "No slskd server found on common local addresses."}) # --- Authentication Routes --- @@ -6566,10 +6566,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}") + print(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}") + print(f"Per-profile Spotify auth failed, falling back to global: {e}") # Global auth (admin or fallback) temp_spotify_client = SpotifyClient() @@ -6577,8 +6577,8 @@ 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}") - add_activity_item("🔐", "Spotify Auth Started", "Please complete OAuth in browser", "Now") + print(f"Spotify auth initiated — redirect_uri: {configured_uri}") + add_activity_item("", "Spotify Auth Started", "Please complete OAuth in browser", "Now") # Detect if accessing remotely host = request.host.split(':')[0] @@ -6607,7 +6607,7 @@ def auth_spotify(): -

    🔐 Spotify Authentication

    +

    Spotify Authentication

    Click the link below to authenticate with Spotify:

    Authenticate with Spotify

    @@ -6643,7 +6643,7 @@ def auth_spotify(): -

    🔐 Spotify Authentication (Remote/Docker)

    +

    Spotify Authentication (Remote/Docker)

    Using a reverse proxy? Your redirect URI is set to {configured_uri} @@ -6683,26 +6683,26 @@ def auth_spotify(): ''' else: # Local access - simple message - return f'

    🔐 Spotify Authentication

    Click the link below to authenticate:

    {auth_url}

    After authentication, return to the app.

    ' + return f'

    Spotify Authentication

    Click the link below to authenticate:

    {auth_url}

    After authentication, return to the app.

    ' else: - return "

    ❌ Spotify Authentication Failed

    Could not initialize Spotify client. Check your credentials.

    ", 400 + return "

    Spotify Authentication Failed

    Could not initialize Spotify client. Check your credentials.

    ", 400 except Exception as e: - print(f"🔴 Error starting Spotify auth: {e}") - return f"

    ❌ Spotify Authentication Error

    {str(e)}

    ", 500 + print(f"Error starting Spotify auth: {e}") + return f"

    Spotify Authentication Error

    {str(e)}

    ", 500 @app.route('/auth/tidal') def auth_tidal(): """ Initiates Tidal OAuth authentication flow """ - print("🔐🔐🔐 TIDAL AUTH ROUTE CALLED 🔐🔐🔐") + print("TIDAL AUTH ROUTE CALLED ") try: # Create a fresh tidal client to get OAuth URL from core.tidal_client import TidalClient temp_tidal_client = TidalClient() if not temp_tidal_client.client_id: - return "

    ❌ Tidal Authentication Failed

    Tidal client ID not configured. Check your credentials.

    ", 400 + return "

    Tidal Authentication Failed

    Tidal client ID not configured. Check your credentials.

    ", 400 # Generate PKCE challenge and store globally temp_tidal_client._generate_pkce_challenge() @@ -6718,20 +6718,20 @@ def auth_tidal(): configured_redirect = config_manager.get('tidal.redirect_uri', '') if configured_redirect: temp_tidal_client.redirect_uri = configured_redirect - print(f"🔗 Using configured Tidal redirect_uri: {configured_redirect}") + print(f"Using configured Tidal redirect_uri: {configured_redirect}") else: # Fallback: dynamically set based on request host (non-Docker local access) request_host = request.host.split(':')[0] if request_host not in ('127.0.0.1', 'localhost'): dynamic_redirect = f"http://{request_host}:8889/tidal/callback" temp_tidal_client.redirect_uri = dynamic_redirect - print(f"🔗 Tidal redirect_uri set from request host: {dynamic_redirect}") + print(f"Tidal redirect_uri set from request host: {dynamic_redirect}") # Store PKCE + redirect_uri for callback to use the same values with tidal_oauth_lock: tidal_oauth_state["redirect_uri"] = temp_tidal_client.redirect_uri - print(f"🔐 Stored PKCE - verifier: {temp_tidal_client.code_verifier[:20]}... challenge: {temp_tidal_client.code_challenge[:20]}...") + print(f"Stored PKCE - verifier: {temp_tidal_client.code_verifier[:20]}... challenge: {temp_tidal_client.code_challenge[:20]}...") # Store profile_id for per-profile auth profile_id = request.args.get('profile_id', '') @@ -6751,10 +6751,10 @@ def auth_tidal(): auth_url = f"{temp_tidal_client.auth_url}?" + urllib.parse.urlencode(params) - print(f"🔗 Generated Tidal OAuth URL: {auth_url}") - print(f"🔗 Redirect URI in URL: {params['redirect_uri']}") + print(f"Generated Tidal OAuth URL: {auth_url}") + print(f"Redirect URI in URL: {params['redirect_uri']}") - add_activity_item("🔐", "Tidal Auth Started", "Please complete OAuth in browser", "Now") + add_activity_item("", "Tidal Auth Started", "Please complete OAuth in browser", "Now") # Detect if accessing remotely (copied from Spotify auth logic) host = request.host.split(':')[0] @@ -6767,7 +6767,7 @@ def auth_tidal(): if is_remote or is_docker: # Show instructions for remote/docker access - page_title = "🔐 Tidal Authentication (Remote/Docker)" + page_title = "Tidal Authentication (Remote/Docker)" step_1_text = "Click the link below to authenticate with Tidal" return f''' @@ -6808,7 +6808,7 @@ def auth_tidal(): function copyIP() {{ navigator.clipboard.writeText('{host}').then(() => {{ const btn = event.target; - btn.textContent = '✓ Copied!'; + btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(() => {{ btn.textContent = 'Copy IP'; @@ -6821,13 +6821,13 @@ def auth_tidal(): ''' else: - return f'

    🔐 Tidal Authentication

    Please visit this URL to authenticate:

    {auth_url}

    After authentication, return to the app.

    ' + return f'

    Tidal Authentication

    Please visit this URL to authenticate:

    {auth_url}

    After authentication, return to the app.

    ' except Exception as e: - print(f"🔴 Error starting Tidal auth: {e}") + print(f"Error starting Tidal auth: {e}") import traceback - print(f"🔴 Full traceback: {traceback.format_exc()}") - return f"

    ❌ Tidal Authentication Error

    {str(e)}

    ", 500 + print(f"Full traceback: {traceback.format_exc()}") + return f"

    Tidal Authentication Error

    {str(e)}

    ", 500 @app.route('/callback') @@ -6843,19 +6843,19 @@ def spotify_callback(): if not auth_code: error = request.args.get('error') if error: - print(f"🔴 Spotify OAuth error on port 8008: Spotify returned error: {error}") - add_activity_item("❌", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now") + print(f"Spotify OAuth error on port 8008: Spotify returned error: {error}") + add_activity_item("", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now") return f"

    Spotify Authentication Failed

    Spotify returned error: {error}

    ", 400 # No code AND no error — check if query params were stripped if request.args: - print(f"🔴 Spotify callback on port 8008 received unexpected params: {dict(request.args)}") + print(f"Spotify callback on port 8008 received unexpected params: {dict(request.args)}") else: # Completely empty — likely a healthcheck or spurious request pass return '', 204 - print(f"🎵 Spotify callback received on port 8008 with authorization code") + print(f"Spotify callback received on port 8008 with authorization code") # Check for per-profile state parameter state = request.args.get('state', '') @@ -6863,7 +6863,7 @@ def spotify_callback(): if state and state.startswith('profile_'): try: profile_id_from_state = int(state.replace('profile_', '')) - print(f"🎵 Per-profile callback detected for profile {profile_id_from_state}") + print(f"Per-profile callback detected for profile {profile_id_from_state}") except ValueError: pass @@ -6891,7 +6891,7 @@ def spotify_callback(): # Invalidate cached profile client so it gets recreated with new tokens with _profile_spotify_lock: _profile_spotify_clients.pop(profile_id_from_state, None) - add_activity_item("✅", "Spotify Auth Complete", f"Profile {profile_id_from_state} authenticated with Spotify", "Now") + add_activity_item("", "Spotify Auth Complete", f"Profile {profile_id_from_state} authenticated with Spotify", "Now") return "

    Spotify Authentication Successful!

    Your personal Spotify account is now connected. You can close this window.

    " else: raise Exception("Failed to exchange authorization code for access token") @@ -6899,7 +6899,7 @@ def spotify_callback(): # Global callback (admin) config = config_manager.get_spotify_config() configured_uri = config.get('redirect_uri', "http://127.0.0.1:8888/callback") - print(f"🎵 Using redirect_uri for token exchange: {configured_uri}") + print(f"Using redirect_uri for token exchange: {configured_uri}") auth_manager = SpotifyOAuth( client_id=config['client_id'], @@ -6927,15 +6927,15 @@ def spotify_callback(): if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'): spotify_enrichment_worker.client.reload_config() spotify_enrichment_worker.client._invalidate_auth_cache() - add_activity_item("✅", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now") + add_activity_item("", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now") return "

    Spotify Authentication Successful!

    You can close this window.

    " else: raise Exception("Token exchange succeeded but authentication validation failed") else: raise Exception("Failed to exchange authorization code for access token") except Exception as e: - print(f"🔴 Spotify OAuth callback error on port 8008: {e}") - add_activity_item("❌", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now") + print(f"Spotify OAuth callback error on port 8008: {e}") + add_activity_item("", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now") return f"

    Spotify Authentication Failed

    {str(e)}

    ", 400 @@ -6959,7 +6959,7 @@ def spotify_disconnect(): } _status_cache_timestamps['spotify'] = time.time() fallback_label = 'Deezer' if fallback_src == 'deezer' else 'Discogs' if fallback_src == 'discogs' else 'iTunes' - add_activity_item("🔌", "Spotify Disconnected", f"Switched to {fallback_label} metadata source", "Now") + add_activity_item("", "Spotify Disconnected", f"Switched to {fallback_label} metadata source", "Now") return jsonify({'success': True, 'message': f'Spotify disconnected. Now using {fallback_label}.'}) except Exception as e: logger.error(f"Error disconnecting Spotify: {e}") @@ -7034,21 +7034,21 @@ def tidal_callback(): WHERE id = ? """, (enc_access, enc_refresh, profile_id_int)) conn.commit() - add_activity_item("✅", "Tidal Auth Complete", f"Profile {profile_id_int} authenticated with Tidal", "Now") - return "

    ✅ Tidal Authentication Successful!

    Your personal Tidal account is now connected. You can close this window.

    " + add_activity_item("", "Tidal Auth Complete", f"Profile {profile_id_int} authenticated with Tidal", "Now") + return "

    Tidal Authentication Successful!

    Your personal Tidal account is now connected. You can close this window.

    " except Exception as profile_err: - print(f"⚠️ Per-profile Tidal auth failed, falling back to global: {profile_err}") + print(f"Per-profile Tidal auth failed, falling back to global: {profile_err}") # Global: Re-initialize the main global tidal_client instance with the new token tidal_client = TidalClient() if tidal_enrichment_worker: tidal_enrichment_worker.client = tidal_client - return "

    ✅ Tidal Authentication Successful!

    You can now close this window and return to the SoulSync application.

    " + return "

    Tidal Authentication Successful!

    You can now close this window and return to the SoulSync application.

    " else: - return "

    ❌ Tidal Authentication Failed

    Could not exchange authorization code for a token. Please try again.

    ", 400 + return "

    Tidal Authentication Failed

    Could not exchange authorization code for a token. Please try again.

    ", 400 except Exception as e: - print(f"🔴 Error during Tidal token exchange: {e}") - return f"

    ❌ An Error Occurred

    An unexpected error occurred during the authentication process: {e}

    ", 500 + print(f"Error during Tidal token exchange: {e}") + return f"

    An Error Occurred

    An unexpected error occurred during the authentication process: {e}

    ", 500 # --- Deezer OAuth --- @@ -7070,7 +7070,7 @@ def auth_deezer(): host = request.host.split(':')[0] return f""" -

    🎵 Deezer Authorization

    +

    Deezer Authorization

    Click the link below to authorize SoulSync with your Deezer account:

    Authorize on Deezer →


    @@ -7127,12 +7127,12 @@ def deezer_callback(): deezer_client = _get_deezer_client() deezer_client.reload_config() - add_activity_item("✅", "Deezer Auth Complete", "Deezer account connected via OAuth", "Now") + add_activity_item("", "Deezer Auth Complete", "Deezer account connected via OAuth", "Now") logger.info("Deezer OAuth authentication successful") return """ -

    ✅ Deezer Authentication Successful!

    +

    Deezer Authentication Successful!

    Your Deezer account is now connected. You can close this window.

    """ @@ -7147,17 +7147,17 @@ def deezer_callback(): def get_beatport_hero_tracks(): """Get fresh tracks from Beatport hero slideshow for the rebuild slider""" try: - logger.info("🎯 Fetching Beatport hero tracks...") + logger.info("Fetching Beatport hero tracks...") # Check cache first cached_data = get_cached_beatport_data('homepage', 'hero_tracks') if cached_data: - logger.info("🎯 Returning cached hero tracks data") + logger.info("Returning cached hero tracks data") response = jsonify(cached_data) return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh hero tracks data...") + logger.info("Cache miss - scraping fresh hero tracks data...") # Initialize scraper scraper = BeatportUnifiedScraper() @@ -7169,7 +7169,7 @@ def get_beatport_hero_tracks(): valid_tracks = [] seen_urls = set() - logger.info(f"🔍 Processing {len(tracks)} raw tracks from scraper (SMART FILTERING)...") + logger.info(f"Processing {len(tracks)} raw tracks from scraper (SMART FILTERING)...") for i, track in enumerate(tracks): logger.info(f" Track {i+1}: {track.get('title', 'NO_TITLE')} - {track.get('artist', 'NO_ARTIST')}") @@ -7220,7 +7220,7 @@ def get_beatport_hero_tracks(): skip_reasons.append("Duplicate URL") if not is_valid: - logger.info(f" ❌ Track {i+1} filtered out: {', '.join(skip_reasons)}") + logger.info(f" Track {i+1} filtered out: {', '.join(skip_reasons)}") continue # Mark URL as seen for deduplication @@ -7263,9 +7263,9 @@ def get_beatport_hero_tracks(): break valid_tracks.append(track_data) - logger.info(f" ✅ Track {i+1} added: {title} - {artist}") + logger.info(f" Track {i+1} added: {title} - {artist}") - logger.info(f"✅ Retrieved {len(valid_tracks)} valid unique Beatport tracks (SMART FILTERING)") + logger.info(f"Retrieved {len(valid_tracks)} valid unique Beatport tracks (SMART FILTERING)") # Prepare response data response_data = { @@ -7282,7 +7282,7 @@ def get_beatport_hero_tracks(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error fetching Beatport tracks: {str(e)}") + logger.error(f"Error fetching Beatport tracks: {str(e)}") return jsonify({ 'success': False, 'error': str(e), @@ -7303,7 +7303,7 @@ def get_beatport_new_releases(): return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh new releases data...") + logger.info("Cache miss - scraping fresh new releases data...") # Initialize scraper scraper = BeatportUnifiedScraper() @@ -7331,12 +7331,12 @@ def get_beatport_new_releases(): if releases_container: release_cards = releases_container.select('[class*="ReleaseCard-style__Wrapper"]') else: - logger.warning("⚠️ No New Releases GridSlider found, trying page-wide ReleaseCard search") + logger.warning("No New Releases GridSlider found, trying page-wide ReleaseCard search") release_cards = soup.select('[class*="ReleaseCard-style__Wrapper"]') releases = [] - logger.info(f"🔍 Found {len(release_cards)} release cards") + logger.info(f"Found {len(release_cards)} release cards") for i, card in enumerate(release_cards[:100]): # Limit to 100 for 10 slides release_data = {} @@ -7395,7 +7395,7 @@ def get_beatport_new_releases(): releases.append(release_data) - logger.info(f"✅ Successfully extracted {len(releases)} new releases") + logger.info(f"Successfully extracted {len(releases)} new releases") # Prepare response data response_data = { @@ -7413,7 +7413,7 @@ def get_beatport_new_releases(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error fetching new releases: {str(e)}") + logger.error(f"Error fetching new releases: {str(e)}") return jsonify({ 'success': False, 'error': str(e), @@ -7425,17 +7425,17 @@ def get_beatport_new_releases(): def get_beatport_featured_charts(): """Get featured charts from Beatport for the charts slider grid using GridSlider approach""" try: - logger.info("🔥 Fetching Beatport featured charts...") + logger.info("Fetching Beatport featured charts...") # Check cache first cached_data = get_cached_beatport_data('homepage', 'featured_charts') if cached_data: - logger.info("🔥 Returning cached featured charts data") + logger.info("Returning cached featured charts data") response = jsonify(cached_data) return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh featured charts data...") + logger.info("Cache miss - scraping fresh featured charts data...") # Initialize scraper scraper = BeatportUnifiedScraper() @@ -7449,21 +7449,21 @@ def get_beatport_featured_charts(): gridsliders = soup.select('[class*="GridSlider-style__Wrapper"]') featured_container = None - logger.info(f"🔍 Checking {len(gridsliders)} GridSlider containers for featured charts...") + logger.info(f"Checking {len(gridsliders)} GridSlider containers for featured charts...") for container in gridsliders: h2 = container.select_one('h2') if h2: title = h2.get_text(strip=True).lower() - logger.info(f"📋 Found section: '{h2.get_text(strip=True)}'") + logger.info(f"Found section: '{h2.get_text(strip=True)}'") if 'featured' in title and 'chart' in title: featured_container = container - logger.info(f"🔥 FOUND FEATURED CHARTS: '{h2.get_text(strip=True)}'") + logger.info(f"FOUND FEATURED CHARTS: '{h2.get_text(strip=True)}'") break if not featured_container: - logger.warning("❌ No Featured Charts GridSlider container found") + logger.warning("No Featured Charts GridSlider container found") return jsonify({ 'success': False, 'error': 'Featured Charts section not found', @@ -7474,7 +7474,7 @@ def get_beatport_featured_charts(): charts = [] chart_links = featured_container.select('a[href*="/chart/"]') - logger.info(f"📊 Found {len(chart_links)} chart links in Featured Charts section") + logger.info(f"Found {len(chart_links)} chart links in Featured Charts section") for i, link in enumerate(chart_links[:100]): # Limit to 100 for 10 slides chart_data = {} @@ -7542,9 +7542,9 @@ def get_beatport_featured_charts(): # Only add if we have meaningful data if 'name' in chart_data and 'url' in chart_data: charts.append(chart_data) - logger.info(f"✅ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}") + logger.info(f"Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}") - logger.info(f"📊 Successfully extracted {len(charts)} featured charts") + logger.info(f"Successfully extracted {len(charts)} featured charts") # Prepare response data response_data = { @@ -7562,7 +7562,7 @@ def get_beatport_featured_charts(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error fetching featured charts: {str(e)}") + logger.error(f"Error fetching featured charts: {str(e)}") return jsonify({ 'success': False, 'error': str(e), @@ -7574,17 +7574,17 @@ def get_beatport_featured_charts(): def get_beatport_dj_charts(): """Get DJ charts from Beatport for the DJ charts slider using Carousel approach""" try: - logger.info("🎧 Fetching Beatport DJ charts...") + logger.info("Fetching Beatport DJ charts...") # Check cache first cached_data = get_cached_beatport_data('homepage', 'dj_charts') if cached_data: - logger.info("🎧 Returning cached DJ charts data") + logger.info("Returning cached DJ charts data") response = jsonify(cached_data) return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh DJ charts data...") + logger.info("Cache miss - scraping fresh DJ charts data...") # Initialize scraper scraper = BeatportUnifiedScraper() @@ -7598,21 +7598,21 @@ def get_beatport_dj_charts(): carousels = soup.select('[class*="Carousel-style__Wrapper"]') dj_container = None - logger.info(f"🔍 Checking {len(carousels)} Carousel containers for DJ charts...") + logger.info(f"Checking {len(carousels)} Carousel containers for DJ charts...") # Based on test results, DJ charts are in the second carousel (index 1) with ~9 chart links for i, container in enumerate(carousels): chart_links = container.select('a[href*="/chart/"]') - logger.info(f"📋 Carousel {i+1}: {len(chart_links)} chart links") + logger.info(f"Carousel {i+1}: {len(chart_links)} chart links") # DJ charts container typically has 8-12 chart links (not 99+ like featured charts) if 5 <= len(chart_links) <= 15: dj_container = container - logger.info(f"🔥 FOUND DJ CHARTS: Carousel {i+1} with {len(chart_links)} charts") + logger.info(f"FOUND DJ CHARTS: Carousel {i+1} with {len(chart_links)} charts") break if not dj_container: - logger.warning("❌ No DJ Charts Carousel container found") + logger.warning("No DJ Charts Carousel container found") return jsonify({ 'success': False, 'error': 'DJ Charts section not found', @@ -7623,7 +7623,7 @@ def get_beatport_dj_charts(): charts = [] chart_links = dj_container.select('a[href*="/chart/"]') - logger.info(f"📊 Found {len(chart_links)} DJ chart links") + logger.info(f"Found {len(chart_links)} DJ chart links") for i, link in enumerate(chart_links): chart_data = {} @@ -7682,9 +7682,9 @@ def get_beatport_dj_charts(): # Only add if we have meaningful data if 'name' in chart_data and 'url' in chart_data: charts.append(chart_data) - logger.info(f"✅ DJ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}") + logger.info(f"DJ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}") - logger.info(f"📊 Successfully extracted {len(charts)} DJ charts") + logger.info(f"Successfully extracted {len(charts)} DJ charts") # Prepare response data response_data = { @@ -7702,7 +7702,7 @@ def get_beatport_dj_charts(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error fetching DJ charts: {str(e)}") + logger.error(f"Error fetching DJ charts: {str(e)}") return jsonify({ 'success': False, 'error': str(e), @@ -7749,7 +7749,7 @@ def search_music(): logger.info(f"Web UI Search initiated for: '{query}'") # Add activity for search start - add_activity_item("🔍", "Search Started", f"'{query}'", "Now") + add_activity_item("", "Search Started", f"'{query}'", "Now") try: tracks, albums = run_async(soulseek_client.search(query)) @@ -7773,7 +7773,7 @@ def search_music(): # Add activity for search completion total_results = len(all_results) - add_activity_item("✅", "Search Complete", f"'{query}' - {total_results} results", "Now") + add_activity_item("", "Search Complete", f"'{query}' - {total_results} results", "Now") return jsonify({"results": all_results}) @@ -8210,7 +8210,7 @@ def stream_enhanced_search_track(): if not track_name or not artist_name: return jsonify({"error": "Track name and artist name are required"}), 400 - logger.info(f"▶️ Enhanced search stream request: '{track_name}' by '{artist_name}'") + logger.info(f"Enhanced search stream request: '{track_name}' by '{artist_name}'") try: # Create a temporary SpotifyTrack-like object for the matching engine @@ -8235,13 +8235,13 @@ def stream_enhanced_search_track(): _hybrid_first = _hybrid_order[0] if _hybrid_order else config_manager.get('download_source.hybrid_primary', 'hifi') if download_mode == 'soulseek' or (download_mode == 'hybrid' and _hybrid_first == 'soulseek'): effective_mode = 'youtube' # Soulseek is too slow for streaming preview - logger.info("▶️ Stream source is 'active' but primary is Soulseek — falling back to YouTube") + logger.info("Stream source is 'active' but primary is Soulseek — falling back to YouTube") elif download_mode == 'hybrid': effective_mode = _hybrid_first else: effective_mode = download_mode - logger.info(f"▶️ Stream source: {stream_source} → effective: {effective_mode}") + logger.info(f"Stream source: {stream_source} → effective: {effective_mode}") # Generate search queries based on effective stream mode search_queries = [] @@ -8258,7 +8258,7 @@ def stream_enhanced_search_track(): if cleaned_name and cleaned_name.lower() != track_name.lower(): search_queries.append(f"{artist_name} {cleaned_name}".strip()) - logger.info(f"🔍 {effective_mode.title()} stream: Searching with artist + track name: {search_queries}") + logger.info(f"{effective_mode.title()} stream: Searching with artist + track name: {search_queries}") else: # Soulseek mode: Track name only to avoid keyword filtering if track_name.strip(): @@ -8270,7 +8270,7 @@ def stream_enhanced_search_track(): if cleaned_name and cleaned_name.lower() != track_name.lower(): search_queries.append(cleaned_name.strip()) - logger.info(f"🔍 Soulseek mode: Searching by track name only (will match with artist): {search_queries}") + logger.info(f"Soulseek mode: Searching by track name only (will match with artist): {search_queries}") # Remove duplicates while preserving order unique_queries = [] @@ -8296,7 +8296,7 @@ def stream_enhanced_search_track(): # Try queries sequentially until we find a good match for query_index, query in enumerate(search_queries): - logger.info(f"🔍 Query {query_index + 1}/{len(search_queries)}: '{query}'") + logger.info(f"Query {query_index + 1}/{len(search_queries)}: '{query}'") try: # Search using the stream source client (not the download source) @@ -8306,7 +8306,7 @@ def stream_enhanced_search_track(): tracks_result, _ = run_async(soulseek_client.search(query, timeout=15)) if tracks_result: - logger.info(f"✅ Found {len(tracks_result)} results for query: '{query}'") + logger.info(f"Found {len(tracks_result)} results for query: '{query}'") # Use matching engine to find best match _max_q = config_manager.get('soulseek.max_peer_queue', 0) or 0 @@ -8330,30 +8330,30 @@ def stream_enhanced_search_track(): "result_type": "track" } - logger.info(f"✅ Returning best match from query '{query}': {best_result.filename} ({best_result.quality})") + logger.info(f"Returning best match from query '{query}': {best_result.filename} ({best_result.quality})") return jsonify({ "success": True, "result": result_dict }) else: - logger.info(f"⏭️ No suitable matches for query '{query}', trying next query...") + logger.info(f"No suitable matches for query '{query}', trying next query...") else: - logger.info(f"⏭️ No results for query '{query}', trying next query...") + logger.info(f"No results for query '{query}', trying next query...") except Exception as search_error: - logger.warning(f"⚠️ Error searching with query '{query}': {search_error}") + logger.warning(f"Error searching with query '{query}': {search_error}") continue # If we get here, none of the queries found a suitable match - logger.warning(f"❌ No suitable matches found after trying {len(search_queries)} queries") + logger.warning(f"No suitable matches found after trying {len(search_queries)} queries") return jsonify({ "success": False, "error": "No suitable track found after trying multiple search strategies" }), 404 except Exception as e: - logger.error(f"❌ Error streaming enhanced search track: {e}", exc_info=True) + logger.error(f"Error streaming enhanced search track: {e}", exc_info=True) return jsonify({"error": str(e)}), 500 # ============================================================================= @@ -8429,16 +8429,16 @@ def download_music_video(): if best and best_score >= 0.5: artist_name = best.artists[0] if best.artists else raw_channel track_title = best.name - print(f"🎬 [Music Video] Matched to: {artist_name} - {track_title} (confidence: {best_score:.2f})") + print(f"[Music Video] Matched to: {artist_name} - {track_title} (confidence: {best_score:.2f})") else: # Parse artist from video title: "Artist - Title" pattern if ' - ' in raw_title: parts = raw_title.split(' - ', 1) artist_name = parts[0].strip() track_title = _re.sub(r'\s*[\(\[].*?[\)\]]', '', parts[1]).strip() - print(f"🎬 [Music Video] No metadata match, using parsed: {artist_name} - {track_title}") + print(f"[Music Video] No metadata match, using parsed: {artist_name} - {track_title}") except Exception as e: - print(f"⚠️ [Music Video] Metadata lookup failed: {e}") + print(f"[Music Video] Metadata lookup failed: {e}") if ' - ' in raw_title: parts = raw_title.split(' - ', 1) artist_name = parts[0].strip() @@ -8470,17 +8470,17 @@ def download_music_video(): _music_video_downloads[video_id]['status'] = 'completed' _music_video_downloads[video_id]['progress'] = 100 _music_video_downloads[video_id]['path'] = final_path - print(f"✅ [Music Video] Downloaded: {artist_name} - {track_title} → {final_path}") - add_activity_item("🎬", "Music Video Downloaded", f"{artist_name} - {track_title}", "Now") + print(f"[Music Video] Downloaded: {artist_name} - {track_title} → {final_path}") + add_activity_item("", "Music Video Downloaded", f"{artist_name} - {track_title}", "Now") else: _music_video_downloads[video_id]['status'] = 'error' _music_video_downloads[video_id]['error'] = 'Download failed — file not found' - print(f"❌ [Music Video] Download failed for: {artist_name} - {track_title}") + print(f"[Music Video] Download failed for: {artist_name} - {track_title}") except Exception as e: _music_video_downloads[video_id]['status'] = 'error' _music_video_downloads[video_id]['error'] = str(e) - print(f"❌ [Music Video] Error: {e}") + print(f"[Music Video] Error: {e}") # Run in background thread import threading @@ -8553,8 +8553,8 @@ def start_download(): # Add activity for album download start album_name = data.get('album_name', 'Unknown Album') - logger.info(f"📥 Starting simple album download: '{album_name}' with {started_downloads}/{len(tracks)} tracks") - add_activity_item("📥", "Album Download Started", f"'{album_name}' - {started_downloads} tracks", "Now") + logger.info(f"Starting simple album download: '{album_name}' with {started_downloads}/{len(tracks)} tracks") + add_activity_item("", "Album Download Started", f"'{album_name}' - {started_downloads} tracks", "Now") return jsonify({ "success": True, @@ -8567,13 +8567,13 @@ def start_download(): filename = data.get('filename') file_size = data.get('size', 0) - logger.info(f"📥 Download request - Username: {username}, Filename: {filename[:50]}...") + logger.info(f"Download request - Username: {username}, Filename: {filename[:50]}...") if not username or not filename: return jsonify({"error": "Missing username or filename."}), 400 download_id = run_async(soulseek_client.download(username, filename, file_size)) - logger.info(f"📥 Download ID returned: {download_id}") + logger.info(f"Download ID returned: {download_id}") if download_id: # Register download for post-processing (simple transfer to /Transfer) @@ -8599,8 +8599,8 @@ def start_download(): # Extract track name from filename for activity track_name = filename.split('/')[-1] if '/' in filename else filename.split('\\')[-1] if '\\' in filename else filename - logger.info(f"📥 Starting simple track download: '{track_name}'") - add_activity_item("📥", "Track Download Started", f"'{track_name}'", "Now") + logger.info(f"Starting simple track download: '{track_name}'") + add_activity_item("", "Track Download Started", f"'{track_name}'", "Now") return jsonify({"success": True, "message": "Download started"}) else: logger.error(f"Failed to start download for: {filename}") @@ -8673,11 +8673,11 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None): file_path = os.path.join(root, file) # Fast path: if path aligns with expected directory structure, return now if api_dir_parts and _path_matches_api_dirs(file_path): - print(f"✅ Found path-confirmed match in {location_name}: {file_path}") + print(f"Found path-confirmed match in {location_name}: {file_path}") return file_path, 1.0 if not api_dir_parts: # No directory info to disambiguate — return first match (original behavior) - print(f"✅ Found exact match in {location_name}: {file_path}") + print(f"Found exact match in {location_name}: {file_path}") return file_path, 1.0 exact_matches.append(file_path) continue @@ -8689,10 +8689,10 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None): if stripped_stem != file_stem and stripped_stem + file_ext_part == target_basename: file_path = os.path.join(root, file) if api_dir_parts and _path_matches_api_dirs(file_path): - print(f"✅ Found path-confirmed dedup match in {location_name}: {file_path}") + print(f"Found path-confirmed dedup match in {location_name}: {file_path}") return file_path, 1.0 if not api_dir_parts: - print(f"✅ Found dedup-suffix match in {location_name}: {file_path}") + print(f"Found dedup-suffix match in {location_name}: {file_path}") return file_path, 1.0 exact_matches.append(file_path) continue @@ -8708,7 +8708,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None): # Return best exact match (disambiguated by path), or fall back to fuzzy if exact_matches: if len(exact_matches) == 1: - print(f"✅ Found exact match in {location_name}: {exact_matches[0]}") + print(f"Found exact match in {location_name}: {exact_matches[0]}") return exact_matches[0], 1.0 # Multiple files share the basename — pick the one whose path best # matches the expected directory structure from the Soulseek remote path @@ -8720,7 +8720,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None): if score > best_score: best_score = score best = m - print(f"⚠️ Found {len(exact_matches)} files named '{target_basename}' in {location_name}, picked best path match: {best}") + print(f"Found {len(exact_matches)} files named '{target_basename}' in {location_name}, picked best path match: {best}") return best, 1.0 return best_fuzzy_path, highest_fuzzy_similarity @@ -8742,7 +8742,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None): if downloads_similarity > 0.85: location = 'downloads' if downloads_similarity < 1.0: - print(f"✅ Found fuzzy match in downloads ({downloads_similarity:.2f}): {best_downloads_path}") + print(f"Found fuzzy match in downloads ({downloads_similarity:.2f}): {best_downloads_path}") return (best_downloads_path, location) # If not found in downloads and transfer_dir is provided, search there @@ -8753,7 +8753,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None): if transfer_similarity > 0.85: location = 'transfer' if transfer_similarity < 1.0: - print(f"✅ Found fuzzy match in transfer ({transfer_similarity:.2f}): {best_transfer_path}") + print(f"Found fuzzy match in transfer ({transfer_similarity:.2f}): {best_transfer_path}") return (best_transfer_path, location) # Don't spam logs - file not found is common for completed/processed downloads @@ -8818,7 +8818,7 @@ def get_download_status(): with matched_context_lock: has_active_context = context_key in matched_downloads_context if has_active_context: - print(f"🔄 Orphaned key {context_key} has active context — retry re-used same source, treating as active") + print(f"Orphaned key {context_key} has active context — retry re-used same source, treating as active") _orphaned_download_keys.discard(context_key) # Fall through to normal processing below else: @@ -8829,10 +8829,10 @@ def get_download_status(): if found_path: try: os.remove(found_path) - print(f"🧹 Deleted orphaned download: {os.path.basename(found_path)}") + print(f"Deleted orphaned download: {os.path.basename(found_path)}") orphan_cleaned = True except Exception as e: - print(f"⚠️ Failed to delete orphaned file (will retry next poll): {e}") + print(f"Failed to delete orphaned file (will retry next poll): {e}") else: # File not on disk (already gone or never written) — nothing to clean orphan_cleaned = True @@ -8856,10 +8856,10 @@ def get_download_status(): available_keys = list(matched_downloads_context.keys())[:5] if not context else None if context: - print(f"✅ [Context Lookup] Found context for key: {context_key}") + print(f"[Context Lookup] Found context for key: {context_key}") elif context_key not in _stale_transfer_keys: # Only log once per stale key to avoid spamming every poll cycle - print(f"⚠️ [Context Lookup] No context found for key: {context_key}") + print(f"[Context Lookup] No context found for key: {context_key}") print(f" Available keys: {available_keys}...") _stale_transfer_keys.add(context_key) @@ -8873,10 +8873,10 @@ def get_download_status(): # Prevent two contexts from claiming the same physical file _norm_path = os.path.normpath(found_path) if _norm_path in _files_claimed_this_cycle: - print(f"⚠️ File already claimed by another context this cycle: {os.path.basename(found_path)} — deferring to next poll") + print(f"File already claimed by another context this cycle: {os.path.basename(found_path)} — deferring to next poll") else: _files_claimed_this_cycle.add(_norm_path) - print(f"🎯 Found completed matched file on disk: {found_path}") + print(f"Found completed matched file on disk: {found_path}") completed_matched_downloads.append((context_key, context, found_path)) # Don't add to _processed_download_ids yet - wait until thread starts successfully @@ -8885,7 +8885,7 @@ def get_download_status(): if context_key in _download_retry_attempts: retry_count = _download_retry_attempts[context_key]['count'] elapsed = time.time() - _download_retry_attempts[context_key]['first_attempt'] - print(f"✅ File found after {retry_count} retry attempt(s) ({elapsed:.1f}s): {os.path.basename(filename_from_api)}") + print(f"File found after {retry_count} retry attempt(s) ({elapsed:.1f}s): {os.path.basename(filename_from_api)}") del _download_retry_attempts[context_key] else: # File not found yet - implement retry logic instead of immediate give-up @@ -8897,7 +8897,7 @@ def get_download_status(): 'count': 1, 'first_attempt': time.time() } - print(f"⏳ File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt 1/{_download_retry_max})") + print(f"File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt 1/{_download_retry_max})") else: # Increment retry count _download_retry_attempts[context_key]['count'] += 1 @@ -8906,19 +8906,19 @@ def get_download_status(): if retry_count >= _download_retry_max: # Max retries reached, give up - print(f"❌ CRITICAL: Could not find '{os.path.basename(filename_from_api)}' after {retry_count} attempts over {elapsed:.1f}s. Giving up.") + print(f"CRITICAL: Could not find '{os.path.basename(filename_from_api)}' after {retry_count} attempts over {elapsed:.1f}s. Giving up.") _processed_download_ids.add(context_key) # Clean up retry tracking del _download_retry_attempts[context_key] else: - print(f"⏳ File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt {retry_count}/{_download_retry_max}, elapsed: {elapsed:.1f}s)") + print(f"File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt {retry_count}/{_download_retry_max}, elapsed: {elapsed:.1f}s)") # If we found completed matched downloads, start processing them in background threads if completed_matched_downloads: def process_completed_downloads(): for context_key, context, found_path in completed_matched_downloads: try: - print(f"🚀 Starting post-processing thread for: {context_key}") + print(f"Starting post-processing thread for: {context_key}") # Use verification wrapper if context has task tracking IDs, # otherwise call directly (race guard flag still gets set on context) _pp_task_id = context.get('task_id') @@ -8935,16 +8935,16 @@ def get_download_status(): # Only mark as processed AFTER thread starts successfully _processed_download_ids.add(context_key) - print(f"✅ Marked as processed: {context_key}") + print(f"Marked as processed: {context_key}") # DON'T remove context immediately - verification worker needs it # Context will be cleaned up by verification worker after both processors complete - print(f"💾 Keeping context for verification worker: {context_key}") + print(f"Keeping context for verification worker: {context_key}") except Exception as e: - print(f"❌ Error starting post-processing thread for {context_key}: {e}") + print(f"Error starting post-processing thread for {context_key}: {e}") # Don't add to processed set if thread failed to start - print(f"⚠️ Will retry {context_key} on next check") + print(f"Will retry {context_key} on next check") # Start a single thread to manage the launching of all processing threads processing_thread = threading.Thread(target=process_completed_downloads) @@ -8990,14 +8990,14 @@ def get_download_status(): # Prevent two contexts from claiming the same physical file _st_norm = os.path.normpath(found_path) if _st_norm in _files_claimed_this_cycle: - print(f"⚠️ [{source_label}] File already claimed this cycle: {os.path.basename(found_path)} — deferring") + print(f"[{source_label}] File already claimed this cycle: {os.path.basename(found_path)} — deferring") continue _files_claimed_this_cycle.add(_st_norm) - print(f"🎯 [{source_label}] Found completed matched file on disk: {found_path}") + print(f"[{source_label}] Found completed matched file on disk: {found_path}") # Start post-processing thread def process_streaming_download(_ctx_key=context_key, _ctx=context, _path=found_path, _label=source_label): try: - print(f"🚀 [{_label}] Starting post-processing thread for: {_ctx_key}") + print(f"[{_label}] Starting post-processing thread for: {_ctx_key}") # Use verification wrapper if context has task tracking IDs _st_task_id = _ctx.get('task_id') _st_batch_id = _ctx.get('batch_id') @@ -9011,9 +9011,9 @@ def get_download_status(): thread.daemon = True thread.start() _processed_download_ids.add(_ctx_key) - print(f"✅ [{_label}] Marked as processed: {_ctx_key}") + print(f"[{_label}] Marked as processed: {_ctx_key}") except Exception as e: - print(f"❌ [{_label}] Error starting post-processing thread for {_ctx_key}: {e}") + print(f"[{_label}] Error starting post-processing thread for {_ctx_key}: {e}") processing_thread = threading.Thread(target=process_streaming_download) processing_thread.daemon = True @@ -9024,7 +9024,7 @@ def get_download_status(): _processed_download_ids.add(context_key) except Exception as streaming_error: import traceback - print(f"⚠️ Could not fetch YouTube/Tidal downloads for status: {streaming_error}") + print(f"Could not fetch YouTube/Tidal downloads for status: {streaming_error}") traceback.print_exc() return jsonify({"transfers": all_transfers}) @@ -9147,7 +9147,7 @@ def get_task_candidates(task_id): "candidate_count": len(serialized), }) except Exception as e: - print(f"❌ [Candidates] Error fetching candidates for task {task_id}: {e}") + print(f"[Candidates] Error fetching candidates for task {task_id}: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/downloads/task//download-candidate', methods=['POST']) @@ -9255,11 +9255,11 @@ def download_selected_candidate(task_id): missing_download_executor.submit(_run_manual_download) track_name = track_info.get('name', 'Unknown') - print(f"🎯 [Manual Download] User selected candidate for '{track_name}' from {username}") + print(f"[Manual Download] User selected candidate for '{track_name}' from {username}") return jsonify({"success": True, "message": f"Download initiated for '{track_name}'"}) except Exception as e: - print(f"❌ [Manual Download] Error: {e}") + print(f"[Manual Download] Error: {e}") import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @@ -9286,12 +9286,12 @@ def clear_quarantine(): shutil.rmtree(entry_path) removed_files += 1 except Exception as e: - print(f"⚠️ [Quarantine] Failed to remove {entry}: {e}") + print(f"[Quarantine] Failed to remove {entry}: {e}") - print(f"🧹 [Quarantine] Cleared {removed_files} item(s) from quarantine folder") + print(f"[Quarantine] Cleared {removed_files} item(s) from quarantine folder") return jsonify({"success": True, "message": f"Quarantine cleared ({removed_files} item{'s' if removed_files != 1 else ''} removed)."}) except Exception as e: - print(f"❌ [Quarantine] Error clearing quarantine: {e}") + print(f"[Quarantine] Error clearing quarantine: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/scan/request', methods=['POST']) @@ -9309,7 +9309,7 @@ def request_media_scan(): result = web_scan_manager.request_scan(reason=reason) - add_activity_item("📡", "Media Scan", f"Scan requested: {reason}", "Now") + add_activity_item("", "Media Scan", f"Scan requested: {reason}", "Now") return jsonify({ "success": True, "scan_info": result, @@ -9380,7 +9380,7 @@ def request_incremental_database_update(): }) db_update_executor.submit(_run_db_update_task, False, active_server) - add_activity_item("🔄", "Database Update", f"Incremental update started: {reason}", "Now") + add_activity_item("", "Database Update", f"Incremental update started: {reason}", "Now") return jsonify({ "success": True, "message": "Incremental database update started", @@ -9483,7 +9483,7 @@ def clear_all_searches(): try: success = run_async(soulseek_client.clear_all_searches()) if success: - add_activity_item("🧹", "Search Cleanup", "All search history cleared manually", "Now") + add_activity_item("", "Search Cleanup", "All search history cleared manually", "Now") return jsonify({"success": True, "message": "All searches cleared."}) else: return jsonify({"success": False, "error": "Backend failed to clear searches."}), 500 @@ -9505,7 +9505,7 @@ def maintain_search_history(): keep_searches=keep_searches, trigger_threshold=trigger_threshold )) if success: - add_activity_item("🧹", "Search Maintenance", f"Search history maintained (keeping {keep_searches} searches)", "Now") + add_activity_item("", "Search Maintenance", f"Search history maintained (keeping {keep_searches} searches)", "Now") return jsonify({"success": True, "message": f"Search history maintained (keeping {keep_searches} searches)."}) else: return jsonify({"success": False, "error": "Backend failed to maintain search history."}), 500 @@ -9531,13 +9531,13 @@ def fix_artist_image_url(thumb_url): if needs_fixing: active_server = config_manager.get_active_media_server() - print(f"🔧 Fixing URL: {thumb_url}, Active server: {active_server}") + print(f"Fixing URL: {thumb_url}, Active server: {active_server}") if active_server == 'plex': plex_config = config_manager.get_plex_config() plex_base_url = plex_config.get('base_url', '') plex_token = plex_config.get('token', '') - print(f"🔧 Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...") + print(f"Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...") if plex_base_url and plex_token: # Extract the path from URL @@ -9552,14 +9552,14 @@ def fix_artist_image_url(thumb_url): # Construct proper Plex URL with token fixed_url = f"{plex_base_url.rstrip('/')}{path}?X-Plex-Token={plex_token}" - print(f"🔧 Fixed URL: {fixed_url}") + print(f"Fixed URL: {fixed_url}") return fixed_url elif active_server == 'jellyfin': jellyfin_config = config_manager.get_jellyfin_config() jellyfin_base_url = jellyfin_config.get('base_url', '') jellyfin_token = jellyfin_config.get('api_key', '') - print(f"🔧 Jellyfin config - base_url: {jellyfin_base_url}, token: {jellyfin_token[:10] if jellyfin_token else 'None'}...") + print(f"Jellyfin config - base_url: {jellyfin_base_url}, token: {jellyfin_token[:10] if jellyfin_token else 'None'}...") if jellyfin_base_url: # Extract the path from URL @@ -9578,7 +9578,7 @@ def fix_artist_image_url(thumb_url): fixed_url = f"{jellyfin_base_url.rstrip('/')}{path}{separator}X-Emby-Token={jellyfin_token}" else: fixed_url = f"{jellyfin_base_url.rstrip('/')}{path}" - print(f"🔧 Fixed URL: {fixed_url}") + print(f"Fixed URL: {fixed_url}") return fixed_url elif active_server == 'navidrome': @@ -9586,7 +9586,7 @@ def fix_artist_image_url(thumb_url): navidrome_base_url = navidrome_config.get('base_url', '') navidrome_username = navidrome_config.get('username', '') navidrome_password = navidrome_config.get('password', '') - print(f"🔧 Navidrome config - base_url: {navidrome_base_url}, username: {navidrome_username}") + print(f"Navidrome config - base_url: {navidrome_base_url}, username: {navidrome_username}") if navidrome_base_url and navidrome_username and navidrome_password: # Extract the path from URL @@ -9611,10 +9611,10 @@ def fix_artist_image_url(thumb_url): # Construct proper Navidrome Subsonic URL fixed_url = f"{navidrome_base_url.rstrip('/')}{path}{separator}{auth_params}" - print(f"🔧 Fixed URL: {fixed_url}") + print(f"Fixed URL: {fixed_url}") return fixed_url - print(f"⚠️ No configuration found for {active_server} or unsupported server type") + print(f"No configuration found for {active_server} or unsupported server type") # Return original URL if no fixing needed/possible return thumb_url @@ -9687,7 +9687,7 @@ def get_library_artists(): }) except Exception as e: - print(f"❌ Error fetching library artists: {e}") + print(f"Error fetching library artists: {e}") import traceback traceback.print_exc() return jsonify({ @@ -9716,7 +9716,7 @@ def test_artist_endpoint(artist_id): def get_artist_detail(artist_id): """Get artist detail data""" try: - print(f"🎵 Getting artist detail for ID: {artist_id}") + print(f"Getting artist detail for ID: {artist_id}") # Get database instance database = get_database() @@ -9725,7 +9725,7 @@ def get_artist_detail(artist_id): db_result = database.get_artist_discography(artist_id) if not db_result.get('success'): - print(f"❌ Database returned error: {db_result}") + print(f"Database returned error: {db_result}") return jsonify({ "success": False, "error": db_result.get('error', 'Artist not found') @@ -9734,18 +9734,18 @@ def get_artist_detail(artist_id): artist_info = db_result['artist'] owned_releases = db_result['owned_releases'] - print(f"✅ Found artist: {artist_info['name']} with {len(owned_releases['albums'])} albums") + print(f"Found artist: {artist_info['name']} with {len(owned_releases['albums'])} albums") # Fix artist image URL - print(f"🖼️ Artist image before fix: '{artist_info.get('image_url')}'") + print(f"Artist image before fix: '{artist_info.get('image_url')}'") if artist_info.get('image_url'): artist_info['image_url'] = fix_artist_image_url(artist_info['image_url']) - print(f"🖼️ Artist image after fix: '{artist_info['image_url']}'") + print(f"Artist image after fix: '{artist_info['image_url']}'") else: - print(f"🖼️ No artist image URL found for {artist_info['name']}") + print(f"No artist image URL found for {artist_info['name']}") # Debug final artist data being sent - print(f"🖼️ Final artist data being sent: {artist_info}") + print(f"Final artist data being sent: {artist_info}") # Fix image URLs for all albums for album in owned_releases['albums']: @@ -9767,7 +9767,7 @@ def get_artist_detail(artist_id): spotify_discography = get_spotify_artist_discography(artist_info['name']) if spotify_discography['success']: - print(f"🎵 Spotify discography found - Albums: {len(spotify_discography['albums'])}, EPs: {len(spotify_discography['eps'])}, Singles: {len(spotify_discography['singles'])}") + print(f"Spotify discography found - Albums: {len(spotify_discography['albums'])}, EPs: {len(spotify_discography['eps'])}, Singles: {len(spotify_discography['singles'])}") # Store Spotify artist data for the response spotify_artist_data = { @@ -9779,11 +9779,11 @@ def get_artist_detail(artist_id): # Merge owned and Spotify data for complete picture merged_discography = merge_discography_data(owned_releases, spotify_discography, db=database, artist_name=artist_info['name']) else: - print(f"⚠️ Spotify discography not found: {spotify_discography.get('error', 'Unknown error')}") + print(f"Spotify discography not found: {spotify_discography.get('error', 'Unknown error')}") # Fall back to our database categorization merged_discography = owned_releases except Exception as spotify_error: - print(f"⚠️ Error fetching Spotify data: {spotify_error}") + print(f"Error fetching Spotify data: {spotify_error}") # Fall back to our database categorization merged_discography = owned_releases @@ -9837,7 +9837,7 @@ def get_artist_detail(artist_id): return jsonify(response_data) except Exception as e: - print(f"❌ Error in get_artist_detail: {e}") + print(f"Error in get_artist_detail: {e}") import traceback traceback.print_exc() return jsonify({ @@ -9897,7 +9897,7 @@ def get_similar_artists_stream(artist_name): """ def generate(): try: - print(f"🎵 Streaming similar artists for: {artist_name}") + print(f"Streaming similar artists for: {artist_name}") # Import required libraries from bs4 import BeautifulSoup @@ -9906,7 +9906,7 @@ def get_similar_artists_stream(artist_name): url_artist = artist_name.lower().replace(' ', '+') musicmap_url = f'https://www.music-map.com/{url_artist}' - print(f"🌐 Fetching MusicMap: {musicmap_url}") + print(f"Fetching MusicMap: {musicmap_url}") # Set headers to mimic a browser headers = { @@ -9941,7 +9941,7 @@ def get_similar_artists_stream(artist_name): similar_artist_names.append(artist_text) - print(f"📦 Found {len(similar_artist_names)} similar artists from MusicMap") + print(f"Found {len(similar_artist_names)} similar artists from MusicMap") # Determine metadata source use_hydrabase = _is_hydrabase_active() @@ -9960,9 +9960,9 @@ def get_similar_artists_stream(artist_name): searched_results = spotify_client.search_artists(artist_name, limit=1) if searched_results and len(searched_results) > 0: searched_artist_id = searched_results[0].id - print(f"🎯 Searched artist ID: {searched_artist_id}") + print(f"Searched artist ID: {searched_artist_id}") except Exception as e: - print(f"⚠️ Could not get searched artist ID: {e}") + print(f"Could not get searched artist ID: {e}") # Match each artist one by one and stream results max_artists = 20 @@ -9971,7 +9971,7 @@ def get_similar_artists_stream(artist_name): for artist_name_to_match in similar_artist_names[:max_artists]: try: - print(f"🔍 Matching: {artist_name_to_match}") + print(f"Matching: {artist_name_to_match}") # Search for the artist via active metadata source if use_hydrabase: @@ -9984,12 +9984,12 @@ def get_similar_artists_stream(artist_name): # Skip if this is the searched artist if spotify_artist.id == searched_artist_id: - print(f"⏭️ Skipping searched artist: {spotify_artist.name}") + print(f"Skipping searched artist: {spotify_artist.name}") continue # Skip if we've already seen this artist ID (deduplication) if spotify_artist.id in seen_artist_ids: - print(f"⏭️ Skipping duplicate artist: {spotify_artist.name}") + print(f"Skipping duplicate artist: {spotify_artist.name}") continue seen_artist_ids.add(spotify_artist.id) @@ -10006,24 +10006,24 @@ def get_similar_artists_stream(artist_name): yield f"data: {json.dumps({'artist': artist_data})}\n\n" matched_count += 1 - print(f"✅ Matched and streamed: {spotify_artist.name}") + print(f"Matched and streamed: {spotify_artist.name}") else: - print(f"❌ No Spotify match found for: {artist_name_to_match}") + print(f"No Spotify match found for: {artist_name_to_match}") except Exception as match_error: - print(f"⚠️ Error matching {artist_name_to_match}: {match_error}") + print(f"Error matching {artist_name_to_match}: {match_error}") continue # Send completion message yield f"data: {json.dumps({'complete': True, 'total': matched_count})}\n\n" - print(f"✅ Streaming complete: {matched_count} artists matched") + print(f"Streaming complete: {matched_count} artists matched") except requests.exceptions.RequestException as e: - print(f"❌ Error fetching MusicMap: {e}") + print(f"Error fetching MusicMap: {e}") yield f"data: {json.dumps({'error': f'Failed to fetch from MusicMap: {str(e)}'})}\n\n" except Exception as e: - print(f"❌ Error streaming similar artists: {e}") + print(f"Error streaming similar artists: {e}") import traceback traceback.print_exc() yield f"data: {json.dumps({'error': str(e)})}\n\n" @@ -10042,7 +10042,7 @@ def get_similar_artists(artist_name): JSON with similar artists matched to Spotify data """ try: - print(f"🎵 Getting similar artists for: {artist_name}") + print(f"Getting similar artists for: {artist_name}") # Import required libraries from bs4 import BeautifulSoup @@ -10051,7 +10051,7 @@ def get_similar_artists(artist_name): url_artist = artist_name.lower().replace(' ', '+') musicmap_url = f'https://www.music-map.com/{url_artist}' - print(f"🌐 Fetching MusicMap: {musicmap_url}") + print(f"Fetching MusicMap: {musicmap_url}") # Set headers to mimic a browser headers = { @@ -10088,7 +10088,7 @@ def get_similar_artists(artist_name): similar_artist_names.append(artist_text) - print(f"📦 Found {len(similar_artist_names)} similar artists from MusicMap") + print(f"Found {len(similar_artist_names)} similar artists from MusicMap") # Determine metadata source use_hydrabase = _is_hydrabase_active() @@ -10109,9 +10109,9 @@ def get_similar_artists(artist_name): searched_results = spotify_client.search_artists(artist_name, limit=1) if searched_results and len(searched_results) > 0: searched_artist_id = searched_results[0].id - print(f"🎯 Searched artist ID: {searched_artist_id}") + print(f"Searched artist ID: {searched_artist_id}") except Exception as e: - print(f"⚠️ Could not get searched artist ID: {e}") + print(f"Could not get searched artist ID: {e}") # Match each artist (limit to first 20 for performance) matched_artists = [] @@ -10120,7 +10120,7 @@ def get_similar_artists(artist_name): for artist_name_to_match in similar_artist_names[:max_artists]: try: - print(f"🔍 Matching: {artist_name_to_match}") + print(f"Matching: {artist_name_to_match}") # Search for the artist via active metadata source if use_hydrabase: @@ -10133,12 +10133,12 @@ def get_similar_artists(artist_name): # Skip if this is the searched artist if spotify_artist.id == searched_artist_id: - print(f"⏭️ Skipping searched artist: {spotify_artist.name}") + print(f"Skipping searched artist: {spotify_artist.name}") continue # Skip if we've already seen this artist ID (deduplication) if spotify_artist.id in seen_artist_ids: - print(f"⏭️ Skipping duplicate artist: {spotify_artist.name}") + print(f"Skipping duplicate artist: {spotify_artist.name}") continue seen_artist_ids.add(spotify_artist.id) @@ -10151,15 +10151,15 @@ def get_similar_artists(artist_name): 'popularity': spotify_artist.popularity if hasattr(spotify_artist, 'popularity') else 0 }) - print(f"✅ Matched: {spotify_artist.name}") + print(f"Matched: {spotify_artist.name}") else: - print(f"❌ No Spotify match found for: {artist_name_to_match}") + print(f"No Spotify match found for: {artist_name_to_match}") except Exception as match_error: - print(f"⚠️ Error matching {artist_name_to_match}: {match_error}") + print(f"Error matching {artist_name_to_match}: {match_error}") continue - print(f"✅ Successfully matched {len(matched_artists)} artists to Spotify") + print(f"Successfully matched {len(matched_artists)} artists to Spotify") return jsonify({ "success": True, @@ -10170,14 +10170,14 @@ def get_similar_artists(artist_name): }) except requests.exceptions.RequestException as e: - print(f"❌ Error fetching MusicMap: {e}") + print(f"Error fetching MusicMap: {e}") return jsonify({ "success": False, "error": f"Failed to fetch from MusicMap: {str(e)}" }), 500 except Exception as e: - print(f"❌ Error getting similar artists: {e}") + print(f"Error getting similar artists: {e}") import traceback traceback.print_exc() return jsonify({ @@ -10260,7 +10260,7 @@ def get_artist_discography(artist_id): fallback_client = _get_metadata_fallback_client() fallback_source = _get_metadata_fallback_source() - print(f"🎤 Fetching discography for artist: {artist_id} (name: {artist_name}, spotify: {spotify_available}, source_override: {source_override or 'auto'})") + print(f"Fetching discography for artist: {artist_id} (name: {artist_name}, spotify: {spotify_available}, source_override: {source_override or 'auto'})") albums = [] active_source = None @@ -10327,7 +10327,7 @@ def get_artist_discography(artist_id): active_source = source_override if albums: - print(f"📊 Got {len(albums)} albums from {active_source} (source override)") + print(f"Got {len(albums)} albums from {active_source} (source override)") except Exception as e: print(f"Source override ({source_override}) lookup failed: {e}") @@ -10337,7 +10337,7 @@ def get_artist_discography(artist_id): albums = hydrabase_client.search_discography(artist_name, limit=50) if albums: active_source = 'hydrabase' - print(f"📊 Got {len(albums)} albums from Hydrabase") + print(f"Got {len(albums)} albums from Hydrabase") except Exception as e: print(f"Hydrabase discography failed: {e}") @@ -10351,7 +10351,7 @@ def get_artist_discography(artist_id): albums = spotify_client.get_artist_albums(artist_id, album_type='album,single') if albums: active_source = 'spotify' - print(f"📊 Got {len(albums)} albums from Spotify") + print(f"Got {len(albums)} albums from Spotify") except Exception as e: print(f"Spotify lookup failed: {e}") @@ -10363,10 +10363,10 @@ def get_artist_discography(artist_id): albums = fallback_client.get_artist_albums(artist_id, album_type='album,single', limit=50) if albums: active_source = fallback_source - print(f"📊 Got {len(albums)} albums from {fallback_source} (direct ID)") + print(f"Got {len(albums)} albums from {fallback_source} (direct ID)") elif artist_name: # Search fallback by name - print(f"🔄 Trying {fallback_source} search by name: '{artist_name}'") + print(f"Trying {fallback_source} search by name: '{artist_name}'") fallback_artists = fallback_client.search_artists(artist_name, limit=5) if fallback_artists: # Find best match @@ -10378,15 +10378,15 @@ def get_artist_discography(artist_id): if not best_match: best_match = fallback_artists[0] - print(f"✅ Found {fallback_source} artist: {best_match.name} (ID: {best_match.id})") + print(f"Found {fallback_source} artist: {best_match.name} (ID: {best_match.id})") albums = fallback_client.get_artist_albums(best_match.id, album_type='album,single', limit=50) if albums: active_source = fallback_source - print(f"📊 Got {len(albums)} albums from {fallback_source} (name search)") + print(f"Got {len(albums)} albums from {fallback_source} (name search)") except Exception as e: print(f"{fallback_source} lookup failed: {e}") - print(f"📊 Total albums returned: {len(albums)} (source: {active_source})") + print(f"Total albums returned: {len(albums)} (source: {active_source})") if not albums: return jsonify({ @@ -10425,7 +10425,7 @@ def get_artist_discography(artist_id): # Skip obvious compilation issues but be more lenient for now if hasattr(album, 'album_type') and album.album_type == 'compilation': - print(f"📀 Found compilation: '{album.name}' - including for now") + print(f"Found compilation: '{album.name}' - including for now") # Categorize by album type if hasattr(album, 'album_type'): @@ -10450,13 +10450,13 @@ def get_artist_discography(artist_id): album_list.sort(key=get_release_year, reverse=True) singles_list.sort(key=get_release_year, reverse=True) - print(f"✅ Found {len(album_list)} albums and {len(singles_list)} singles for artist {artist_id}") + print(f"Found {len(album_list)} albums and {len(singles_list)} singles for artist {artist_id}") # Debug: Log the final album list for album in album_list: - print(f"📀 Album: {album['name']} ({album['album_type']}) - {album['release_date']}") + print(f"Album: {album['name']} ({album['album_type']}) - {album['release_date']}") for single in singles_list: - print(f"🎵 Single/EP: {single['name']} ({single['album_type']}) - {single['release_date']}") + print(f"Single/EP: {single['name']} ({single['album_type']}) - {single['release_date']}") # Gather artist enrichment info from cache + library artist_info = {} @@ -10563,7 +10563,7 @@ def get_artist_discography(artist_id): }) except Exception as e: - print(f"❌ Error fetching artist discography: {e}") + print(f"Error fetching artist discography: {e}") import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @@ -10605,20 +10605,20 @@ def _resolve_db_album_id(album_id, artist_id=None): album_title = row['title'] artist_name = row['artist_name'] query = f"{artist_name} {album_title}" - print(f"🔍 Searching for album by name: '{query}'") + print(f"Searching for album by name: '{query}'") results = spotify_client.search_albums(query, limit=5) if results: # Pick the best match (search already ranks by relevance) for album in results: if album.name.lower().strip() == album_title.lower().strip(): - print(f"✅ Found exact album match: {album.name} (ID: {album.id})") + print(f"Found exact album match: {album.name} (ID: {album.id})") return album.id # Fall back to first result if no exact title match - print(f"⚠️ No exact match, using best result: {results[0].name} (ID: {results[0].id})") + print(f"No exact match, using best result: {results[0].name} (ID: {results[0].id})") return results[0].id except Exception as e: - print(f"⚠️ Error resolving DB album ID {album_id}: {e}") + print(f"Error resolving DB album ID {album_id}: {e}") return None @@ -10658,7 +10658,7 @@ def get_artist_album_tracks(artist_id, album_id): 'uri': '', 'album': album_info }) - print(f"✅ Hydrabase returned {len(formatted_tracks)} tracks for album: {album_info['name']}") + print(f"Hydrabase returned {len(formatted_tracks)} tracks for album: {album_info['name']}") return jsonify({ 'success': True, 'album': album_info, @@ -10686,7 +10686,7 @@ def get_artist_album_tracks(artist_id, album_id): elif source_override == 'discogs': client = _get_discogs_client() - print(f"🎵 Fetching tracks for album: {album_id} by artist: {artist_id} (source: {source_override or 'auto'})") + print(f"Fetching tracks for album: {album_id} by artist: {artist_id} (source: {source_override or 'auto'})") # Get album information first album_data = client.get_album(album_id) @@ -10696,7 +10696,7 @@ def get_artist_album_tracks(artist_id, album_id): if not album_data: resolved_album_id = _resolve_db_album_id(album_id, artist_id) if resolved_album_id and resolved_album_id != album_id: - print(f"🔄 Resolved DB album ID {album_id} -> external ID {resolved_album_id}") + print(f"Resolved DB album ID {album_id} -> external ID {resolved_album_id}") album_data = client.get_album(resolved_album_id) if not album_data: @@ -10750,7 +10750,7 @@ def get_artist_album_tracks(artist_id, album_id): } formatted_tracks.append(formatted_track) - print(f"✅ Successfully formatted {len(formatted_tracks)} tracks for album: {album_info['name']}") + print(f"Successfully formatted {len(formatted_tracks)} tracks for album: {album_info['name']}") return jsonify({ 'success': True, @@ -10759,7 +10759,7 @@ def get_artist_album_tracks(artist_id, album_id): }) except Exception as e: - print(f"❌ Error fetching album tracks: {e}") + print(f"Error fetching album tracks: {e}") import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @@ -10878,7 +10878,7 @@ def download_discography(artist_id): total_added += added total_skipped += skipped - print(f"📥 [Discography] {album_name}: {added} added, {skipped} skipped") + print(f"[Discography] {album_name}: {added} added, {skipped} skipped") yield json.dumps({ "album_id": album_id, "name": album_name, "status": "done", "tracks_added": added, "tracks_skipped": skipped, "tracks_total": len(tracks) @@ -10887,13 +10887,13 @@ def download_discography(artist_id): except Exception as album_err: yield json.dumps({"album_id": album_id, "status": "error", "message": str(album_err)}) + '\n' - print(f"📥 [Discography] Complete for {artist_name}: {total_added} tracks added, {total_skipped} skipped across {len(album_ids)} albums") + print(f"[Discography] Complete for {artist_name}: {total_added} tracks added, {total_skipped} skipped across {len(album_ids)} albums") yield json.dumps({"status": "complete", "total_added": total_added, "total_skipped": total_skipped, "total_albums": len(album_ids)}) + '\n' return app.response_class(generate_ndjson(), mimetype='application/x-ndjson', headers={'X-Accel-Buffering': 'no'}) except Exception as e: - print(f"❌ Error in download discography: {e}") + print(f"Error in download discography: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/artist//completion', methods=['POST']) @@ -10918,7 +10918,7 @@ def check_artist_discography_completion(artist_id): # If no artist name provided, try to infer it from the request if artist_name == 'Unknown Artist': - print(f"⚠️ No artist name provided in request, attempting to infer from discography data") + print(f"No artist name provided in request, attempting to infer from discography data") # Try to extract from first album's title by using a simple search all_items = discography.get('albums', []) + discography.get('singles', []) if all_items and spotify_client and spotify_client.is_authenticated(): @@ -10928,12 +10928,12 @@ def check_artist_discography_completion(artist_id): search_results = spotify_client.search_tracks(first_item.get('name', ''), limit=1) if search_results and len(search_results) > 0: artist_name = search_results[0].artists[0] if search_results[0].artists else "Unknown Artist" - print(f"🎤 Inferred artist name from search: {artist_name}") + print(f"Inferred artist name from search: {artist_name}") except Exception as e: - print(f"⚠️ Could not infer artist name: {e}") + print(f"Could not infer artist name: {e}") artist_name = "Unknown Artist" - print(f"🎤 Checking completion for artist: {artist_name}") + print(f"Checking completion for artist: {artist_name}") # Process albums for album in discography.get('albums', []): @@ -10951,7 +10951,7 @@ def check_artist_discography_completion(artist_id): }) except Exception as e: - print(f"❌ Error checking discography completion: {e}") + print(f"Error checking discography completion: {e}") import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @@ -10975,7 +10975,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b except Exception: pass - print(f"🔍 Checking album: '{album_name}' ({total_tracks} tracks)") + print(f"Checking album: '{album_name}' ({total_tracks} tracks)") formats = [] if test_mode: @@ -10985,7 +10985,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b expected_tracks = total_tracks confidence = random.uniform(0.7, 1.0) db_album = True # Simulate found album - print(f"🧪 TEST MODE: Simulating {owned_tracks}/{expected_tracks} tracks for '{album_name}'") + print(f"TEST MODE: Simulating {owned_tracks}/{expected_tracks} tracks for '{album_name}'") else: # Check if album exists in database with completeness info try: @@ -10999,7 +10999,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b server_source=active_server # Check only the active server ) except Exception as db_error: - print(f"⚠️ Database error for album '{album_name}': {db_error}") + print(f"Database error for album '{album_name}': {db_error}") # Return error state for this album return { "id": album_id, @@ -11030,7 +11030,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b else: status = "missing" - print(f" 📊 Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}") + print(f" Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}") return { "id": album_id, @@ -11045,7 +11045,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b } except Exception as e: - print(f"❌ Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}") + print(f"Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}") return { "id": album_data.get('id', ''), "name": album_data.get('name', 'Unknown'), @@ -11067,7 +11067,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: album_type = single_data.get('album_type', 'single') formats = [] - print(f"🎵 Checking {album_type}: '{single_name}' ({total_tracks} tracks)") + print(f"Checking {album_type}: '{single_name}' ({total_tracks} tracks)") if test_mode: # Generate test data for singles/EPs @@ -11076,12 +11076,12 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: owned_tracks = random.randint(0, total_tracks) expected_tracks = total_tracks confidence = random.uniform(0.7, 1.0) - print(f"🧪 TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks") + print(f"TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks") else: owned_tracks = random.choice([0, 1]) # 50/50 chance expected_tracks = 1 confidence = random.uniform(0.7, 1.0) if owned_tracks else 0.0 - print(f"🧪 TEST MODE: Single with {owned_tracks}/{expected_tracks} tracks") + print(f"TEST MODE: Single with {owned_tracks}/{expected_tracks} tracks") elif album_type == 'ep' or total_tracks > 1: # Treat EPs like albums try: @@ -11095,7 +11095,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: server_source=active_server # Check only the active server ) except Exception as db_error: - print(f"⚠️ Database error for EP '{single_name}': {db_error}") + print(f"Database error for EP '{single_name}': {db_error}") owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0 # Calculate completion percentage @@ -11112,7 +11112,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: else: status = "missing" - print(f" 📊 EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}") + print(f" EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}") else: # Single track - just check if the track exists @@ -11123,7 +11123,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: confidence_threshold=0.7 ) except Exception as db_error: - print(f"⚠️ Database error for single '{single_name}': {db_error}") + print(f"Database error for single '{single_name}': {db_error}") db_track, confidence = None, 0.0 owned_tracks = 1 if db_track else 0 @@ -11141,7 +11141,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: elif ext: formats = [ext] - print(f" 🎵 Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}") + print(f" Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}") return { "id": single_id, @@ -11157,7 +11157,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: } except Exception as e: - print(f"❌ Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}") + print(f"Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}") return { "id": single_data.get('id', ''), "name": single_data.get('name', 'Unknown'), @@ -11189,7 +11189,7 @@ def check_artist_discography_completion_stream(artist_id): def generate_completion_stream(): try: - print(f"🎤 Starting streaming completion check for artist: {artist_name}") + print(f"Starting streaming completion check for artist: {artist_name}") # Get database instance from database.music_database import MusicDatabase @@ -11254,7 +11254,7 @@ def check_artist_discography_completion_stream(artist_id): yield f"data: {json.dumps({'type': 'complete', 'processed_count': processed_count})}\n\n" except Exception as e: - print(f"❌ Error in streaming completion check: {e}") + print(f"Error in streaming completion check: {e}") import traceback traceback.print_exc() yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n" @@ -11322,7 +11322,7 @@ def library_completion_stream(): yield f"data: {json.dumps({'type': 'complete', 'processed_count': len(all_items)})}\n\n" except Exception as e: - print(f"❌ Error in library completion stream: {e}") + print(f"Error in library completion stream: {e}") import traceback traceback.print_exc() yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n" @@ -11430,7 +11430,7 @@ def library_check_tracks(): return jsonify({"success": True, "owned_tracks": owned_map}) except Exception as e: - print(f"❌ Error checking track ownership: {e}") + print(f"Error checking track ownership: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -11639,7 +11639,7 @@ def enhance_artist_quality(artist_id): 'external_urls': {}, } except Exception as e: - print(f"⚠️ [Enhance] Spotify lookup failed for {spotify_tid}: {e}") + print(f"[Enhance] Spotify lookup failed for {spotify_tid}: {e}") if not matched_track_data and spotify_client: # Fallback: Spotify search matching — need full track data for wishlist @@ -11713,7 +11713,7 @@ def enhance_artist_quality(artist_id): 'external_urls': best_match.external_urls or {}, } except Exception as e: - print(f"⚠️ [Enhance] Search match failed for {title}: {e}") + print(f"[Enhance] Search match failed for {title}: {e}") # Fallback source when Spotify unavailable or no match found if not matched_track_data: @@ -11782,9 +11782,9 @@ def enhance_artist_quality(artist_id): 'preview_url': itunes_best.preview_url, 'external_urls': itunes_best.external_urls or {}, } - print(f"🍎 [Enhance] Fallback match for {title}: {itunes_best.artists[0]} - {itunes_best.name} (conf: {itunes_best_conf:.3f})") + print(f"[Enhance] Fallback match for {title}: {itunes_best.artists[0]} - {itunes_best.name} (conf: {itunes_best_conf:.3f})") except Exception as e: - print(f"⚠️ [Enhance] Fallback source failed for {title}: {e}") + print(f"[Enhance] Fallback source failed for {title}: {e}") if not matched_track_data: failed_count += 1 @@ -11811,7 +11811,7 @@ def enhance_artist_quality(artist_id): if success: enhanced_count += 1 - print(f"✅ [Enhance] Queued for upgrade: {artist_name} - {title} ({tier_name})") + print(f"[Enhance] Queued for upgrade: {artist_name} - {title} ({tier_name})") else: failed_count += 1 failed_tracks.append({'track_id': track_id, 'title': title, 'reason': 'Wishlist add failed'}) @@ -11823,7 +11823,7 @@ def enhance_artist_quality(artist_id): 'failed_tracks': failed_tracks }) except Exception as e: - print(f"❌ [Enhance] Error: {e}") + print(f"[Enhance] Error: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -12741,9 +12741,9 @@ def reorganize_album_files(album_id): if not os.path.exists(sidecar_dst): try: shutil.move(sidecar_src, sidecar_dst) - print(f"📷 [Reorganize] Moved {sidecar} to {dest_dir}") + print(f"[Reorganize] Moved {sidecar} to {dest_dir}") except Exception as sc_err: - print(f"⚠️ [Reorganize] Failed to move {sidecar}: {sc_err}") + print(f"[Reorganize] Failed to move {sidecar}: {sc_err}") # Clean up empty directories left behind (after sidecars moved) for src_dir in moved_dirs: @@ -13256,7 +13256,7 @@ def library_play_track(): else: return jsonify({"success": False, "error": _get_file_not_found_error(file_path)}), 404 - print(f"📚 Library play request: {os.path.basename(file_path)}") + print(f"Library play request: {os.path.basename(file_path)}") # Set stream state to ready with the library file path directly with stream_lock: @@ -13276,7 +13276,7 @@ def library_play_track(): return jsonify({"success": True, "message": "Library track ready for playback"}) except Exception as e: - print(f"❌ Error playing library track: {e}") + print(f"Error playing library track: {e}") return jsonify({"success": False, "error": str(e)}), 500 _enrichment_locks = {svc: threading.Lock() for svc in ('audiodb', 'deezer', 'musicbrainz', 'spotify', 'itunes', 'lastfm', 'genius', 'tidal', 'qobuz', 'discogs')} @@ -13350,7 +13350,7 @@ def library_enrich_entity(): }) except Exception as e: - print(f"❌ Error enriching entity: {e}") + print(f"Error enriching entity: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -13507,7 +13507,7 @@ def library_search_service(): return jsonify({"success": True, "results": results}) except Exception as e: - print(f"❌ Error searching service: {e}") + print(f"Error searching service: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -13831,7 +13831,7 @@ def library_manual_match(): }) except Exception as e: - print(f"❌ Error manual matching: {e}") + print(f"Error manual matching: {e}") @app.route('/api/library/clear-match', methods=['PUT']) def library_clear_match(): @@ -13889,7 +13889,7 @@ def library_clear_match(): }) except Exception as e: - print(f"❌ Error clearing match: {e}") + print(f"Error clearing match: {e}") return jsonify({"success": False, "error": str(e)}), 500 import traceback @@ -13955,7 +13955,7 @@ def library_delete_track(track_id): return jsonify({"success": True, "deleted_count": cursor.rowcount, "file_deleted": file_deleted, "blacklisted": blacklisted}) except Exception as e: - print(f"❌ Error deleting track {track_id}: {e}") + print(f"Error deleting track {track_id}: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -14582,11 +14582,11 @@ def sync_artist_library(artist_id): if new_name != artist_name: cursor.execute("UPDATE artists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", (new_name, db_artist_id)) - print(f"🔄 [Artist Sync] Name updated: '{artist_name}' → '{new_name}'") + print(f"[Artist Sync] Name updated: '{artist_name}' → '{new_name}'") artist_name = new_name name_updated = True except Exception as e: - print(f"⚠️ [Artist Sync] Could not refresh name from {server_source}: {e}") + print(f"[Artist Sync] Could not refresh name from {server_source}: {e}") stale_tracks = [] valid_tracks = 0 @@ -14625,7 +14625,7 @@ def sync_artist_library(artist_id): conn.commit() - print(f"🔄 [Artist Sync] {artist_name}: {valid_tracks} valid, {len(stale_tracks)} stale removed, {empty_albums_removed} empty albums cleaned") + print(f"[Artist Sync] {artist_name}: {valid_tracks} valid, {len(stale_tracks)} stale removed, {empty_albums_removed} empty albums cleaned") return jsonify({ "success": True, @@ -14637,7 +14637,7 @@ def sync_artist_library(artist_id): }) except Exception as e: - print(f"❌ Error syncing artist {artist_id}: {e}") + print(f"Error syncing artist {artist_id}: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/library/album/', methods=['DELETE']) @@ -14658,7 +14658,7 @@ def library_delete_album(album_id): conn.commit() return jsonify({"success": True, "deleted_count": 1, "tracks_deleted": tracks_deleted}) except Exception as e: - print(f"❌ Error deleting album {album_id}: {e}") + print(f"Error deleting album {album_id}: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -14683,7 +14683,7 @@ def library_delete_tracks_batch(): conn.commit() return jsonify({"success": True, "deleted_count": cursor.rowcount}) except Exception as e: - print(f"❌ Error batch deleting tracks: {e}") + print(f"Error batch deleting tracks: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -14731,7 +14731,7 @@ def stream_start(): if not data: return jsonify({"success": False, "error": "No track data provided"}), 400 - print(f"🎵 Web UI Stream request for: {data.get('filename')}") + print(f"Web UI Stream request for: {data.get('filename')}") try: # Stop any existing streaming task @@ -14755,7 +14755,7 @@ def stream_start(): return jsonify({"success": True, "message": "Streaming started"}) except Exception as e: - print(f"❌ Error starting stream: {e}") + print(f"Error starting stream: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/stream/status') @@ -14771,7 +14771,7 @@ def stream_status(): "error_message": stream_state["error_message"] }) except Exception as e: - print(f"❌ Error getting stream status: {e}") + print(f"Error getting stream status: {e}") return jsonify({ "status": "error", "progress": 0, @@ -14792,7 +14792,7 @@ def stream_audio(): if not os.path.exists(file_path): return jsonify({"error": "Audio file not found"}), 404 - print(f"🎵 Serving audio file: {os.path.basename(file_path)}") + print(f"Serving audio file: {os.path.basename(file_path)}") # Determine MIME type based on file extension file_ext = os.path.splitext(file_path)[1].lower() @@ -14874,7 +14874,7 @@ def stream_audio(): return response except Exception as e: - print(f"❌ Error serving audio file: {e}") + print(f"Error serving audio file: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/stream/stop', methods=['POST']) @@ -14900,9 +14900,9 @@ def stream_stop(): file_path = os.path.join(stream_folder, filename) if os.path.isfile(file_path): os.remove(file_path) - print(f"🗑️ Removed stream file: {filename}") + print(f"Removed stream file: {filename}") else: - print(f"📚 Library playback stopped - skipping file deletion") + print(f"Library playback stopped - skipping file deletion") # Reset stream state with stream_lock: @@ -14918,7 +14918,7 @@ def stream_stop(): return jsonify({"success": True, "message": "Stream stopped"}) except Exception as e: - print(f"❌ Error stopping stream: {e}") + print(f"Error stopping stream: {e}") return jsonify({"success": False, "error": str(e)}), 500 # --- Matched Downloads API Endpoints --- @@ -14932,12 +14932,12 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non return [] try: - print(f"🔍 Generating artist suggestions for: {search_result.get('artist', '')} - {search_result.get('title', '')}") + print(f"Generating artist suggestions for: {search_result.get('artist', '')} - {search_result.get('title', '')}") suggestions = [] # Special handling for albums - use album title to find artist if is_album and album_result and album_result.get('album_title'): - print(f"🎵 Album mode detected - using album title for artist search") + print(f"Album mode detected - using album title for artist search") album_title = album_result.get('album_title', '') # Clean album title (remove year prefixes like "(2005)") @@ -14947,7 +14947,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non # Search tracks using album title to find the artist tracks = spotify_client.search_tracks(clean_album_title, limit=10) - print(f"📊 Found {len(tracks)} tracks from album search") + print(f"Found {len(tracks)} tracks from album search") # Collect unique artists and their associated tracks/albums unique_artists = {} # artist_name -> list of (track, album) tuples @@ -14967,7 +14967,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non if matches: return artist_name, matches[0] except Exception as e: - print(f"⚠️ Error fetching artist '{artist_name}': {e}") + print(f"Error fetching artist '{artist_name}': {e}") return artist_name, None # Use limited concurrency to respect rate limits @@ -15018,7 +15018,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non if not search_artist: return [] - print(f"🎵 Single track mode - searching for artist: '{search_artist}'") + print(f"Single track mode - searching for artist: '{search_artist}'") # Search for artists directly artist_matches = spotify_client.search_artists(search_artist, limit=10) @@ -15046,7 +15046,7 @@ def _generate_artist_suggestions(search_result, is_album=False, album_result=Non return suggestions[:4] except Exception as e: - print(f"❌ Error generating artist suggestions: {e}") + print(f"Error generating artist suggestions: {e}") return [] def _generate_album_suggestions(selected_artist, search_result): @@ -15058,12 +15058,12 @@ def _generate_album_suggestions(selected_artist, search_result): return [] try: - print(f"🔍 Generating album suggestions for artist: {selected_artist['name']}") + print(f"Generating album suggestions for artist: {selected_artist['name']}") # Determine target album name from search result target_album_name = search_result.get('album', '') or search_result.get('album_title', '') if not target_album_name: - print("⚠️ No album name found in search result") + print("No album name found in search result") return [] # Clean target album name @@ -15073,7 +15073,7 @@ def _generate_album_suggestions(selected_artist, search_result): # Get artist's albums from Spotify artist_albums = spotify_client.get_artist_albums(selected_artist['id']) - print(f"📊 Found {len(artist_albums)} albums for artist") + print(f"Found {len(artist_albums)} albums for artist") album_matches = [] for album in artist_albums: @@ -15100,7 +15100,7 @@ def _generate_album_suggestions(selected_artist, search_result): return album_matches[:4] except Exception as e: - print(f"❌ Error generating album suggestions: {e}") + print(f"Error generating album suggestions: {e}") return [] @app.route('/api/match/suggestions', methods=['POST']) @@ -15124,7 +15124,7 @@ def get_match_suggestions(): return jsonify({"suggestions": suggestions}) except Exception as e: - print(f"❌ Error in match suggestions: {e}") + print(f"Error in match suggestions: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/match/search', methods=['POST']) @@ -15220,7 +15220,7 @@ def search_match(): return jsonify({"error": "Invalid context. Must be 'artist' or 'album'"}), 400 except Exception as e: - print(f"❌ Error in match search: {e}") + print(f"Error in match search: {e}") return jsonify({"error": str(e)}), 500 @@ -15247,7 +15247,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar Download album tracks that have been matched to Spotify with full track metadata. This provides the best possible metadata enhancement and organization. """ - logger.info(f"🎯 Processing enhanced album download for '{spotify_album['name']}' with {len(enhanced_tracks)} matched tracks") + logger.info(f"Processing enhanced album download for '{spotify_album['name']}' with {len(enhanced_tracks)} matched tracks") # Compute total_discs for multi-disc album subfolder support total_discs = max((t['spotify_track'].get('disc_number', 1) for t in enhanced_tracks), default=1) @@ -15270,10 +15270,10 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar _mb_release_cache[(spotify_album['name'].lower().strip(), _pf_artist_key)] = _pf_mbid with _mb_release_detail_cache_lock: _mb_release_detail_cache[_pf_mbid] = _pf_release - print(f"🏷️ [Preflight] Pre-cached MB release for '{spotify_album['name']}': " + print(f"[Preflight] Pre-cached MB release for '{spotify_album['name']}': " f"'{_pf_release.get('title', '')}' ({_pf_mbid[:8]}...)") except Exception as pf_err: - print(f"⚠️ [Preflight] MB release preflight failed: {pf_err}") + print(f"[Preflight] MB release preflight failed: {pf_err}") # Process matched tracks with full Spotify metadata for matched_item in enhanced_tracks: @@ -15282,7 +15282,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar spotify_track = matched_item['spotify_track'] if _is_explicit_blocked(spotify_track): - logger.info(f"🚫 [Content Filter] Skipping explicit track: '{spotify_track.get('name')}'") + logger.info(f"[Content Filter] Skipping explicit track: '{spotify_track.get('name')}'") continue username = slskd_track.get('username') @@ -15319,7 +15319,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar "has_full_spotify_metadata": True # Flag for robust processing } - logger.info(f"✅ Queued matched track: '{spotify_track['name']}' (track #{spotify_track['track_number']})") + logger.info(f"Queued matched track: '{spotify_track['name']}' (track #{spotify_track['track_number']})") started_count += 1 else: logger.error(f"Failed to queue track: {filename}") @@ -15356,7 +15356,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar 'track_info': None } - logger.info(f"⚠️ Queued unmatched track (basic cleanup): {filename}") + logger.info(f"Queued unmatched track (basic cleanup): {filename}") started_count += 1 except Exception as e: @@ -15372,18 +15372,18 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album): match and correct the metadata for each individual track before downloading, ensuring perfect tagging and naming. """ - print(f"🎵 Processing matched album download for '{spotify_album['name']}' with {len(album_result.get('tracks', []))} tracks.") + print(f"Processing matched album download for '{spotify_album['name']}' with {len(album_result.get('tracks', []))} tracks.") tracks_to_download = album_result.get('tracks', []) if not tracks_to_download: - print("⚠️ Album result contained no tracks. Aborting.") + print("Album result contained no tracks. Aborting.") return 0 # --- THIS IS THE NEW LOGIC --- # Fetch the official tracklist from Spotify ONCE for the entire album. official_spotify_tracks = _get_spotify_album_tracks(spotify_album) if not official_spotify_tracks: - print("⚠️ Could not fetch official tracklist from Spotify. Metadata may be inaccurate.") + print("Could not fetch official tracklist from Spotify. Metadata may be inaccurate.") # --- END OF NEW LOGIC --- # Compute total_discs for multi-disc album subfolder support @@ -15407,10 +15407,10 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album): _mb_release_cache[(spotify_album['name'].lower().strip(), _pf_artist_key)] = _pf_mbid with _mb_release_detail_cache_lock: _mb_release_detail_cache[_pf_mbid] = _pf_release - print(f"🏷️ [Preflight] Pre-cached MB release for '{spotify_album['name']}': " + print(f"[Preflight] Pre-cached MB release for '{spotify_album['name']}': " f"'{_pf_release.get('title', '')}' ({_pf_mbid[:8]}...)") except Exception as pf_err: - print(f"⚠️ [Preflight] MB release preflight failed: {pf_err}") + print(f"[Preflight] MB release preflight failed: {pf_err}") started_count = 0 for track_data in tracks_to_download: @@ -15431,7 +15431,7 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album): # --- END OF CRITICAL STEP --- if _is_explicit_blocked(corrected_meta): - print(f"🚫 [Content Filter] Skipping explicit track: '{corrected_meta.get('title')}'") + print(f"[Content Filter] Skipping explicit track: '{corrected_meta.get('title')}'") continue # Create a clean context object using the CORRECTED metadata @@ -15467,7 +15467,7 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album): print(f" - Failed to queue track: {filename}") except Exception as e: - print(f"❌ Error processing track in album batch: {track_data.get('filename')}. Error: {e}") + print(f"Error processing track in album batch: {track_data.get('filename')}. Error: {e}") continue return started_count @@ -15503,7 +15503,7 @@ def start_matched_download(): if is_single_track and spotify_track: if _is_explicit_blocked(spotify_track): return jsonify({"success": False, "error": "Explicit content is disabled in settings", "explicit_blocked": True}), 403 - logger.info(f"🎯 Starting enhanced single track download: '{spotify_track['name']}' by {spotify_artist['name']}") + logger.info(f"Starting enhanced single track download: '{spotify_track['name']}' by {spotify_artist['name']}") username = download_payload.get('username') filename = download_payload.get('filename') @@ -15536,14 +15536,14 @@ def start_matched_download(): "has_full_spotify_metadata": True # Flag for robust processing } - logger.info(f"✅ Queued enhanced single track: '{spotify_track['name']}'") + logger.info(f"Queued enhanced single track: '{spotify_track['name']}'") return jsonify({"success": True, "message": "Enhanced single track download started"}) else: return jsonify({"success": False, "error": "Failed to start download via slskd"}), 500 # NEW: Enhanced album download with track-to-track matching if enhanced_tracks: - logger.info(f"🎯 Starting enhanced album download: {len(enhanced_tracks)} matched tracks, {len(unmatched_tracks)} unmatched") + logger.info(f"Starting enhanced album download: {len(enhanced_tracks)} matched tracks, {len(unmatched_tracks)} unmatched") started_count = _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_artist, spotify_album) if started_count > 0: return jsonify({"success": True, "message": f"Queued {started_count} tracks with full Spotify metadata."}) @@ -15672,7 +15672,7 @@ def _parse_filename_metadata(filename: str) -> dict: cleaned_album = re.sub(r'^\d{4}\s*-\s*', '', potential_album).strip() metadata['album'] = cleaned_album - print(f"🧠 Parsed Filename '{base_name}': Artist='{metadata['artist']}', Title='{metadata['title']}', Album='{metadata['album']}', Track#='{metadata['track_number']}'") + print(f"Parsed Filename '{base_name}': Artist='{metadata['artist']}', Title='{metadata['title']}', Album='{metadata['album']}', Track#='{metadata['track_number']}'") return metadata @@ -15842,7 +15842,7 @@ def _search_track_in_album_context(original_search: dict, artist: dict) -> dict: matching_engine.normalize_string(track_data['name']) ) if similarity > 0.7: - print(f"✅ Found track in album context: '{track_data['name']}'") + print(f"Found track in album context: '{track_data['name']}'") return { 'is_album': True, 'album_name': spotify_album.name, @@ -15852,7 +15852,7 @@ def _search_track_in_album_context(original_search: dict, artist: dict) -> dict: } return None except Exception as e: - print(f"❌ Error in _search_track_in_album_context: {e}") + print(f"Error in _search_track_in_album_context: {e}") return None @@ -15866,8 +15866,8 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: try: # Log available data for debugging (GUI PARITY) original_search = context.get("original_search_result", {}) - print(f"\n🔍 [Album Detection] Starting for track: '{original_search.get('title', 'Unknown')}'") - print(f"📊 [Data Available]:") + print(f"\n[Album Detection] Starting for track: '{original_search.get('title', 'Unknown')}'") + print(f"[Data Available]:") print(f" - Clean Spotify title: '{original_search.get('spotify_clean_title', 'None')}'") print(f" - Clean Spotify album: '{original_search.get('spotify_clean_album', 'None')}'") print(f" - Filename album: '{original_search.get('album', 'None')}'") @@ -15878,7 +15878,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: is_album_download = context.get("is_album_download", False) artist_name = artist['name'] - print(f"🔍 Album detection for '{original_search.get('title', 'Unknown')}' by '{artist_name}':") + print(f"Album detection for '{original_search.get('title', 'Unknown')}' by '{artist_name}':") print(f" Has album attr: {bool(original_search.get('album'))}") if original_search.get('album'): print(f" Album value: '{original_search.get('album')}'") @@ -15887,7 +15887,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: # If this is part of a matched album download, we TRUST the context data completely. # This is the exact logic from downloads.py. if is_album_download and spotify_album_context: - print("✅ Matched Album context found. Prioritizing pre-matched Spotify data.") + print("Matched Album context found. Prioritizing pre-matched Spotify data.") # We exclusively use the track number and title that were matched # *before* the download started. We do not try to re-parse the filename. @@ -15921,7 +15921,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: if album_name_to_use: track_title = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown') - print(f"🎯 ALBUM-AWARE SEARCH ({album_source}): Looking for '{track_title}' in album '{album_name_to_use}'") + print(f"ALBUM-AWARE SEARCH ({album_source}): Looking for '{track_title}' in album '{album_name_to_use}'") # Temporarily set the album for the search original_album = original_search.get('album') @@ -15930,10 +15930,10 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: try: album_result = _search_track_in_album_context_web(context, artist) if album_result: - print(f"✅ PRIORITY 1 SUCCESS: Found track using {album_source} album name - FORCING album classification") + print(f"PRIORITY 1 SUCCESS: Found track using {album_source} album name - FORCING album classification") return album_result else: - print(f"⚠️ PRIORITY 1 FAILED: Track not found using {album_source} album name") + print(f"PRIORITY 1 FAILED: Track not found using {album_source} album name") finally: # Restore original album value if original_album is not None: @@ -15942,13 +15942,13 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: original_search.pop('album', None) # PRIORITY 2: Fallback to individual track search for clean metadata - print(f"🔍 Searching Spotify for individual track info (PRIORITY 2)...") + print(f"Searching Spotify for individual track info (PRIORITY 2)...") # Clean the track title before searching - remove artist prefix # Prioritize clean Spotify title over filename-parsed title track_title_to_use = original_search.get('spotify_clean_title') or original_search.get('title', '') clean_title = _clean_track_title_web(track_title_to_use, artist_name) - print(f"🧹 Cleaned title: '{track_title_to_use}' -> '{clean_title}'") + print(f"Cleaned title: '{track_title_to_use}' -> '{clean_title}'") # Search for the track by artist and cleaned title query = f"artist:{artist_name} track:{clean_title}" @@ -15987,25 +15987,25 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: # If we found a good Spotify match, use it for clean metadata if best_match and best_confidence > 0.75: - print(f"✅ Found matching Spotify track: '{best_match.name}' - Album: '{best_match.album}' (confidence: {best_confidence:.2f})") + print(f"Found matching Spotify track: '{best_match.name}' - Album: '{best_match.album}' (confidence: {best_confidence:.2f})") # Get detailed track information using Spotify's track API detailed_track = None if hasattr(best_match, 'id') and best_match.id: - print(f"🔍 Getting detailed track info from Spotify API for track ID: {best_match.id}") + print(f"Getting detailed track info from Spotify API for track ID: {best_match.id}") detailed_track = spotify_client.get_track_details(best_match.id) # Use detailed track data if available if detailed_track: - print(f"✅ Got detailed track data from Spotify API") + print(f"Got detailed track data from Spotify API") album_name = _clean_album_title_web(detailed_track['album']['name'], artist_name) clean_track_name = detailed_track['name'] # Use Spotify's clean track name album_type = detailed_track['album'].get('album_type', 'album') total_tracks = detailed_track['album'].get('total_tracks', 1) spotify_track_number = detailed_track.get('track_number', 1) - print(f"📀 Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})") - print(f"🎵 Clean track name from Spotify: '{clean_track_name}'") + print(f"Spotify album info: '{album_name}' (type: {album_type}, total_tracks: {total_tracks}, track#: {spotify_track_number})") + print(f"Clean track name from Spotify: '{clean_track_name}'") # Enhanced album detection using detailed API data (GUI PARITY) is_album = ( @@ -16023,7 +16023,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: if detailed_track['album'].get('images'): album_image_url = detailed_track['album']['images'][0].get('url') - print(f"📊 Album classification: {is_album} (type={album_type}, tracks={total_tracks})") + print(f"Album classification: {is_album} (type={album_type}, tracks={total_tracks})") return { 'is_album': is_album, @@ -16036,7 +16036,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: } # Fallback: Use original data with basic cleaning - print("⚠️ No good Spotify match found, using original data") + print("No good Spotify match found, using original data") fallback_title = _clean_track_title_web(original_search.get('title', 'Unknown Track'), artist_name) # Preserve track_number from context if available (playlist sync tracks have it) @@ -16054,7 +16054,7 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict: } except Exception as e: - print(f"❌ Error in _detect_album_info_web: {e}") + print(f"Error in _detect_album_info_web: {e}") clean_title = _clean_track_title_web(context.get("original_search_result", {}).get('title', 'Unknown'), artist.get('name', '')) _err_tn = (context.get("original_search_result", {}).get('track_number') or context.get('track_info', {}).get('track_number') @@ -16120,10 +16120,10 @@ def _sweep_empty_download_directories(): pass # Directory not actually empty or locked — skip silently if removed > 0: - print(f"🧹 [Folder Cleanup] Removed {removed} empty director{'y' if removed == 1 else 'ies'} from downloads folder") + print(f"[Folder Cleanup] Removed {removed} empty director{'y' if removed == 1 else 'ies'} from downloads folder") return removed except Exception as e: - print(f"⚠️ [Folder Cleanup] Error sweeping empty directories: {e}") + print(f"[Folder Cleanup] Error sweeping empty directories: {e}") return 0 @@ -16183,7 +16183,7 @@ def _detect_deluxe_edition(album_name: str) -> bool: for indicator in deluxe_indicators: if indicator in album_lower: - print(f"🎯 Detected deluxe edition: '{album_name}' contains '{indicator}'") + print(f"Detected deluxe edition: '{album_name}' contains '{indicator}'") return True return False @@ -16206,7 +16206,7 @@ def _normalize_base_album_name(base_album: str, artist_name: str) -> str: # Check for exact matches in our corrections for variant, correction in known_corrections.items(): if normalized_lower == variant.lower(): - print(f"📀 Album correction applied: '{base_album}' -> '{correction}'") + print(f"Album correction applied: '{base_album}' -> '{correction}'") return correction # Handle punctuation variations @@ -16217,7 +16217,7 @@ def _normalize_base_album_name(base_album: str, artist_name: str) -> str: normalized = re.sub(r'\s+', ' ', normalized) # Clean multiple spaces normalized = normalized.strip() - print(f"📀 Album variant normalization: '{base_album}' -> '{normalized}'") + print(f"Album variant normalization: '{base_album}' -> '{normalized}'") return normalized def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: str = None) -> str: @@ -16250,10 +16250,10 @@ def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: # Check if we already have a cached result for this album if album_key in album_name_cache: cached_name = album_name_cache[album_key] - print(f"🔍 Using cached album name for '{album_key}': '{cached_name}'") + print(f"Using cached album name for '{album_key}': '{cached_name}'") return cached_name - print(f"🔍 Album grouping - Key: '{album_key}', Detected: '{detected_album}'") + print(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'") # Check if this track indicates a deluxe edition is_deluxe_track = False @@ -16267,7 +16267,7 @@ def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: # SMART ALGORITHM: Upgrade to deluxe if this track is deluxe if is_deluxe_track and current_edition == "standard": - print(f"🎯 UPGRADE: Album '{base_album}' upgraded from standard to deluxe!") + print(f"UPGRADE: Album '{base_album}' upgraded from standard to deluxe!") album_editions[album_key] = "deluxe" current_edition = "deluxe" @@ -16282,12 +16282,12 @@ def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: album_name_cache[album_key] = final_album_name album_artists[album_key] = artist_name - print(f"🔗 Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})") + print(f"Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})") return final_album_name except Exception as e: - print(f"❌ Error resolving album group: {e}") + print(f"Error resolving album group: {e}") return album_info.get('album_name', 'Unknown Album') def _clean_album_title_web(album_title: str, artist_name: str) -> str: @@ -16297,7 +16297,7 @@ def _clean_album_title_web(album_title: str, artist_name: str) -> str: # Start with the original title original = album_title.strip() cleaned = original - print(f"🧹 Album Title Cleaning: '{original}' (artist: '{artist_name}')") + print(f"Album Title Cleaning: '{original}' (artist: '{artist_name}')") # Remove "Album - " prefix cleaned = re.sub(r'^Album\s*-\s*', '', cleaned, flags=re.IGNORECASE) @@ -16336,7 +16336,7 @@ def _clean_album_title_web(album_title: str, artist_name: str) -> str: # Remove leading/trailing punctuation cleaned = re.sub(r'^[-\s]+|[-\s]+$', '', cleaned) - print(f"🧹 Album Title Result: '{original}' -> '{cleaned}'") + print(f"Album Title Result: '{original}' -> '{cleaned}'") return cleaned if cleaned else original def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> dict: @@ -16355,10 +16355,10 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d artist_name = spotify_artist["name"] if not album_name or not track_title: - print(f"❌ Album-aware search failed: Missing album ({album_name}) or track ({track_title})") + print(f"Album-aware search failed: Missing album ({album_name}) or track ({track_title})") return None - print(f"🎯 Album-aware search: '{track_title}' in album '{album_name}' by '{artist_name}'") + print(f"Album-aware search: '{track_title}' in album '{album_name}' by '{artist_name}'") # Clean the album name for better search results clean_album = _clean_album_title_web(album_name, artist_name) @@ -16366,21 +16366,21 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d # Search for the specific album first album_query = f"album:{clean_album} artist:{artist_name}" - print(f"🔍 Searching albums: {album_query}") + print(f"Searching albums: {album_query}") albums = spotify_client.search_albums(album_query, limit=5) if not albums: - print(f"❌ No albums found for query: {album_query}") + print(f"No albums found for query: {album_query}") return None # Check each album to see if our track is in it for album in albums: - print(f"🎵 Checking album: '{album.name}' ({album.total_tracks} tracks)") + print(f"Checking album: '{album.name}' ({album.total_tracks} tracks)") # Get tracks from this album album_tracks_data = spotify_client.get_album_tracks(album.id) if not album_tracks_data or 'items' not in album_tracks_data: - print(f"❌ Could not get tracks for album: {album.name}") + print(f"Could not get tracks for album: {album.name}") continue # Check if our track is in this album @@ -16399,7 +16399,7 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d threshold = 0.9 if is_remix else 0.65 # Lower threshold to favor album matches over singles if similarity > threshold: - print(f"✅ FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})") + print(f"FOUND: '{track_name}' (track #{track_number}) matches '{clean_track}' (similarity: {similarity:.2f})") # Classify as album vs single using same logic as _detect_album_info_web ctx_album_type = getattr(album, 'album_type', 'album') or 'album' @@ -16410,7 +16410,7 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d matching_engine.normalize_string(album.name) != matching_engine.normalize_string(clean_track) and matching_engine.normalize_string(album.name) != matching_engine.normalize_string(artist_name) ) - print(f"📊 Album context classification: is_album={ctx_is_album} (type={ctx_album_type}, tracks={ctx_total_tracks})") + print(f"Album context classification: is_album={ctx_is_album} (type={ctx_album_type}, tracks={ctx_total_tracks})") return { 'is_album': ctx_is_album, @@ -16422,13 +16422,13 @@ def _search_track_in_album_context_web(context: dict, spotify_artist: dict) -> d 'source': 'album_context_search' } - print(f"❌ Track '{clean_track}' not found in album '{album.name}'") + print(f"Track '{clean_track}' not found in album '{album.name}'") - print(f"❌ Track '{clean_track}' not found in any matching albums") + print(f"Track '{clean_track}' not found in any matching albums") return None except Exception as e: - print(f"❌ Error in album-aware search: {e}") + print(f"Error in album-aware search: {e}") return None def _clean_track_title_web(track_title: str, artist_name: str) -> str: @@ -16438,7 +16438,7 @@ def _clean_track_title_web(track_title: str, artist_name: str) -> str: # Start with the original title original = track_title.strip() cleaned = original - print(f"🧹 Track Title Cleaning: '{original}' (artist: '{artist_name}')") + print(f"Track Title Cleaning: '{original}' (artist: '{artist_name}')") # Remove artist name prefix if it appears at the beginning # This handles cases like "Kendrick Lamar - HUMBLE." @@ -16467,7 +16467,7 @@ def _clean_track_title_web(track_title: str, artist_name: str) -> str: # Remove leading/trailing punctuation cleaned = re.sub(r'^[-\s]+|[-\s]+$', '', cleaned) - print(f"🧹 Track Title Result: '{original}' -> '{cleaned}'") + print(f"Track Title Result: '{original}' -> '{cleaned}'") return cleaned if cleaned else original @@ -16503,7 +16503,7 @@ def clean_youtube_track_title(title, artist_name=None): cleaned_title = re.sub(artist_pattern, '', title, flags=re.IGNORECASE).strip() if cleaned_title != title: - print(f"🎯 Removed artist from start: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')") + print(f"Removed artist from start: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')") title = cleaned_title artist_removed = True else: @@ -16513,7 +16513,7 @@ def clean_youtube_track_title(title, artist_name=None): cleaned_title = re.sub(artist_end_pattern, '', title, flags=re.IGNORECASE).strip() if cleaned_title != title: - print(f"🎯 Removed artist from end: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')") + print(f"Removed artist from end: '{title}' -> '{cleaned_title}' (artist: '{artist_name}')") title = cleaned_title artist_removed = True @@ -16575,7 +16575,7 @@ def clean_youtube_track_title(title, artist_name=None): title = re.sub(rf'\b{re.escape(artist_name)}\s*[-–—:]\s*', '', title, flags=re.IGNORECASE) title = re.sub(rf'^{re.escape(artist_name)}\s*[-–—:]\s*', '', title, flags=re.IGNORECASE) else: - print(f"⚠️ Skipping artist removal - collaboration detected: '{title}'") + print(f"Skipping artist removal - collaboration detected: '{title}'") # Remove "prod. Producer" patterns title = re.sub(r'\s+prod\.?\s+\S+', '', title, flags=re.IGNORECASE) @@ -16603,7 +16603,7 @@ def clean_youtube_track_title(title, artist_name=None): title = original_title if title != original_title: - print(f"🧹 YouTube title cleaned: '{original_title}' → '{title}'") + print(f"YouTube title cleaned: '{original_title}' → '{title}'") return title @@ -16668,7 +16668,7 @@ def clean_youtube_artist(artist_string): artist_string = original_artist if artist_string != original_artist: - print(f"🧹 YouTube artist cleaned: '{original_artist}' → '{artist_string}'") + print(f"YouTube artist cleaned: '{original_artist}' → '{artist_string}'") return artist_string @@ -16695,14 +16695,14 @@ def parse_youtube_playlist(url): playlist_info = ydl.extract_info(url, download=False) if not playlist_info: - print("❌ Could not extract playlist information") + print("Could not extract playlist information") return None playlist_name = playlist_info.get('title', 'Unknown Playlist') playlist_id = playlist_info.get('id', 'unknown_id') entries = list(playlist_info.get('entries', []) or []) - print(f"🎵 Found YouTube playlist: '{playlist_name}' with {len(entries)} entries") + print(f"Found YouTube playlist: '{playlist_name}' with {len(entries)} entries") for entry in entries: if not entry: @@ -16741,11 +16741,11 @@ def parse_youtube_playlist(url): 'source': 'youtube' } - print(f"✅ Successfully parsed YouTube playlist: {len(tracks)} tracks extracted") + print(f"Successfully parsed YouTube playlist: {len(tracks)} tracks extracted") return playlist_data except Exception as e: - print(f"❌ Error parsing YouTube playlist: {e}") + print(f"Error parsing YouTube playlist: {e}") return None @@ -16825,7 +16825,7 @@ def _build_final_path_for_track(context, spotify_artist, album_info, file_ext): original_stem = os.path.splitext(os.path.basename(original_path))[0] final_path = os.path.join(original_dir, original_stem + file_ext) os.makedirs(original_dir, exist_ok=True) - print(f"🔄 [Enhance] Using original file location: {final_path}") + print(f"[Enhance] Using original file location: {final_path}") return final_path, True # Extract year and album_type from spotify_album for template use (safe for all modes) @@ -17199,13 +17199,13 @@ def _downsample_hires_flac(final_path, context): original_bits = audio.info.bits_per_sample original_rate = audio.info.sample_rate except Exception as e: - print(f"⚠️ [Downsample] Could not read FLAC info: {e}") + print(f"[Downsample] Could not read FLAC info: {e}") return None if original_bits <= 16 and original_rate <= 44100: return None # Already CD quality or below - print(f"📀 [Downsample] Converting {original_bits}-bit/{original_rate}Hz → 16-bit/44100Hz: {os.path.basename(final_path)}") + print(f"[Downsample] Converting {original_bits}-bit/{original_rate}Hz → 16-bit/44100Hz: {os.path.basename(final_path)}") ffmpeg_bin = shutil.which('ffmpeg') if not ffmpeg_bin: @@ -17213,7 +17213,7 @@ def _downsample_hires_flac(final_path, context): if os.path.isfile(local): ffmpeg_bin = local else: - print("⚠️ [Downsample] ffmpeg not found — skipping hi-res conversion") + print("[Downsample] ffmpeg not found — skipping hi-res conversion") return None temp_path = final_path + '.tmp.flac' @@ -17228,27 +17228,27 @@ def _downsample_hires_flac(final_path, context): ], capture_output=True, text=True, timeout=300) if result.returncode != 0: - print(f"⚠️ [Downsample] ffmpeg failed: {result.stderr[:200]}") + print(f"[Downsample] ffmpeg failed: {result.stderr[:200]}") if os.path.exists(temp_path): os.remove(temp_path) return None # Verify the output is a valid 16-bit FLAC if not os.path.isfile(temp_path) or os.path.getsize(temp_path) == 0: - print(f"⚠️ [Downsample] Output file missing or empty") + print(f"[Downsample] Output file missing or empty") if os.path.exists(temp_path): os.remove(temp_path) return None verify_audio = FLAC(temp_path) if verify_audio.info.bits_per_sample != 16: - print(f"⚠️ [Downsample] Output not 16-bit ({verify_audio.info.bits_per_sample}-bit), aborting") + print(f"[Downsample] Output not 16-bit ({verify_audio.info.bits_per_sample}-bit), aborting") os.remove(temp_path) return None # Atomic swap — replace original with downsampled version os.replace(temp_path, final_path) - print(f"✅ [Downsample] Converted to 16-bit/44.1kHz: {os.path.basename(final_path)}") + print(f"[Downsample] Converted to 16-bit/44.1kHz: {os.path.basename(final_path)}") # Update QUALITY tag in the new file new_quality = 'FLAC 16bit' @@ -17257,7 +17257,7 @@ def _downsample_hires_flac(final_path, context): updated_audio['QUALITY'] = new_quality updated_audio.save() except Exception as tag_err: - print(f"⚠️ [Downsample] Could not update QUALITY tag: {tag_err}") + print(f"[Downsample] Could not update QUALITY tag: {tag_err}") # Update context so downstream (lossy copy, metadata) reflects new quality old_quality = context.get('_audio_quality', '') @@ -17269,7 +17269,7 @@ def _downsample_hires_flac(final_path, context): new_path = os.path.join(os.path.dirname(final_path), new_basename) try: os.rename(final_path, new_path) - print(f"📝 [Downsample] Renamed: {os.path.basename(final_path)} → {new_basename}") + print(f"[Downsample] Renamed: {os.path.basename(final_path)} → {new_basename}") # Rename matching lyrics sidecar file if it exists (.lrc or .txt) for lyrics_ext in ('.lrc', '.txt'): old_lyrics = os.path.splitext(final_path)[0] + lyrics_ext @@ -17278,16 +17278,16 @@ def _downsample_hires_flac(final_path, context): os.rename(old_lyrics, new_lyrics) return new_path except Exception as rename_err: - print(f"⚠️ [Downsample] Could not rename file: {rename_err}") + print(f"[Downsample] Could not rename file: {rename_err}") return final_path except subprocess.TimeoutExpired: - print(f"⚠️ [Downsample] Conversion timed out for: {os.path.basename(final_path)}") + print(f"[Downsample] Conversion timed out for: {os.path.basename(final_path)}") if os.path.exists(temp_path): os.remove(temp_path) except Exception as e: - print(f"⚠️ [Downsample] Conversion error: {e}") + print(f"[Downsample] Conversion error: {e}") if os.path.exists(temp_path): try: os.remove(temp_path) @@ -17327,7 +17327,7 @@ def _create_lossy_copy(final_path): } if codec not in codec_map: - print(f"⚠️ [Lossy Copy] Unknown codec '{codec}' — skipping conversion") + print(f"[Lossy Copy] Unknown codec '{codec}' — skipping conversion") return None ffmpeg_codec, out_ext, quality_label, extra_args = codec_map[codec] @@ -17347,11 +17347,11 @@ def _create_lossy_copy(final_path): if os.path.isfile(local): ffmpeg_bin = local else: - print(f"⚠️ [Lossy Copy] ffmpeg not found — skipping {codec.upper()} conversion") + print(f"[Lossy Copy] ffmpeg not found — skipping {codec.upper()} conversion") return None try: - print(f"🎵 [Lossy Copy] Converting to {quality_label}: {os.path.basename(final_path)}") + print(f"[Lossy Copy] Converting to {quality_label}: {os.path.basename(final_path)}") cmd = [ ffmpeg_bin, '-i', final_path, '-codec:a', ffmpeg_codec, @@ -17362,7 +17362,7 @@ def _create_lossy_copy(final_path): result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode == 0: - print(f"✅ [Lossy Copy] Created {quality_label} copy: {os.path.basename(out_path)}") + print(f"[Lossy Copy] Created {quality_label} copy: {os.path.basename(out_path)}") # Fix QUALITY tag — the FLAC's tag was copied verbatim by ffmpeg try: @@ -17379,7 +17379,7 @@ def _create_lossy_copy(final_path): audio['----:com.apple.iTunes:QUALITY'] = [MP4FreeForm(quality_label.encode('utf-8'))] audio.save() except Exception as tag_err: - print(f"⚠️ [Lossy Copy] Could not update QUALITY tag: {tag_err}") + print(f"[Lossy Copy] Could not update QUALITY tag: {tag_err}") # Embed cover art from source FLAC into the lossy copy # Opus/OGG can't inherit FLAC cover art via ffmpeg -map_metadata alone @@ -17409,7 +17409,7 @@ def _create_lossy_copy(final_path): pic.depth = 0 pic.colors = 0 pic.data = img_data - print(f"🎨 [Lossy Copy] Using cover.jpg as art source (FLAC had no embedded art)") + print(f"[Lossy Copy] Using cover.jpg as art source (FLAC had no embedded art)") except Exception: pass @@ -17434,15 +17434,15 @@ def _create_lossy_copy(final_path): ) dest_audio['METADATA_BLOCK_PICTURE'] = [base64.b64encode(picture_data).decode('ascii')] dest_audio.save() - print(f"🎨 [Lossy Copy] Embedded cover art in Opus file") + print(f"[Lossy Copy] Embedded cover art in Opus file") elif codec == 'aac': from mutagen.mp4 import MP4Cover fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)] dest_audio.save() - print(f"🎨 [Lossy Copy] Embedded cover art in M4A file") + print(f"[Lossy Copy] Embedded cover art in M4A file") except Exception as art_err: - print(f"⚠️ [Lossy Copy] Could not embed cover art: {art_err}") + print(f"[Lossy Copy] Could not embed cover art: {art_err}") # Blasphemy Mode: delete original FLAC if enabled and output is verified if config_manager.get('lossy_copy.delete_original', False): @@ -17458,7 +17458,7 @@ def _create_lossy_copy(final_path): except Exception: pass os.remove(final_path) - print(f"🔥 [Blasphemy Mode] Deleted original: {os.path.basename(final_path)}") + print(f"[Blasphemy Mode] Deleted original: {os.path.basename(final_path)}") # Rename lyrics sidecar file to match the output filename for lyrics_ext in ('.lrc', '.txt'): src_lyrics = os.path.splitext(final_path)[0] + lyrics_ext @@ -17466,22 +17466,22 @@ def _create_lossy_copy(final_path): dst_lyrics = os.path.splitext(out_path)[0] + lyrics_ext try: os.rename(src_lyrics, dst_lyrics) - print(f"🔥 [Blasphemy Mode] Renamed {lyrics_ext}: {os.path.basename(src_lyrics)} -> {os.path.basename(dst_lyrics)}") + print(f"[Blasphemy Mode] Renamed {lyrics_ext}: {os.path.basename(src_lyrics)} -> {os.path.basename(dst_lyrics)}") except Exception as lrc_err: - print(f"⚠️ [Blasphemy Mode] Could not rename {lyrics_ext}: {lrc_err}") + print(f"[Blasphemy Mode] Could not rename {lyrics_ext}: {lrc_err}") return out_path else: - print(f"⚠️ [Blasphemy Mode] Output failed audio validation, keeping original: {os.path.basename(final_path)}") + print(f"[Blasphemy Mode] Output failed audio validation, keeping original: {os.path.basename(final_path)}") else: - print(f"⚠️ [Blasphemy Mode] Output missing or empty, keeping original: {os.path.basename(final_path)}") + print(f"[Blasphemy Mode] Output missing or empty, keeping original: {os.path.basename(final_path)}") except Exception as del_err: - print(f"⚠️ [Blasphemy Mode] Error during original deletion, keeping original: {del_err}") + print(f"[Blasphemy Mode] Error during original deletion, keeping original: {del_err}") else: - print(f"⚠️ [Lossy Copy] ffmpeg failed: {result.stderr[:200]}") + print(f"[Lossy Copy] ffmpeg failed: {result.stderr[:200]}") except subprocess.TimeoutExpired: - print(f"⚠️ [Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}") + print(f"[Lossy Copy] Conversion timed out for: {os.path.basename(final_path)}") except Exception as e: - print(f"⚠️ [Lossy Copy] Conversion error: {e}") + print(f"[Lossy Copy] Conversion error: {e}") return None def _apply_path_template(template: str, context: dict) -> str: @@ -17672,11 +17672,11 @@ def _strip_all_non_audio_tags(file_path: str) -> dict: apev2_tags.delete(file_path) summary['apev2_stripped'] = True summary['apev2_tag_count'] = tag_count - print(f"🧹 Stripped {tag_count} APEv2 tags: {', '.join(tag_keys[:10])}") + print(f"Stripped {tag_count} APEv2 tags: {', '.join(tag_keys[:10])}") except APENoHeaderError: pass # No APEv2 tags — common case except Exception as e: - print(f"⚠️ Could not strip APEv2 tags (non-fatal): {e}") + print(f"Could not strip APEv2 tags (non-fatal): {e}") return summary def _verify_metadata_written(file_path: str) -> bool: @@ -17684,7 +17684,7 @@ def _verify_metadata_written(file_path: str) -> bool: try: check = MutagenFile(file_path) if check is None or check.tags is None: - print(f"❌ [VERIFY] Tags are None after save: {file_path}") + print(f"[VERIFY] Tags are None after save: {file_path}") return False title_found = False artist_found = False @@ -17694,7 +17694,7 @@ def _verify_metadata_written(file_path: str) -> bool: # Confirm APEv2 is gone try: APEv2(file_path) - print(f"⚠️ [VERIFY] APEv2 tags still present after processing!") + print(f"[VERIFY] APEv2 tags still present after processing!") return False except APENoHeaderError: pass @@ -17705,12 +17705,12 @@ def _verify_metadata_written(file_path: str) -> bool: title_found = bool(check.get('\xa9nam')) artist_found = bool(check.get('\xa9ART')) if not title_found or not artist_found: - print(f"❌ [VERIFY] Missing metadata - title:{title_found} artist:{artist_found}") + print(f"[VERIFY] Missing metadata - title:{title_found} artist:{artist_found}") return False - print(f"✅ [VERIFY] Metadata verified OK") + print(f"[VERIFY] Metadata verified OK") return True except Exception as e: - print(f"⚠️ [VERIFY] Verification error (non-fatal): {e}") + print(f"[VERIFY] Verification error (non-fatal): {e}") return False def _is_ogg_opus(audio_file): @@ -17728,20 +17728,20 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in which stripped the ID3v2 header from MP3 files, leaving them tagless. """ if not config_manager.get('metadata_enhancement.enabled', True): - print("🎵 Metadata enhancement disabled in config.") + print("Metadata enhancement disabled in config.") return True # Acquire per-file lock to prevent concurrent metadata writes to the same file file_lock = _get_file_lock(file_path) with file_lock: - print(f"🎵 Enhancing metadata for: {os.path.basename(file_path)}") + print(f"Enhancing metadata for: {os.path.basename(file_path)}") try: # Strip APEv2 tags from MP3 (invisible to ID3 handler) strip_summary = _strip_all_non_audio_tags(file_path) audio_file = MutagenFile(file_path) if audio_file is None: - print(f"❌ Could not load audio file with Mutagen: {file_path}") + print(f"Could not load audio file with Mutagen: {file_path}") return False # ── Wipe ALL existing tags and save immediately ── @@ -17756,7 +17756,7 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in if audio_file.tags is not None: if len(audio_file.tags) > 0: tag_keys = list(audio_file.tags.keys())[:15] - print(f"🧹 Clearing {len(audio_file.tags)} existing tags: " + print(f"Clearing {len(audio_file.tags)} existing tags: " f"{', '.join(str(k) for k in tag_keys)}") audio_file.tags.clear() else: @@ -17772,7 +17772,7 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in metadata = _extract_spotify_metadata(context, artist, album_info) if not metadata: - print("⚠️ Could not extract Spotify metadata, saving with cleared tags.") + print("Could not extract Spotify metadata, saving with cleared tags.") if isinstance(audio_file.tags, ID3): audio_file.save(v1=0, v2_version=4) elif isinstance(audio_file, FLAC): @@ -17875,18 +17875,18 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in # Verify metadata was written verified = _verify_metadata_written(file_path) if verified: - print("✅ Metadata enhanced successfully.") + print("Metadata enhanced successfully.") else: - print("⚠️ Metadata saved but verification found issues (see above).") + print("Metadata saved but verification found issues (see above).") return True except Exception as e: import traceback - print(f"❌ Error enhancing metadata for {file_path}: {e}") - print(f"❌ [Metadata Debug] Exception type: {type(e).__name__}") - print(f"❌ [Metadata Debug] File exists: {os.path.exists(file_path)}") - print(f"❌ [Metadata Debug] Artist: {artist.get('name', 'MISSING') if artist else 'None'}") - print(f"❌ [Metadata Debug] Album info: {album_info.get('album_name', 'MISSING') if album_info else 'None'}") - print(f"❌ [Metadata Debug] Traceback:\n{traceback.format_exc()}") + print(f"Error enhancing metadata for {file_path}: {e}") + print(f"[Metadata Debug] Exception type: {type(e).__name__}") + print(f"[Metadata Debug] File exists: {os.path.exists(file_path)}") + print(f"[Metadata Debug] Artist: {artist.get('name', 'MISSING') if artist else 'None'}") + print(f"[Metadata Debug] Album info: {album_info.get('album_name', 'MISSING') if album_info else 'None'}") + print(f"[Metadata Debug] Traceback:\n{traceback.format_exc()}") return False def _generate_lrc_file(file_path: str, context: dict, artist: dict, album_info: dict) -> bool: @@ -17935,14 +17935,14 @@ def _generate_lrc_file(file_path: str, context: dict, artist: dict, album_info: ) if success: - print(f"🎵 LRC file generated for: {track_name}") + print(f"LRC file generated for: {track_name}") else: - print(f"🎵 No lyrics found for: {track_name}") + print(f"No lyrics found for: {track_name}") return success except Exception as e: - print(f"❌ Error generating LRC file for {file_path}: {e}") + print(f"Error generating LRC file for {file_path}: {e}") return False def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> dict: @@ -17954,15 +17954,15 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> # Priority 1: Spotify clean title from context if original_search.get('spotify_clean_title'): metadata['title'] = original_search['spotify_clean_title'] - print(f"🎵 Metadata: Using Spotify clean title: '{metadata['title']}'") + print(f"Metadata: Using Spotify clean title: '{metadata['title']}'") # Priority 2: Album info clean name elif album_info.get('clean_track_name'): metadata['title'] = album_info['clean_track_name'] - print(f"🎵 Metadata: Using album info clean name: '{metadata['title']}'") + print(f"Metadata: Using album info clean name: '{metadata['title']}'") # Priority 3: Original title as fallback else: metadata['title'] = original_search.get('title', '') - print(f"🎵 Metadata: Using original title as fallback: '{metadata['title']}'") + print(f"Metadata: Using original title as fallback: '{metadata['title']}'") # Handle multiple artists from Spotify data original_search = context.get("original_search_result", {}) if 'artists' in original_search and isinstance(original_search['artists'], list) and len(original_search['artists']) > 0: @@ -17976,11 +17976,11 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> else: all_artists.append(str(a)) metadata['artist'] = ', '.join(all_artists) - print(f"🎵 Metadata: Using all artists: '{metadata['artist']}'") + print(f"Metadata: Using all artists: '{metadata['artist']}'") else: # Fallback to single artist metadata['artist'] = artist.get('name', '') - print(f"🎵 Metadata: Using primary artist: '{metadata['artist']}'") + print(f"Metadata: Using primary artist: '{metadata['artist']}'") # Resolve album_artist for consistent tagging across all tracks in an album. # Priority: 1) explicit batch artist context (same artist for whole album) @@ -18040,18 +18040,18 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> track_num = album_info.get('track_number', 1) metadata['track_number'] = track_num metadata['total_tracks'] = spotify_album.get('total_tracks', 1) if spotify_album else 1 - print(f"🎵 [METADATA] Album track - track_number: {track_num}, album: {metadata['album']}") + print(f"[METADATA] Album track - track_number: {track_num}, album: {metadata['album']}") else: # SAFEGUARD: If we have spotify_album context, never use track title as album name # This prevents album tracks from being tagged as singles due to classification errors if spotify_album and spotify_album.get('name'): - print(f"🛡️ [SAFEGUARD] Using spotify_album name instead of track title for album metadata") + print(f"[SAFEGUARD] Using spotify_album name instead of track title for album metadata") metadata['album'] = spotify_album['name'] # Use corrected track_number from album_info (which should be updated by post-processing) corrected_track_number = album_info.get('track_number', 1) if album_info else 1 metadata['track_number'] = corrected_track_number metadata['total_tracks'] = spotify_album.get('total_tracks', 1) - print(f"🛡️ [SAFEGUARD] Using track_number: {corrected_track_number}") + print(f"[SAFEGUARD] Using track_number: {corrected_track_number}") else: metadata['album'] = metadata['title'] # For true singles, album is the title metadata['track_number'] = 1 @@ -18100,7 +18100,7 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> metadata['spotify_album_id'] = album_id # Summary log for debugging metadata issues (e.g. wrong album_artist / track_number) - print(f"📋 [Metadata Summary] title='{metadata.get('title')}' | artist='{metadata.get('artist')}' | album_artist='{metadata.get('album_artist')}' | album='{metadata.get('album')}' | track={metadata.get('track_number')}/{metadata.get('total_tracks')} | disc={metadata.get('disc_number')}") + print(f"[Metadata Summary] title='{metadata.get('title')}' | artist='{metadata.get('artist')}' | album_artist='{metadata.get('album_artist')}' | album='{metadata.get('album')}' | track={metadata.get('track_number')}/{metadata.get('total_tracks')} | disc={metadata.get('disc_number')}") return metadata @@ -18147,7 +18147,7 @@ def _embed_album_art_metadata(audio_file, metadata: dict): image_data = response.read() mime_type = response.info().get_content_type() or 'image/jpeg' if image_data and len(image_data) > 1000: - print(f"🎨 Cover art from Cover Art Archive ({len(image_data) // 1024}KB)") + print(f"Cover art from Cover Art Archive ({len(image_data) // 1024}KB)") else: image_data = None # Too small, likely an error page except Exception: @@ -18157,14 +18157,14 @@ def _embed_album_art_metadata(audio_file, metadata: dict): if not image_data: art_url = metadata.get('album_art_url') if not art_url: - print("🎨 No album art URL available for embedding.") + print("No album art URL available for embedding.") return with urllib.request.urlopen(art_url, timeout=10) as response: image_data = response.read() mime_type = response.info().get_content_type() if not image_data: - print("❌ Failed to download album art data.") + print("Failed to download album art data.") return # MP3 (ID3) @@ -18187,9 +18187,9 @@ def _embed_album_art_metadata(audio_file, metadata: dict): fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in mime_type else MP4Cover.FORMAT_PNG audio_file['covr'] = [MP4Cover(image_data, imageformat=fmt)] - print("🎨 Album art successfully embedded.") + print("Album art successfully embedded.") except Exception as e: - print(f"❌ Error embedding album art: {e}") + print(f"Error embedding album art: {e}") def _embed_source_ids(audio_file, metadata: dict): """ @@ -18380,13 +18380,13 @@ def _embed_source_ids(audio_file, metadata: dict): """, (int(release_year), _pp_album_name, _pp_artist_name)) if cursor.rowcount > 0: conn.commit() - print(f"📅 Updated album year to {release_year} in database") + print(f"Updated album year to {release_year} in database") else: conn.rollback() finally: conn.close() except Exception as e: - print(f"⚠️ Could not update album year in DB: {e}") + print(f"Could not update album year in DB: {e}") # (All source lookups now handled by _pp_lookup_* functions called via configurable order above) if False: # Dead code — old inline blocks preserved for reference during transition @@ -18458,10 +18458,10 @@ def _embed_source_ids(audio_file, metadata: dict): break except (ValueError, TypeError): pass - print(f"🎵 MusicBrainz release details: type={primary_type or '?'}, " + print(f"MusicBrainz release details: type={primary_type or '?'}, " f"country={country or '?'}, media={id_tags.get('MEDIA', '?')}") except Exception as e: - print(f"⚠️ MusicBrainz release detail lookup failed (non-fatal): {e}") + print(f"MusicBrainz release detail lookup failed (non-fatal): {e}") # ── 2b. Deezer lookup for BPM, ISRC fallback, and source IDs ── deezer_bpm = None @@ -18480,7 +18480,7 @@ def _embed_source_ids(audio_file, metadata: dict): dz_artist_id = dz_result.get('artist', {}).get('id') if dz_artist_id: id_tags['DEEZER_ARTIST_ID'] = str(dz_artist_id) - print(f"🎵 Deezer track matched: {dz_track_id}") + print(f"Deezer track matched: {dz_track_id}") # Get full track details for BPM and ISRC dz_details = dz_client.get_track_details(dz_track_id) @@ -18492,9 +18492,9 @@ def _embed_source_ids(audio_file, metadata: dict): if dz_isrc: deezer_isrc = dz_isrc else: - print("⚠️ Deezer worker not available, skipping Deezer lookup") + print("Deezer worker not available, skipping Deezer lookup") except Exception as e: - print(f"⚠️ Deezer lookup failed (non-fatal): {e}") + print(f"Deezer lookup failed (non-fatal): {e}") # ── 2c. AudioDB lookup for mood, style, genre, and source ID ── audiodb_mood = None @@ -18512,13 +18512,13 @@ def _embed_source_ids(audio_file, metadata: dict): adb_track_id = adb_result.get('idTrack') if adb_track_id: id_tags['AUDIODB_TRACK_ID'] = str(adb_track_id) - print(f"🎵 AudioDB track matched: {adb_track_id}") + print(f"AudioDB track matched: {adb_track_id}") # Use AudioDB's MusicBrainz IDs as fallbacks for any missing from MB lookup adb_mb_track = adb_result.get('strMusicBrainzID') if adb_mb_track and 'MUSICBRAINZ_RECORDING_ID' not in id_tags: id_tags['MUSICBRAINZ_RECORDING_ID'] = adb_mb_track recording_mbid = adb_mb_track - print(f"🎵 MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}") + print(f"MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}") # NOTE: AudioDB's strMusicBrainzAlbumID is intentionally # NOT used as a fallback for MUSICBRAINZ_RELEASE_ID. # AudioDB links each track to its original album in MB, @@ -18529,14 +18529,14 @@ def _embed_source_ids(audio_file, metadata: dict): if adb_mb_artist and 'MUSICBRAINZ_ARTIST_ID' not in id_tags: id_tags['MUSICBRAINZ_ARTIST_ID'] = adb_mb_artist artist_mbid = adb_mb_artist - print(f"🎵 MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}") + print(f"MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}") audiodb_mood = adb_result.get('strMood') or None audiodb_style = adb_result.get('strStyle') or None audiodb_genre = adb_result.get('strGenre') or None else: - print("⚠️ AudioDB worker not available, skipping AudioDB lookup") + print("AudioDB worker not available, skipping AudioDB lookup") except Exception as e: - print(f"⚠️ AudioDB lookup failed (non-fatal): {e}") + print(f"AudioDB lookup failed (non-fatal): {e}") # ── 2d. Tidal lookup for ISRC fallback, copyright, and source IDs ── tidal_isrc = None @@ -18551,7 +18551,7 @@ def _embed_source_ids(audio_file, metadata: dict): td_track_id = td_result.get('id') if td_track_id: id_tags['TIDAL_TRACK_ID'] = str(td_track_id) - print(f"🎵 Tidal track matched: {td_track_id}") + print(f"Tidal track matched: {td_track_id}") td_artist = td_result.get('artist', {}) if isinstance(td_artist, dict) and td_artist.get('id'): id_tags['TIDAL_ARTIST_ID'] = str(td_artist['id']) @@ -18568,7 +18568,7 @@ def _embed_source_ids(audio_file, metadata: dict): if td_copyright: tidal_copyright = td_copyright except Exception as e: - print(f"⚠️ Tidal lookup failed (non-fatal): {e}") + print(f"Tidal lookup failed (non-fatal): {e}") # ── 2e. Qobuz lookup for ISRC fallback, copyright, label, and source IDs ── qobuz_isrc = None @@ -18591,7 +18591,7 @@ def _embed_source_ids(audio_file, metadata: dict): qz_track_id = qz_result.get('id') if qz_track_id: id_tags['QOBUZ_TRACK_ID'] = str(qz_track_id) - print(f"🎵 Qobuz track matched: {qz_track_id}") + print(f"Qobuz track matched: {qz_track_id}") if isinstance(qz_performer, dict) and qz_performer.get('id'): id_tags['QOBUZ_ARTIST_ID'] = str(qz_performer['id']) qz_isrc = qz_result.get('isrc') @@ -18610,7 +18610,7 @@ def _embed_source_ids(audio_file, metadata: dict): if isinstance(qz_label_info, dict) and qz_label_info.get('name'): qobuz_label = qz_label_info['name'] except Exception as e: - print(f"⚠️ Qobuz lookup failed (non-fatal): {e}") + print(f"Qobuz lookup failed (non-fatal): {e}") # ── 2f. Last.fm lookup for tags (genre merge) and URL ── lastfm_tags = [] @@ -18633,9 +18633,9 @@ def _embed_source_ids(audio_file, metadata: dict): lastfm_tags = [t.get('name', '') for t in tag_list if isinstance(t, dict) and t.get('name')] elif isinstance(tag_list, dict) and tag_list.get('name'): lastfm_tags = [tag_list['name']] - print(f"🎵 Last.fm track info found: {len(lastfm_tags)} tags") + print(f"Last.fm track info found: {len(lastfm_tags)} tags") except Exception as e: - print(f"⚠️ Last.fm lookup failed (non-fatal): {e}") + print(f"Last.fm lookup failed (non-fatal): {e}") # ── 2g. Genius lookup for source ID and URL ── # Genius has an aggressive global rate limiter (30→60→120s backoff) that @@ -18649,7 +18649,7 @@ def _embed_source_ids(audio_file, metadata: dict): try: import core.genius_client as _genius_module if time.time() < _genius_module._rate_limit_until: - print("⏭️ Genius rate-limited, skipping (non-blocking)") + print("Genius rate-limited, skipping (non-blocking)") else: g_client = genius_worker.client if genius_worker else None if g_client: @@ -18658,12 +18658,12 @@ def _embed_source_ids(audio_file, metadata: dict): g_id = g_result.get('id') if g_id: id_tags['GENIUS_TRACK_ID'] = str(g_id) - print(f"🎵 Genius song matched: {g_id}") + print(f"Genius song matched: {g_id}") g_url = g_result.get('url') if g_url: genius_url = g_url except Exception as e: - print(f"⚠️ Genius lookup failed (non-fatal): {e}") + print(f"Genius lookup failed (non-fatal): {e}") if not id_tags and not deezer_bpm and not deezer_isrc and not audiodb_mood and not audiodb_style: return @@ -18751,7 +18751,7 @@ def _embed_source_ids(audio_file, metadata: dict): written.append(key) if written: - print(f"🔗 Embedded IDs: {', '.join(written)}") + print(f"Embedded IDs: {', '.join(written)}") # ── 3a½. Write date tag if discovered during lookups (initial write had no date) ── if _needs_date_tag and release_year: @@ -18761,7 +18761,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['date'] = [release_year] elif isinstance(audio_file, MP4): audio_file['\xa9day'] = [release_year] - print(f"📅 Date tag: {release_year}") + print(f"Date tag: {release_year}") # ── 3b. Write BPM tag (from Deezer) ── if _tag_enabled('deezer.tags.bpm') and deezer_bpm and deezer_bpm > 0: @@ -18772,7 +18772,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['BPM'] = [str(bpm_int)] elif isinstance(audio_file, MP4): audio_file['tmpo'] = [bpm_int] - print(f"🥁 BPM: {bpm_int}") + print(f"BPM: {bpm_int}") # ── 3c. Write mood tag (from AudioDB) ── if _tag_enabled('audiodb.tags.mood') and audiodb_mood: @@ -18782,7 +18782,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['MOOD'] = [audiodb_mood] elif isinstance(audio_file, MP4): audio_file['----:com.apple.iTunes:MOOD'] = [MP4FreeForm(audiodb_mood.encode('utf-8'))] - print(f"🎭 Mood: {audiodb_mood}") + print(f"Mood: {audiodb_mood}") # ── 3d. Write style tag (from AudioDB) ── if _tag_enabled('audiodb.tags.style') and audiodb_style: @@ -18792,7 +18792,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['STYLE'] = [audiodb_style] elif isinstance(audio_file, MP4): audio_file['----:com.apple.iTunes:STYLE'] = [MP4FreeForm(audiodb_style.encode('utf-8'))] - print(f"🎨 Style: {audiodb_style}") + print(f"Style: {audiodb_style}") # ── 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag ── if _tag_enabled('metadata_enhancement.tags.genre_merge'): @@ -18819,7 +18819,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['GENRE'] = [genre_string] elif isinstance(audio_file, MP4): audio_file['\xa9gen'] = [genre_string] - print(f"🎶 Genres merged: {genre_string}") + print(f"Genres merged: {genre_string}") # ── 5. Write ISRC if available (per-source fallback chain) ── _isrc_candidates = [] @@ -18839,7 +18839,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['ISRC'] = [final_isrc] elif isinstance(audio_file, MP4): audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))] - print(f"🔖 ISRC ({source}): {final_isrc}") + print(f"ISRC ({source}): {final_isrc}") # ── 6. Write copyright tag (Tidal → Qobuz fallback) ── _copyright_candidates = [] @@ -18865,7 +18865,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['LABEL'] = [qobuz_label] elif isinstance(audio_file, MP4): audio_file['----:com.apple.iTunes:LABEL'] = [MP4FreeForm(qobuz_label.encode('utf-8'))] - print(f"🏷️ Label (Qobuz): {qobuz_label}") + print(f"Label (Qobuz): {qobuz_label}") # ── 8. Write Last.fm and Genius URLs as custom tags ── if _tag_enabled('lastfm.tags.url') and lastfm_url: @@ -18885,7 +18885,7 @@ def _embed_source_ids(audio_file, metadata: dict): audio_file['----:com.apple.iTunes:GENIUS_URL'] = [MP4FreeForm(genius_url.encode('utf-8'))] except Exception as e: - print(f"⚠️ Error embedding source IDs (non-fatal): {e}") + print(f"Error embedding source IDs (non-fatal): {e}") def _download_cover_art(album_info: dict, target_dir: str, context: dict = None): """Downloads cover.jpg into the specified directory. @@ -18910,7 +18910,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None) return # Already high-res, skip # Low-res cover exists — try to upgrade from CAA is_upgrade = True - print(f"🔄 Existing cover.jpg is {existing_size // 1024}KB — attempting CAA upgrade...") + print(f"Existing cover.jpg is {existing_size // 1024}KB — attempting CAA upgrade...") except Exception: return else: @@ -18929,7 +18929,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None) with urllib.request.urlopen(req, timeout=10) as response: image_data = response.read() if image_data and len(image_data) > 1000: - print(f"🎨 Cover art from Cover Art Archive ({len(image_data) // 1024}KB)") + print(f"Cover art from Cover Art Archive ({len(image_data) // 1024}KB)") else: image_data = None except Exception: @@ -18937,7 +18937,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None) # If upgrading and CAA failed, keep existing cover — don't overwrite with same low-res if is_upgrade and not image_data: - print(f"📷 CAA upgrade failed — keeping existing cover.jpg") + print(f"CAA upgrade failed — keeping existing cover.jpg") return # Fallback to Spotify/iTunes/Deezer URL (typically 640x640) @@ -18953,9 +18953,9 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None) if images and isinstance(images, list) and len(images) > 0: art_url = images[0].get('url') if isinstance(images[0], dict) else None if art_url: - print(f"📷 Using cover art URL from spotify_album context") + print(f"Using cover art URL from spotify_album context") if not art_url: - print("📷 No cover art URL available for download.") + print("No cover art URL available for download.") return with urllib.request.urlopen(art_url, timeout=10) as response: image_data = response.read() @@ -18966,9 +18966,9 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None) with open(cover_path, 'wb') as f: f.write(image_data) - print(f"✅ Cover art downloaded to: {cover_path}") + print(f"Cover art downloaded to: {cover_path}") except Exception as e: - print(f"❌ Error downloading cover.jpg: {e}") + print(f"Error downloading cover.jpg: {e}") @@ -18989,7 +18989,7 @@ def _get_spotify_album_tracks(spotify_album: dict) -> list: } for item in tracks_data['items']] return [] except Exception as e: - print(f"❌ Error fetching Spotify album tracks: {e}") + print(f"Error fetching Spotify album tracks: {e}") return [] def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -> dict: @@ -19005,7 +19005,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) - track_num = slsk_track_meta['track_number'] for sp_track in spotify_tracks: if sp_track.get('track_number') == track_num: - print(f"✅ Matched track by number ({track_num}): '{slsk_track_meta['title']}' -> '{sp_track['name']}'") + print(f"Matched track by number ({track_num}): '{slsk_track_meta['title']}' -> '{sp_track['name']}'") # Return a new dict with the corrected title and number return { 'title': sp_track['name'], @@ -19029,7 +19029,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) - best_match = sp_track if best_match: - print(f"✅ Matched track by title similarity ({best_score:.2f}): '{slsk_track_meta['title']}' -> '{best_match['name']}'") + print(f"Matched track by title similarity ({best_score:.2f}): '{slsk_track_meta['title']}' -> '{best_match['name']}'") return { 'title': best_match['name'], 'artist': slsk_track_meta.get('artist'), @@ -19039,7 +19039,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) - 'explicit': best_match.get('explicit', False) } - print(f"⚠️ Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.") + print(f"Could not confidently match track '{slsk_track_meta['title']}'. Using original metadata.") return slsk_track_meta # Fallback to original @@ -19062,13 +19062,13 @@ def _pp_lookup_musicbrainz(pp, _names_match): try: mb_service = mb_worker.mb_service if mb_worker else None if not mb_service: - print("⚠️ MusicBrainz worker not available, skipping MBID lookup") + print("MusicBrainz worker not available, skipping MBID lookup") return result = mb_service.match_recording(track_title, artist_name) if result and result.get('mbid'): pp['recording_mbid'] = result['mbid'] id_tags['MUSICBRAINZ_RECORDING_ID'] = pp['recording_mbid'] - print(f"🎵 MusicBrainz recording matched: {pp['recording_mbid']}") + print(f"MusicBrainz recording matched: {pp['recording_mbid']}") details = mb_service.mb_client.get_recording(pp['recording_mbid'], includes=['isrcs', 'genres']) if details: isrcs = details.get('isrcs', []) @@ -19166,7 +19166,7 @@ def _pp_lookup_musicbrainz(pp, _names_match): except (ValueError, TypeError): pass except Exception as e: - print(f"⚠️ MusicBrainz lookup failed (non-fatal): {e}") + print(f"MusicBrainz lookup failed (non-fatal): {e}") def _pp_lookup_deezer(pp, _names_match): @@ -19180,7 +19180,7 @@ def _pp_lookup_deezer(pp, _names_match): try: dz_client = deezer_worker.client if deezer_worker else None if not dz_client: - print("⚠️ Deezer worker not available, skipping Deezer lookup") + print("Deezer worker not available, skipping Deezer lookup") return dz_result = dz_client.search_track(artist_name, track_title) if dz_result and _names_match(dz_result.get('title', ''), track_title) and \ @@ -19190,7 +19190,7 @@ def _pp_lookup_deezer(pp, _names_match): dz_artist_id = dz_result.get('artist', {}).get('id') if dz_artist_id: id_tags['DEEZER_ARTIST_ID'] = str(dz_artist_id) - print(f"🎵 Deezer track matched: {dz_track_id}") + print(f"Deezer track matched: {dz_track_id}") dz_details = dz_client.get_track_details(dz_track_id) if dz_details: bpm_val = dz_details.get('bpm') @@ -19206,7 +19206,7 @@ def _pp_lookup_deezer(pp, _names_match): if len(dz_release) >= 4 and dz_release[:4].isdigit(): pp['release_year'] = dz_release[:4] except Exception as e: - print(f"⚠️ Deezer lookup failed (non-fatal): {e}") + print(f"Deezer lookup failed (non-fatal): {e}") def _pp_lookup_audiodb(pp, _names_match): @@ -19220,7 +19220,7 @@ def _pp_lookup_audiodb(pp, _names_match): try: adb_client = audiodb_worker.client if audiodb_worker else None if not adb_client: - print("⚠️ AudioDB worker not available, skipping AudioDB lookup") + print("AudioDB worker not available, skipping AudioDB lookup") return adb_result = adb_client.search_track(artist_name, track_title) if adb_result and _names_match(adb_result.get('strTrack', ''), track_title) and \ @@ -19228,22 +19228,22 @@ def _pp_lookup_audiodb(pp, _names_match): adb_track_id = adb_result.get('idTrack') if adb_track_id: id_tags['AUDIODB_TRACK_ID'] = str(adb_track_id) - print(f"🎵 AudioDB track matched: {adb_track_id}") + print(f"AudioDB track matched: {adb_track_id}") adb_mb_track = adb_result.get('strMusicBrainzID') if adb_mb_track and 'MUSICBRAINZ_RECORDING_ID' not in id_tags: id_tags['MUSICBRAINZ_RECORDING_ID'] = adb_mb_track pp['recording_mbid'] = adb_mb_track - print(f"🎵 MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}") + print(f"MusicBrainz recording ID from AudioDB fallback: {adb_mb_track}") adb_mb_artist = adb_result.get('strMusicBrainzArtistID') if adb_mb_artist and 'MUSICBRAINZ_ARTIST_ID' not in id_tags: id_tags['MUSICBRAINZ_ARTIST_ID'] = adb_mb_artist pp['artist_mbid'] = adb_mb_artist - print(f"🎵 MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}") + print(f"MusicBrainz artist ID from AudioDB fallback: {adb_mb_artist}") pp['audiodb_mood'] = adb_result.get('strMood') or None pp['audiodb_style'] = adb_result.get('strStyle') or None pp['audiodb_genre'] = adb_result.get('strGenre') or None except Exception as e: - print(f"⚠️ AudioDB lookup failed (non-fatal): {e}") + print(f"AudioDB lookup failed (non-fatal): {e}") def _pp_lookup_tidal(pp, _names_match): @@ -19262,7 +19262,7 @@ def _pp_lookup_tidal(pp, _names_match): td_track_id = td_result.get('id') if td_track_id: id_tags['TIDAL_TRACK_ID'] = str(td_track_id) - print(f"🎵 Tidal track matched: {td_track_id}") + print(f"Tidal track matched: {td_track_id}") td_artist = td_result.get('artist', {}) if isinstance(td_artist, dict) and td_artist.get('id'): id_tags['TIDAL_ARTIST_ID'] = str(td_artist['id']) @@ -19286,7 +19286,7 @@ def _pp_lookup_tidal(pp, _names_match): if len(td_release) >= 4 and td_release[:4].isdigit(): pp['release_year'] = td_release[:4] except Exception as e: - print(f"⚠️ Tidal lookup failed (non-fatal): {e}") + print(f"Tidal lookup failed (non-fatal): {e}") def _pp_lookup_qobuz(pp, _names_match): @@ -19312,7 +19312,7 @@ def _pp_lookup_qobuz(pp, _names_match): qz_track_id = qz_result.get('id') if qz_track_id: id_tags['QOBUZ_TRACK_ID'] = str(qz_track_id) - print(f"🎵 Qobuz track matched: {qz_track_id}") + print(f"Qobuz track matched: {qz_track_id}") if isinstance(qz_performer, dict) and qz_performer.get('id'): id_tags['QOBUZ_ARTIST_ID'] = str(qz_performer['id']) qz_isrc = qz_result.get('isrc') @@ -19342,7 +19342,7 @@ def _pp_lookup_qobuz(pp, _names_match): if len(qz_release) >= 4 and qz_release[:4].isdigit(): pp['release_year'] = qz_release[:4] except Exception as e: - print(f"⚠️ Qobuz lookup failed (non-fatal): {e}") + print(f"Qobuz lookup failed (non-fatal): {e}") def _pp_lookup_lastfm(pp, _names_match): @@ -19368,9 +19368,9 @@ def _pp_lookup_lastfm(pp, _names_match): pp['lastfm_tags'] = [t.get('name', '') for t in tag_list if isinstance(t, dict) and t.get('name')] elif isinstance(tag_list, dict) and tag_list.get('name'): pp['lastfm_tags'] = [tag_list['name']] - print(f"🎵 Last.fm track info found: {len(pp['lastfm_tags'])} tags") + print(f"Last.fm track info found: {len(pp['lastfm_tags'])} tags") except Exception as e: - print(f"⚠️ Last.fm lookup failed (non-fatal): {e}") + print(f"Last.fm lookup failed (non-fatal): {e}") def _pp_lookup_genius(pp, _names_match): @@ -19384,7 +19384,7 @@ def _pp_lookup_genius(pp, _names_match): try: import core.genius_client as _genius_module if time.time() < _genius_module._rate_limit_until: - print("⏭️ Genius rate-limited, skipping (non-blocking)") + print("Genius rate-limited, skipping (non-blocking)") return g_client = genius_worker.client if genius_worker else None if not g_client: @@ -19394,12 +19394,12 @@ def _pp_lookup_genius(pp, _names_match): g_id = g_result.get('id') if g_id: id_tags['GENIUS_TRACK_ID'] = str(g_id) - print(f"🎵 Genius song matched: {g_id}") + print(f"Genius song matched: {g_id}") g_url = g_result.get('url') if g_url: pp['genius_url'] = g_url except Exception as e: - print(f"⚠️ Genius lookup failed (non-fatal): {e}") + print(f"Genius lookup failed (non-fatal): {e}") def _post_process_matched_download_with_verification(context_key, context, file_path, task_id, batch_id): @@ -19482,7 +19482,7 @@ def _post_process_matched_download_with_verification(context_key, context, file_ # causing false "file verification failed" errors on successfully processed files. expected_final_path = context.get('_final_processed_path') if not expected_final_path: - _pp.info(f"⚠️ No _final_processed_path in context for task {task_id} — cannot verify, assuming success") + _pp.info(f"No _final_processed_path in context for task {task_id} — cannot verify, assuming success") with tasks_lock: if task_id in download_tasks: _mark_task_completed(task_id, context.get('track_info')) @@ -19603,16 +19603,16 @@ def _check_flac_bit_depth(file_path, context, context_key): track_info = context.get('track_info', {}) track_name = track_info.get('name', os.path.basename(file_path)) if _downsample_enabled: - print(f"📀 [FLAC Downsample] Accepted {_actual_bits}-bit FLAC (will be downsampled to {_flac_pref}-bit): {track_name}") + print(f"[FLAC Downsample] Accepted {_actual_bits}-bit FLAC (will be downsampled to {_flac_pref}-bit): {track_name}") else: - print(f"📀 [FLAC Fallback] Accepted {_actual_bits}-bit FLAC (preferred {_flac_pref}-bit): {track_name}") + print(f"[FLAC Fallback] Accepted {_actual_bits}-bit FLAC (preferred {_flac_pref}-bit): {track_name}") return False # Strict mode — reject and quarantine rejection_msg = f"FLAC bit depth mismatch: file is {_actual_bits}-bit, preference is {_flac_pref}-bit" try: quarantine_path = _move_to_quarantine(file_path, context, rejection_msg) - print(f"🚫 File quarantined due to bit depth filter: {quarantine_path}") + print(f"File quarantined due to bit depth filter: {quarantine_path}") except Exception as quarantine_error: logger.error(f"Quarantine failed ({quarantine_error}), deleting file: {file_path}") try: @@ -19700,7 +19700,7 @@ def _move_to_quarantine(file_path: str, context: dict, reason: str) -> str: except Exception as e: logger.warning(f"Failed to write quarantine metadata: {e}") - logger.warning(f"🚫 File quarantined: {quarantine_path} - Reason: {reason}") + logger.warning(f"File quarantined: {quarantine_path} - Reason: {reason}") try: if automation_engine: @@ -19781,7 +19781,7 @@ def _safe_move_file(src, dst): # to delete the source (e.g. permission denied on slskd-owned downloads). # If destination exists with content, treat as success. if dst.exists() and dst.stat().st_size > 0: - logger.warning(f"⚠️ Move raised {type(e).__name__} but destination exists, treating as success: {e}") + logger.warning(f"Move raised {type(e).__name__} but destination exists, treating as success: {e}") # Try to clean up source, but don't fail if we can't try: src.unlink() @@ -19791,7 +19791,7 @@ def _safe_move_file(src, dst): # Cross-device link error — do manual binary copy if "cross-device" in error_msg or "operation not permitted" in error_msg or "permission denied" in error_msg: - logger.warning(f"⚠️ Cross-device move detected, using fallback copy method: {e}") + logger.warning(f"Cross-device move detected, using fallback copy method: {e}") try: # Simple copy without metadata preservation (avoids permission errors) with open(src, 'rb') as f_src: @@ -19805,10 +19805,10 @@ def _safe_move_file(src, dst): src.unlink() except PermissionError: logger.info(f"Could not delete source file (may be owned by another process): {src}") - logger.info(f"✅ Successfully moved file using fallback method: {src} -> {dst}") + logger.info(f"Successfully moved file using fallback method: {src} -> {dst}") return except Exception as fallback_error: - logger.error(f"❌ Fallback copy also failed: {fallback_error}") + logger.error(f"Fallback copy also failed: {fallback_error}") raise else: # Re-raise if it's a different error @@ -19849,9 +19849,9 @@ def _post_process_matched_download(context_key, context, file_path): if not os.path.exists(file_path): existing_final = context.get('_final_processed_path') if existing_final and os.path.exists(existing_final): - print(f"✅ [Race Guard] Source gone but destination exists — already processed by another thread: {os.path.basename(existing_final)}") + print(f"[Race Guard] Source gone but destination exists — already processed by another thread: {os.path.basename(existing_final)}") return - print(f"⚠️ [Race Guard] Source file gone and no known destination — marking as failed: {os.path.basename(file_path)}") + print(f"[Race Guard] Source file gone and no known destination — marking as failed: {os.path.basename(file_path)}") context['_race_guard_failed'] = True return # --- END RACE CONDITION GUARD --- @@ -19872,10 +19872,10 @@ def _post_process_matched_download(context_key, context, file_path): break _prev_size = _cur_size if _stability_check == 0: - print(f"⏳ Waiting for file to stabilise: {_basename} ({_cur_size} bytes)") + print(f"Waiting for file to stabilise: {_basename} ({_cur_size} bytes)") time.sleep(1.5) else: - print(f"⚠️ File may still be writing after stability checks: {_basename} ({_prev_size} bytes)") + print(f"File may still be writing after stability checks: {_basename} ({_prev_size} bytes)") # --- END FILE STABILITY CHECK --- # --- ACOUSTID VERIFICATION --- @@ -19919,26 +19919,26 @@ def _post_process_matched_download(context_key, context, file_path): expected_artist = spotify_artist.get('name', '') if expected_track and expected_artist: - print(f"🔍 Running AcoustID verification for: '{expected_track}' by '{expected_artist}'") + print(f"Running AcoustID verification for: '{expected_track}' by '{expected_artist}'") verification_result, verification_msg = verifier.verify_audio_file( file_path, expected_track, expected_artist, context ) - print(f"🔍 AcoustID verification result: {verification_result.value} - {verification_msg}") + print(f"AcoustID verification result: {verification_result.value} - {verification_msg}") context['_acoustid_result'] = verification_result.value if verification_result == VerificationResult.FAIL: # Move to quarantine instead of Transfer try: quarantine_path = _move_to_quarantine(file_path, context, verification_msg) - print(f"🚫 File quarantined due to verification failure: {quarantine_path}") + print(f"File quarantined due to verification failure: {quarantine_path}") except Exception as quarantine_error: # Quarantine failed — delete the known-wrong file instead # NEVER save a file we've confirmed is wrong logger.error(f"Quarantine failed ({quarantine_error}), deleting wrong file: {file_path}") - print(f"🚫 Quarantine failed, deleting wrong file: {file_path}") + print(f"Quarantine failed, deleting wrong file: {file_path}") try: os.remove(file_path) except Exception as del_error: @@ -19968,14 +19968,14 @@ def _post_process_matched_download(context_key, context, file_path): return # NEVER continue processing a known-wrong file else: - print(f"⚠️ AcoustID verification skipped: missing track/artist info") + print(f"AcoustID verification skipped: missing track/artist info") context['_acoustid_result'] = 'skip' else: print(f"ℹ️ AcoustID verification not available: {available_reason}") context['_acoustid_result'] = 'disabled' except Exception as verify_error: # Any verification error should NOT block the download - fail open - print(f"⚠️ AcoustID verification error (continuing normally): {verify_error}") + print(f"AcoustID verification error (continuing normally): {verify_error}") context['_acoustid_result'] = 'error' # --- END ACOUSTID VERIFICATION --- @@ -20022,7 +20022,7 @@ def _post_process_matched_download(context_key, context, file_path): logger.info(f"Moving to Transfer root (single track)") _safe_move_file(file_path, destination) - logger.info(f"✅ Moved simple download to: {destination}") + logger.info(f"Moved simple download to: {destination}") # Clean up context with matched_context_lock: @@ -20036,8 +20036,8 @@ def _post_process_matched_download(context_key, context, file_path): daemon=True ).start() - add_activity_item("✅", "Download Complete", f"{album_name}/{filename}", "Now") - logger.info(f"✅ Simple download post-processing complete: {album_name}/{filename}") + add_activity_item("", "Download Complete", f"{album_name}/{filename}", "Now") + logger.info(f"Simple download post-processing complete: {album_name}/{filename}") # Set flag in context so verification function knows this was fully handled context['_simple_download_completed'] = True @@ -20048,39 +20048,39 @@ def _post_process_matched_download(context_key, context, file_path): return # --- END SIMPLE DOWNLOAD HANDLING --- - print(f"🎯 Starting robust post-processing for: {context_key}") + print(f"Starting robust post-processing for: {context_key}") spotify_artist = context.get("spotify_artist") if not spotify_artist: - print(f"❌ Post-processing failed: Missing spotify_artist context.") + print(f"Post-processing failed: Missing spotify_artist context.") return # Check if playlist folder mode is enabled (sync page playlists only) track_info = context.get("track_info", {}) playlist_folder_mode = track_info.get("_playlist_folder_mode", False) - print(f"🔍 [Debug] Post-processing - track_info type: {type(track_info)}, is None: {track_info is None}, is empty: {not track_info}") - print(f"🔍 [Debug] Post-processing - playlist_folder_mode: {playlist_folder_mode}") + print(f"[Debug] Post-processing - track_info type: {type(track_info)}, is None: {track_info is None}, is empty: {not track_info}") + print(f"[Debug] Post-processing - playlist_folder_mode: {playlist_folder_mode}") if track_info: - print(f"🔍 [Debug] Post-processing - track_info keys: {list(track_info.keys())}") + print(f"[Debug] Post-processing - track_info keys: {list(track_info.keys())}") if playlist_folder_mode: # Use shared path builder for playlist mode playlist_name = track_info.get("_playlist_name", "Unknown Playlist") - print(f"📁 [Playlist Folder Mode] Organizing in playlist folder: {playlist_name}") + print(f"[Playlist Folder Mode] Organizing in playlist folder: {playlist_name}") file_ext = os.path.splitext(file_path)[1] # Build final path FIRST so we can check for already-processed files final_path, _ = _build_final_path_for_track(context, spotify_artist, None, file_ext) - print(f"📁 Playlist mode final path: '{final_path}'") + print(f"Playlist mode final path: '{final_path}'") # RACE CONDITION GUARD: If source file is gone but destination exists, # another thread (stream processor or verification worker) already moved it. # Return early to avoid deleting the successfully processed file. if not os.path.exists(file_path): if os.path.exists(final_path): - print(f"✅ [Playlist Folder Mode] Source gone but destination exists — already processed by another thread: {os.path.basename(final_path)}") + print(f"[Playlist Folder Mode] Source gone but destination exists — already processed by another thread: {os.path.basename(final_path)}") context['_final_processed_path'] = final_path return else: @@ -20089,7 +20089,7 @@ def _post_process_matched_download(context_key, context, file_path): context['_audio_quality'] = _get_audio_quality_string(file_path) if context['_audio_quality']: - print(f"🎧 Audio quality detected: {context['_audio_quality']}") + print(f"Audio quality detected: {context['_audio_quality']}") # FLAC bit depth filter if _check_flac_bit_depth(file_path, context, context_key): @@ -20097,14 +20097,14 @@ def _post_process_matched_download(context_key, context, file_path): # Enhance metadata before moving try: - print(f"🔍 [Metadata Input] Playlist mode - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})") + print(f"[Metadata Input] Playlist mode - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})") _enhance_file_metadata(file_path, context, spotify_artist, None) except Exception as meta_err: import traceback pp_logger.info(f"[inner] Metadata enhancement FAILED for {context_key}: {meta_err}\n{traceback.format_exc()}") # Move file to playlist folder - print(f"🚚 Moving '{os.path.basename(file_path)}' to '{final_path}'") + print(f"Moving '{os.path.basename(file_path)}' to '{final_path}'") _safe_move_file(file_path, final_path) # Store final path for verification wrapper (before conversions may override) @@ -20125,13 +20125,13 @@ def _post_process_matched_download(context_key, context, file_path): downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) _cleanup_empty_directories(downloads_path, file_path) - print(f"✅ [Playlist Folder Mode] Post-processing complete: {final_path}") + print(f"[Playlist Folder Mode] Post-processing complete: {final_path}") # WISHLIST REMOVAL: Check if this track should be removed from wishlist try: _check_and_remove_from_wishlist(context) except Exception as wishlist_error: - print(f"⚠️ [Playlist Folder] Error checking wishlist removal: {wishlist_error}") + print(f"[Playlist Folder] Error checking wishlist removal: {wishlist_error}") _emit_track_downloaded(context) _record_library_history_download(context) @@ -20146,7 +20146,7 @@ def _post_process_matched_download(context_key, context, file_path): if task_id in download_tasks: download_tasks[task_id]['stream_processed'] = True download_tasks[task_id]['status'] = 'completed' - print(f"✅ [Playlist Folder Mode] Marked task {task_id} as completed") + print(f"[Playlist Folder Mode] Marked task {task_id} as completed") _on_download_completed(batch_id, task_id, success=True) return # Skip normal album/artist folder structure processing @@ -20156,7 +20156,7 @@ def _post_process_matched_download(context_key, context, file_path): if is_album_download and has_clean_spotify_data: # Build album_info directly from clean Spotify metadata (GUI PARITY) - print("✅ Album context with clean Spotify data found - using direct album info") + print("Album context with clean Spotify data found - using direct album info") original_search = context.get("original_search_result", {}) spotify_album = context.get("spotify_album", {}) @@ -20165,7 +20165,7 @@ def _post_process_matched_download(context_key, context, file_path): clean_album_name = original_search.get('spotify_clean_album', 'Unknown Album') # DEBUG: Check what's in original_search - print(f"🔍 [DEBUG] Path 1 - Clean Spotify data path:") + print(f"[DEBUG] Path 1 - Clean Spotify data path:") print(f" original_search keys: {list(original_search.keys())}") print(f" track_number in original_search: {'track_number' in original_search}") print(f" track_number value: {original_search.get('track_number', 'NOT_FOUND')}") @@ -20181,16 +20181,16 @@ def _post_process_matched_download(context_key, context, file_path): 'source': 'clean_spotify_metadata' } - print(f"🎯 Using clean Spotify album: '{clean_album_name}' for track: '{clean_track_name}'") + print(f"Using clean Spotify album: '{clean_album_name}' for track: '{clean_track_name}'") elif is_album_download: # CRITICAL FIX: Album context without clean Spotify data - still force album treatment - print("⚠️ Album context found but no clean Spotify data - using enhanced fallback") + print("Album context found but no clean Spotify data - using enhanced fallback") original_search = context.get("original_search_result", {}) spotify_album = context.get("spotify_album", {}) clean_track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track') # DEBUG: Check what's in original_search for path 2 - print(f"🔍 [DEBUG] Path 2 - Enhanced fallback album context path:") + print(f"[DEBUG] Path 2 - Enhanced fallback album context path:") print(f" original_search keys: {list(original_search.keys())}") print(f" track_number in original_search: {'track_number' in original_search}") print(f" track_number value: {original_search.get('track_number', 'NOT_FOUND')}") @@ -20211,10 +20211,10 @@ def _post_process_matched_download(context_key, context, file_path): 'confidence': 0.9, # Higher confidence - user explicitly chose album 'source': 'enhanced_fallback_album_context' } - print(f"🎯 [FORCED ALBUM] Using album: '{album_name}' for track: '{clean_track_name}'") + print(f"[FORCED ALBUM] Using album: '{album_name}' for track: '{clean_track_name}'") else: # For singles, we still need to detect if they belong to an album. - print("🎵 Single track download - attempting album detection") + print("Single track download - attempting album detection") album_info = _detect_album_info_web(context, spotify_artist) # --- Album grouping resolution --- @@ -20222,7 +20222,7 @@ def _post_process_matched_download(context_key, context, file_path): # Explicit album downloads already have the correct Spotify album name — # re-grouping would mangle names like "(Reworked and Remastered)" into "(Deluxe Edition)". if album_info and album_info['is_album'] and not is_album_download: - print(f"\n🎯 SMART ALBUM GROUPING for track: '{album_info.get('clean_track_name', 'Unknown')}'") + print(f"\nSMART ALBUM GROUPING for track: '{album_info.get('clean_track_name', 'Unknown')}'") print(f" Original album: '{album_info.get('album_name', 'None')}'") # Get original album name from context if available @@ -20235,16 +20235,16 @@ def _post_process_matched_download(context_key, context, file_path): album_info['album_name'] = consistent_album_name print(f" Final album name: '{consistent_album_name}'") - print(f"🔗 ✅ Album grouping complete!\n") + print(f"Album grouping complete!\n") elif album_info and album_info['is_album'] and is_album_download: - print(f"\n🎯 EXPLICIT ALBUM DOWNLOAD - preserving Spotify album name: '{album_info.get('album_name', 'None')}'") + print(f"\nEXPLICIT ALBUM DOWNLOAD - preserving Spotify album name: '{album_info.get('album_name', 'None')}'") print(f" Skipping smart grouping (not needed for explicit album downloads)\n") # 1. Get transfer path (directory creation handled by _build_final_path_for_track) file_ext = os.path.splitext(file_path)[1] context['_audio_quality'] = _get_audio_quality_string(file_path) if context['_audio_quality']: - print(f"🎧 Audio quality detected: {context['_audio_quality']}") + print(f"Audio quality detected: {context['_audio_quality']}") # FLAC bit depth filter if _check_flac_bit_depth(file_path, context, context_key): @@ -20261,46 +20261,46 @@ def _post_process_matched_download(context_key, context, file_path): # Priority 1: Spotify clean title from context if original_search.get('spotify_clean_title'): clean_track_name = original_search['spotify_clean_title'] - print(f"🎵 Using Spotify clean title: '{clean_track_name}'") + print(f"Using Spotify clean title: '{clean_track_name}'") # Priority 2: Album info clean name elif album_info.get('clean_track_name'): clean_track_name = album_info['clean_track_name'] - print(f"🎵 Using album info clean name: '{clean_track_name}'") + print(f"Using album info clean name: '{clean_track_name}'") # Priority 3: Original title as fallback else: clean_track_name = original_search.get('title', 'Unknown Track') - print(f"🎵 Using original title as fallback: '{clean_track_name}'") + print(f"Using original title as fallback: '{clean_track_name}'") final_track_name_sanitized = _sanitize_filename(clean_track_name) track_number = album_info['track_number'] # DEBUG: Check final track_number values - print(f"🔍 [DEBUG] Final track_number processing:") + print(f"[DEBUG] Final track_number processing:") print(f" album_info source: {album_info.get('source', 'unknown')}") print(f" album_info track_number: {album_info.get('track_number', 'NOT_FOUND')}") print(f" track_number variable: {track_number}") # Fix: Handle None track_number if track_number is None: - print(f"⚠️ Track number is None, extracting from filename: {os.path.basename(file_path)}") + print(f"Track number is None, extracting from filename: {os.path.basename(file_path)}") track_number = _extract_track_number_from_filename(file_path) print(f" -> Extracted track number: {track_number}") # Ensure track_number is valid if not isinstance(track_number, int) or track_number < 1: - print(f"⚠️ Invalid track number ({track_number}), defaulting to 1") + print(f"Invalid track number ({track_number}), defaulting to 1") track_number = 1 - print(f"🎯 [DEBUG] FINAL track_number used for filename: {track_number}") + print(f"[DEBUG] FINAL track_number used for filename: {track_number}") # CRITICAL FIX: Update album_info with corrected track_number for metadata enhancement album_info['track_number'] = track_number album_info['clean_track_name'] = clean_track_name # Ensure clean name is in album_info - print(f"✅ [FIX] Updated album_info track_number to {track_number} for consistent metadata") + print(f"[FIX] Updated album_info track_number to {track_number} for consistent metadata") # Use shared path builder for album mode final_path, _ = _build_final_path_for_track(context, spotify_artist, album_info, file_ext) - print(f"📁 Album path: '{final_path}'") + print(f"Album path: '{final_path}'") else: # Single track structure: Transfer/ARTIST/ARTIST - SINGLE/SINGLE.ext # --- GUI PARITY: Use multiple sources for clean track name --- @@ -20310,15 +20310,15 @@ def _post_process_matched_download(context_key, context, file_path): # Priority 1: Spotify clean title from context if original_search.get('spotify_clean_title'): clean_track_name = original_search['spotify_clean_title'] - print(f"🎵 Using Spotify clean title: '{clean_track_name}'") + print(f"Using Spotify clean title: '{clean_track_name}'") # Priority 2: Album info clean name elif album_info and album_info.get('clean_track_name'): clean_track_name = album_info['clean_track_name'] - print(f"🎵 Using album info clean name: '{clean_track_name}'") + print(f"Using album info clean name: '{clean_track_name}'") # Priority 3: Original title as fallback else: clean_track_name = original_search.get('title', 'Unknown Track') - print(f"🎵 Using original title as fallback: '{clean_track_name}'") + print(f"Using original title as fallback: '{clean_track_name}'") # Ensure clean name is in album_info for path builder if album_info: @@ -20326,7 +20326,7 @@ def _post_process_matched_download(context_key, context, file_path): # Use shared path builder for single mode final_path, _ = _build_final_path_for_track(context, spotify_artist, album_info, file_ext) - print(f"📁 Single path: '{final_path}'") + print(f"Single path: '{final_path}'") # Store the actual computed path so verification uses this exact path # instead of recomputing independently (which can produce mismatches) @@ -20334,11 +20334,11 @@ def _post_process_matched_download(context_key, context, file_path): # 3. Enhance metadata, move file, download art, and cleanup try: - print(f"🔍 [Metadata Input] artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})") + print(f"[Metadata Input] artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})") if album_info: - print(f"🔍 [Metadata Input] album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, disc#: {album_info.get('disc_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}") + print(f"[Metadata Input] album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, disc#: {album_info.get('disc_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}") else: - print(f"🔍 [Metadata Input] album_info: None (single track)") + print(f"[Metadata Input] album_info: None (single track)") _enhance_file_metadata(file_path, context, spotify_artist, album_info) except Exception as meta_err: import traceback @@ -20354,12 +20354,12 @@ def _post_process_matched_download(context_key, context, file_path): _enhance_source_info = {} is_enhance_download = _enhance_source_info.get('enhance', False) - print(f"🚚 Moving '{os.path.basename(file_path)}' to '{final_path}'") + print(f"Moving '{os.path.basename(file_path)}' to '{final_path}'") if os.path.exists(final_path): # PROTECTION: If destination already exists, check before overwriting # If the source file is gone, another thread already handled this - don't delete the destination if not os.path.exists(file_path): - print(f"✅ [Protection] Destination exists and source already gone - file already transferred: {os.path.basename(final_path)}") + print(f"[Protection] Destination exists and source already gone - file already transferred: {os.path.basename(final_path)}") return try: from mutagen import File as MutagenFile @@ -20374,50 +20374,50 @@ def _post_process_matched_download(context_key, context, file_path): _incoming_tier = _get_quality_tier_from_extension(file_path) if _incoming_tier[1] < _existing_tier[1]: # Incoming is higher quality (lower tier number) — replace - print(f"🔄 [Quality Replace] Replacing {_existing_tier[0]} with {_incoming_tier[0]}: {os.path.basename(final_path)}") + print(f"[Quality Replace] Replacing {_existing_tier[0]} with {_incoming_tier[0]}: {os.path.basename(final_path)}") try: os.remove(final_path) except Exception as e: - print(f"⚠️ [Quality Replace] Could not remove existing file: {e}") + print(f"[Quality Replace] Could not remove existing file: {e}") else: - print(f"⚠️ [Protection] Existing file is same or better quality ({_existing_tier[0]} vs {_incoming_tier[0]}) - skipping: {os.path.basename(final_path)}") + print(f"[Protection] Existing file is same or better quality ({_existing_tier[0]} vs {_incoming_tier[0]}) - skipping: {os.path.basename(final_path)}") try: os.remove(file_path) except FileNotFoundError: pass except Exception as e: - print(f"⚠️ [Protection] Error removing redundant file: {e}") + print(f"[Protection] Error removing redundant file: {e}") return else: - print(f"⚠️ [Protection] Existing file already has metadata enhancement - skipping overwrite: {os.path.basename(final_path)}") - print(f"🗑️ [Protection] Removing redundant download file: {os.path.basename(file_path)}") + print(f"[Protection] Existing file already has metadata enhancement - skipping overwrite: {os.path.basename(final_path)}") + print(f"[Protection] Removing redundant download file: {os.path.basename(file_path)}") try: os.remove(file_path) except FileNotFoundError: - print(f"⚠️ [Protection] Could not remove redundant file (already gone): {file_path}") + print(f"[Protection] Could not remove redundant file (already gone): {file_path}") except Exception as e: - print(f"⚠️ [Protection] Error removing redundant file: {e}") + print(f"[Protection] Error removing redundant file: {e}") return # Don't overwrite the good file elif is_enhance_download: # ENHANCE BYPASS: Allow overwrite — backup original, then remove to allow move - print(f"🔄 [Enhance] Quality enhance mode — replacing existing file: {os.path.basename(final_path)}") + print(f"[Enhance] Quality enhance mode — replacing existing file: {os.path.basename(final_path)}") try: os.remove(final_path) except Exception as e: - print(f"⚠️ [Enhance] Could not remove existing file for replacement: {e}") + print(f"[Enhance] Could not remove existing file for replacement: {e}") else: - print(f"🔄 [Protection] Existing file lacks metadata - safe to overwrite: {os.path.basename(final_path)}") + print(f"[Protection] Existing file lacks metadata - safe to overwrite: {os.path.basename(final_path)}") try: os.remove(final_path) except FileNotFoundError: pass # It was just there, but now gone? except Exception as check_error: - print(f"⚠️ [Protection] Error checking existing file metadata, proceeding with overwrite: {check_error}") + print(f"[Protection] Error checking existing file metadata, proceeding with overwrite: {check_error}") try: if os.path.exists(final_path): os.remove(final_path) except Exception as e: - print(f"⚠️ [Protection] Failed to remove existing file for overwrite: {e}") + print(f"[Protection] Failed to remove existing file for overwrite: {e}") # --- PRE-MOVE SOURCE CHECK --- # Right before moving, verify the source file still exists. @@ -20425,7 +20425,7 @@ def _post_process_matched_download(context_key, context, file_path): # already moved this file during the sleep + metadata enhancement window. if not os.path.exists(file_path): if os.path.exists(final_path): - print(f"✅ [Pre-Move] Source already gone and destination exists - another thread completed transfer: {os.path.basename(final_path)}") + print(f"[Pre-Move] Source already gone and destination exists - another thread completed transfer: {os.path.basename(final_path)}") # Still do cover art + lyrics since the other thread might not have finished those _download_cover_art(album_info, os.path.dirname(final_path), context) _generate_lrc_file(final_path, context, spotify_artist, album_info) @@ -20453,13 +20453,13 @@ def _post_process_matched_download(context_key, context, file_path): found_variant = os.path.join(expected_dir, f) break if found_variant: - print(f"✅ [Pre-Move] Source gone but found variant in destination (stream processor handled it): {os.path.basename(found_variant)}") + print(f"[Pre-Move] Source gone but found variant in destination (stream processor handled it): {os.path.basename(found_variant)}") context['_final_processed_path'] = found_variant _download_cover_art(album_info, expected_dir, context) _generate_lrc_file(found_variant, context, spotify_artist, album_info) return else: - print(f"⚠️ [Pre-Move] Source file gone and no matching file in destination: {os.path.basename(file_path)}") + print(f"[Pre-Move] Source file gone and no matching file in destination: {os.path.basename(file_path)}") raise FileNotFoundError(f"Source file vanished before move and destination does not exist: {file_path}") _safe_move_file(file_path, final_path) @@ -20472,13 +20472,13 @@ def _post_process_matched_download(context_key, context, file_path): os.remove(original_enhance_path) old_fmt = os.path.splitext(original_enhance_path)[1] new_fmt = os.path.splitext(final_path)[1] - print(f"✅ [Enhance] Upgraded {old_fmt} → {new_fmt}: {os.path.basename(final_path)}") + print(f"[Enhance] Upgraded {old_fmt} → {new_fmt}: {os.path.basename(final_path)}") except Exception as e: - print(f"⚠️ [Enhance] Could not remove old-format file: {e}") + print(f"[Enhance] Could not remove old-format file: {e}") elif is_enhance_download: old_fmt = _enhance_source_info.get('original_format', 'unknown') new_fmt = os.path.splitext(final_path)[1] - print(f"✅ [Enhance] Replaced in-place ({old_fmt} → {new_fmt}): {os.path.basename(final_path)}") + print(f"[Enhance] Replaced in-place ({old_fmt} → {new_fmt}): {os.path.basename(final_path)}") _download_cover_art(album_info, os.path.dirname(final_path), context) @@ -20499,7 +20499,7 @@ def _post_process_matched_download(context_key, context, file_path): downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) _cleanup_empty_directories(downloads_path, file_path) - print(f"✅ Post-processing complete for: {context.get('_final_processed_path', final_path)}") + print(f"Post-processing complete for: {context.get('_final_processed_path', final_path)}") _emit_track_downloaded(context) _record_library_history_download(context) @@ -20511,7 +20511,7 @@ def _post_process_matched_download(context_key, context, file_path): completed_path = context.get('_final_processed_path', final_path) _record_retag_download(context, spotify_artist, album_info, completed_path) except Exception as retag_err: - print(f"⚠️ [Post-Process] Retag data capture failed (non-fatal): {retag_err}") + print(f"[Post-Process] Retag data capture failed (non-fatal): {retag_err}") # REPAIR: Register album folder for repair scanning when batch completes try: @@ -20522,19 +20522,19 @@ def _post_process_matched_download(context_key, context, file_path): if album_folder: repair_worker.register_folder(batch_id_for_repair, album_folder) except Exception as repair_err: - print(f"⚠️ [Post-Process] Repair folder registration failed: {repair_err}") + print(f"[Post-Process] Repair folder registration failed: {repair_err}") # WISHLIST REMOVAL: Check if this track should be removed from wishlist after successful download try: _check_and_remove_from_wishlist(context) except Exception as wishlist_error: - print(f"⚠️ [Post-Process] Error checking wishlist removal: {wishlist_error}") + print(f"[Post-Process] Error checking wishlist removal: {wishlist_error}") # Call completion callback for missing downloads tasks to start next batch task_id = context.get('task_id') batch_id = context.get('batch_id') if task_id and batch_id: - print(f"🎯 [Post-Process] Calling completion callback for task {task_id} in batch {batch_id}") + print(f"[Post-Process] Calling completion callback for task {task_id} in batch {batch_id}") # Mark task as stream processed and set terminal status so # _validate_worker_counts won't count this task as active @@ -20546,7 +20546,7 @@ def _post_process_matched_download(context_key, context, file_path): if task_id in download_tasks: download_tasks[task_id]['stream_processed'] = True download_tasks[task_id]['status'] = 'completed' - print(f"✅ [Post-Process] Marked task {task_id} as completed") + print(f"[Post-Process] Marked task {task_id} as completed") _on_download_completed(batch_id, task_id, success=True) @@ -20554,7 +20554,7 @@ def _post_process_matched_download(context_key, context, file_path): import traceback pp_logger.info(f"[inner] EXCEPTION in post-processing for {context_key}: {e}") pp_logger.info(traceback.format_exc()) - print(f"\n❌ CRITICAL ERROR in post-processing for {context_key}: {e}") + print(f"\nCRITICAL ERROR in post-processing for {context_key}: {e}") traceback.print_exc() # Only retry if the source file still exists - otherwise retrying is pointless @@ -20565,15 +20565,15 @@ def _post_process_matched_download(context_key, context, file_path): # Remove from processed set so it can be retried if context_key in _processed_download_ids: _processed_download_ids.remove(context_key) - print(f"🔄 Removed {context_key} from processed set - will retry on next check") + print(f"Removed {context_key} from processed set - will retry on next check") # Re-add to matched context for retry with matched_context_lock: if context_key not in matched_downloads_context: matched_downloads_context[context_key] = context - print(f"♻️ Re-added {context_key} to context for retry") + print(f"Re-added {context_key} to context for retry") else: - print(f"⚠️ Source file gone, not retrying: {context_key}") + print(f"Source file gone, not retrying: {context_key}") finally: file_lock.release() # Clean up the lock entry to prevent unbounded memory growth @@ -20671,7 +20671,7 @@ def _record_retag_download(context, spotify_artist, album_info, final_path): title=title, file_path=str(final_path), file_format=file_format, spotify_track_id=spotify_track_id, itunes_track_id=itunes_track_id ) - print(f"📝 [Retag] Recorded track for retag: '{title}' in '{album_name}'") + print(f"[Retag] Recorded track for retag: '{title}' in '{album_name}'") # Cap retag groups at 100, remove oldest db.trim_retag_groups(100) @@ -20770,7 +20770,7 @@ def _execute_retag(group_id, album_id): if best_match: matched_pairs.append((existing_track, best_match)) else: - print(f"⚠️ [Retag] No match found for track: '{existing_track.get('title')}'") + print(f"[Retag] No match found for track: '{existing_track.get('title')}'") matched_pairs.append((existing_track, None)) with retag_lock: @@ -20792,7 +20792,7 @@ def _execute_retag(group_id, album_id): # Verify file exists if not os.path.exists(current_file_path): - print(f"⚠️ [Retag] File not found, skipping: {current_file_path}") + print(f"[Retag] File not found, skipping: {current_file_path}") with retag_lock: retag_state['processed'] += 1 retag_state['progress'] = int(retag_state['processed'] / retag_state['total_tracks'] * 100) @@ -20834,9 +20834,9 @@ def _execute_retag(group_id, album_id): # Re-write metadata tags try: _enhance_file_metadata(current_file_path, context, new_artist, album_info) - print(f"✅ [Retag] Re-tagged: '{track_title}'") + print(f"[Retag] Re-tagged: '{track_title}'") except Exception as meta_err: - print(f"⚠️ [Retag] Metadata write failed for '{track_title}': {meta_err}") + print(f"[Retag] Metadata write failed for '{track_title}': {meta_err}") # Compute new path and move if different file_ext = os.path.splitext(current_file_path)[1] @@ -20844,7 +20844,7 @@ def _execute_retag(group_id, album_id): new_path, _ = _build_final_path_for_track(context, new_artist, album_info, file_ext) if os.path.normpath(current_file_path) != os.path.normpath(new_path): - print(f"🚚 [Retag] Moving '{os.path.basename(current_file_path)}' -> '{new_path}'") + print(f"[Retag] Moving '{os.path.basename(current_file_path)}' -> '{new_path}'") old_dir = os.path.dirname(current_file_path) os.makedirs(os.path.dirname(new_path), exist_ok=True) _safe_move_file(current_file_path, new_path) @@ -20856,9 +20856,9 @@ def _execute_retag(group_id, album_id): new_lyrics = os.path.splitext(new_path)[0] + lyrics_ext try: _safe_move_file(old_lyrics, new_lyrics) - print(f"📝 [Retag] Moved {lyrics_ext} file alongside audio") + print(f"[Retag] Moved {lyrics_ext} file alongside audio") except Exception as lrc_err: - print(f"⚠️ [Retag] Failed to move {lyrics_ext} file: {lrc_err}") + print(f"[Retag] Failed to move {lyrics_ext} file: {lrc_err}") # Remove old cover.jpg if directory changed and old dir is now empty of audio new_dir = os.path.dirname(new_path) @@ -20872,7 +20872,7 @@ def _execute_retag(group_id, album_id): if not remaining_audio: try: os.remove(old_cover) - print(f"🗑️ [Retag] Removed orphaned cover.jpg from old directory") + print(f"[Retag] Removed orphaned cover.jpg from old directory") except Exception: pass @@ -20884,15 +20884,15 @@ def _execute_retag(group_id, album_id): db.update_retag_track_path(existing_track['id'], str(new_path)) current_file_path = new_path else: - print(f"📍 [Retag] Path unchanged for '{track_title}', no move needed") + print(f"[Retag] Path unchanged for '{track_title}', no move needed") except Exception as move_err: - print(f"⚠️ [Retag] Path/move failed for '{track_title}': {move_err}") + print(f"[Retag] Path/move failed for '{track_title}': {move_err}") # Download cover art to album directory try: _download_cover_art(album_info, os.path.dirname(current_file_path), context) except Exception as cover_err: - print(f"⚠️ [Retag] Cover art download failed: {cover_err}") + print(f"[Retag] Cover art download failed: {cover_err}") with retag_lock: retag_state['processed'] += 1 @@ -20923,11 +20923,11 @@ def _execute_retag(group_id, album_id): "progress": 100, "current_track": "" }) - print(f"✅ [Retag] Retag operation complete for group {group_id}") + print(f"[Retag] Retag operation complete for group {group_id}") except Exception as e: import traceback - print(f"❌ [Retag] Error during retag: {e}") + print(f"[Retag] Error during retag: {e}") print(traceback.format_exc()) with retag_lock: retag_state.update({ @@ -20953,17 +20953,17 @@ def _check_and_remove_from_wishlist(context): track_info = context.get('track_info', {}) if track_info.get('id'): spotify_track_id = track_info['id'] - print(f"📋 [Wishlist] Found Spotify ID from track_info: {spotify_track_id}") + print(f"[Wishlist] Found Spotify ID from track_info: {spotify_track_id}") # Method 2: From original search result elif context.get('original_search_result', {}).get('id'): spotify_track_id = context['original_search_result']['id'] - print(f"📋 [Wishlist] Found Spotify ID from original_search_result: {spotify_track_id}") + print(f"[Wishlist] Found Spotify ID from original_search_result: {spotify_track_id}") # Method 3: Check if this is a wishlist download (context has wishlist_id) elif 'wishlist_id' in track_info: wishlist_id = track_info['wishlist_id'] - print(f"📋 [Wishlist] Found wishlist_id in context: {wishlist_id}") + print(f"[Wishlist] Found wishlist_id in context: {wishlist_id}") # Get the Spotify track ID from the wishlist entry (search all profiles) database = get_database() @@ -20974,7 +20974,7 @@ def _check_and_remove_from_wishlist(context): for wl_track in wishlist_tracks: if wl_track.get('wishlist_id') == wishlist_id: spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id') - print(f"📋 [Wishlist] Found Spotify ID from wishlist entry: {spotify_track_id}") + print(f"[Wishlist] Found Spotify ID from wishlist entry: {spotify_track_id}") break # Method 4: Try to construct ID from track metadata for fuzzy matching @@ -20983,7 +20983,7 @@ def _check_and_remove_from_wishlist(context): artist_name = _get_track_artist_name(track_info) or _get_track_artist_name(context.get('original_search_result', {})) if track_name and artist_name: - print(f"📋 [Wishlist] No Spotify ID found, checking for fuzzy match: '{track_name}' by '{artist_name}'") + print(f"[Wishlist] No Spotify ID found, checking for fuzzy match: '{track_name}' by '{artist_name}'") # Get all wishlist tracks and find potential matches (search all profiles) if not wishlist_tracks: @@ -21007,22 +21007,22 @@ def _check_and_remove_from_wishlist(context): # Simple fuzzy matching if (wl_name == track_name.lower() and wl_artist_name == artist_name.lower()): spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id') - print(f"📋 [Wishlist] Found fuzzy match - Spotify ID: {spotify_track_id}") + print(f"[Wishlist] Found fuzzy match - Spotify ID: {spotify_track_id}") break # If we found a Spotify track ID, remove it from wishlist if spotify_track_id: - print(f"📋 [Wishlist] Attempting to remove track from wishlist: {spotify_track_id}") + print(f"[Wishlist] Attempting to remove track from wishlist: {spotify_track_id}") removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True) if removed: - print(f"✅ [Wishlist] Successfully removed track from wishlist: {spotify_track_id}") + print(f"[Wishlist] Successfully removed track from wishlist: {spotify_track_id}") else: print(f"ℹ️ [Wishlist] Track not found in wishlist or already removed: {spotify_track_id}") else: print(f"ℹ️ [Wishlist] No Spotify track ID found for wishlist removal check") except Exception as e: - print(f"❌ [Wishlist] Error in wishlist removal check: {e}") + print(f"[Wishlist] Error in wishlist removal check: {e}") import traceback traceback.print_exc() @@ -21040,13 +21040,13 @@ def _check_and_remove_track_from_wishlist_by_metadata(track_data): track_id = track_data.get('id', '') artists = track_data.get('artists', []) - print(f"📋 [Analysis] Checking if track should be removed from wishlist: '{track_name}' (ID: {track_id})") + print(f"[Analysis] Checking if track should be removed from wishlist: '{track_name}' (ID: {track_id})") # Method 1: Direct Spotify ID match if track_id: removed = wishlist_service.mark_track_download_result(track_id, success=True) if removed: - print(f"✅ [Analysis] Removed track from wishlist via direct ID match: {track_id}") + print(f"[Analysis] Removed track from wishlist via direct ID match: {track_id}") return True # Method 2: Fuzzy matching by name and artist if no direct ID match @@ -21060,7 +21060,7 @@ def _check_and_remove_track_from_wishlist_by_metadata(track_data): else: primary_artist = str(artists[0]) - print(f"📋 [Analysis] No direct ID match, trying fuzzy match: '{track_name}' by '{primary_artist}'") + print(f"[Analysis] No direct ID match, trying fuzzy match: '{track_name}' by '{primary_artist}'") # Get all wishlist tracks and find matches (search all profiles) database = get_database() @@ -21086,14 +21086,14 @@ def _check_and_remove_track_from_wishlist_by_metadata(track_data): if spotify_track_id: removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True) if removed: - print(f"✅ [Analysis] Removed track from wishlist via fuzzy match: {spotify_track_id}") + print(f"[Analysis] Removed track from wishlist via fuzzy match: {spotify_track_id}") return True print(f"ℹ️ [Analysis] Track not found in wishlist or already removed: '{track_name}'") return False except Exception as e: - print(f"❌ [Analysis] Error checking wishlist removal by metadata: {e}") + print(f"[Analysis] Error checking wishlist removal by metadata: {e}") import traceback traceback.print_exc() return False @@ -21111,7 +21111,7 @@ def _automatic_wishlist_cleanup_after_db_update(): db = MusicDatabase() active_server = config_manager.get_active_media_server() - print("📋 [Auto Cleanup] Starting automatic wishlist cleanup after database update...") + print("[Auto Cleanup] Starting automatic wishlist cleanup after database update...") # Get all wishlist tracks (across all profiles - cleanup is global) database = get_database() @@ -21120,10 +21120,10 @@ def _automatic_wishlist_cleanup_after_db_update(): for p in all_profiles: wishlist_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id'])) if not wishlist_tracks: - print("📋 [Auto Cleanup] No tracks in wishlist to clean up") + print("[Auto Cleanup] No tracks in wishlist to clean up") return - print(f"📋 [Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist") + print(f"[Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist") removed_count = 0 @@ -21158,11 +21158,11 @@ def _automatic_wishlist_cleanup_after_db_update(): if db_track and confidence >= 0.7: found_in_db = True - print(f"📋 [Auto Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})") + print(f"[Auto Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})") break except Exception as db_error: - print(f"⚠️ [Auto Cleanup] Error checking database for track '{track_name}': {db_error}") + print(f"[Auto Cleanup] Error checking database for track '{track_name}': {db_error}") continue # If found in database, remove from wishlist @@ -21171,14 +21171,14 @@ def _automatic_wishlist_cleanup_after_db_update(): removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True) if removed: removed_count += 1 - print(f"✅ [Auto Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})") + print(f"[Auto Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})") except Exception as remove_error: - print(f"❌ [Auto Cleanup] Error removing track from wishlist: {remove_error}") + print(f"[Auto Cleanup] Error removing track from wishlist: {remove_error}") - print(f"📋 [Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist") + print(f"[Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist") except Exception as e: - print(f"❌ [Auto Cleanup] Error in automatic wishlist cleanup: {e}") + print(f"[Auto Cleanup] Error in automatic wishlist cleanup: {e}") import traceback traceback.print_exc() @@ -21253,7 +21253,7 @@ def get_version_info(): "subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes", "sections": [ { - "title": "🎬 Music Videos — Search & Download from YouTube", + "title": "Music Videos — Search & Download from YouTube", "description": "New Music Videos tab in enhanced and global search for finding and downloading music videos", "features": [ "• Music Videos pill tab alongside Spotify/Deezer/iTunes/Discogs in both search bars", @@ -21266,7 +21266,7 @@ def get_version_info(): "usage_note": "Set your Music Videos directory in Settings > Downloads, then search for any artist in the search bar and click the Music Videos tab." }, { - "title": "📦 Lidarr Download Source (Development)", + "title": "Lidarr Download Source (Development)", "description": "Use Lidarr as a download source for Usenet and torrent content", "features": [ "• 7th download source alongside Soulseek, YouTube, Tidal, Qobuz, HiFi, and Deezer", @@ -21278,7 +21278,7 @@ def get_version_info(): "usage_note": "Requires a running Lidarr instance with configured indexers and download clients. Set Download Source to 'Lidarr Only (Development)' or add to Hybrid order." }, { - "title": "🔧 Metadata Pipeline Overhaul — Fix Unknown Artist & Source Selection", + "title": "Metadata Pipeline Overhaul — Fix Unknown Artist & Source Selection", "description": "Major fix for tracks downloading as 'Unknown Artist' and Spotify being used when Deezer/iTunes was selected", "features": [ "• Fixed playlist pipeline (discover → sync → wishlist → download) losing artist, track number, and album year data", @@ -21292,7 +21292,7 @@ def get_version_info(): "usage_note": "If you have existing Unknown Artist tracks, run the Fix Unknown Artists job from Settings > Maintenance." }, { - "title": "🛡️ Matching Engine — Artist Verification Gate", + "title": "Matching Engine — Artist Verification Gate", "description": "Prevents downloading tracks from completely wrong artists on Soulseek and YouTube", "features": [ "• New artist gate rejects candidates where the artist doesn't match the target (Soulseek: < 0.25, YouTube: < 0.15)", @@ -21304,7 +21304,7 @@ def get_version_info(): "usage_note": "No action needed — matching improvements apply automatically to all new downloads." }, { - "title": "🎵 Deezer User Playlists — Browse & Download Your Library", + "title": "Deezer User Playlists — Browse & Download Your Library", "description": "New Deezer tab on the Sync page shows your personal playlists via ARL token — same flow as Spotify", "features": [ "• Click Refresh to load all your Deezer playlists with track counts", @@ -21316,7 +21316,7 @@ def get_version_info(): "usage_note": "Configure your ARL token in Settings > Connections or Downloads, then open the Deezer tab on the Sync page." }, { - "title": "🔒 Qobuz Token Auth — CAPTCHA Bypass", + "title": "Qobuz Token Auth — CAPTCHA Bypass", "description": "Qobuz added reCAPTCHA to their login — token auth lets you paste your session token directly", "features": [ "• New 'Auth Token' field on both Connections and Downloads tabs for Qobuz", @@ -21327,7 +21327,7 @@ def get_version_info(): "usage_note": "If Qobuz email/password login fails, use the Auth Token field instead." }, { - "title": "🛡️ Streaming Source Matching — Artist Gate", + "title": "Streaming Source Matching — Artist Gate", "description": "Tidal, Qobuz, HiFi, and Deezer downloads no longer match to wrong artists", "features": [ "• Artist similarity gate rejects candidates below 0.4 match threshold", @@ -21339,7 +21339,7 @@ def get_version_info(): "usage_note": "Downloads from official sources are now much more accurate. Check Download History for verification details." }, { - "title": "📋 Download History — Source Provenance", + "title": "Download History — Source Provenance", "description": "Collapsible download history with full source tracking and AcoustID verification badges", "features": [ "• Expected vs Downloaded comparison — shows what you asked for vs what the source provided", @@ -21351,7 +21351,7 @@ def get_version_info(): "usage_note": "Click 'Download History' on the Dashboard to see source provenance for new downloads." }, { - "title": "🔧 Fixes & Improvements", + "title": "Fixes & Improvements", "description": "Bug fixes, quality of life improvements, and new settings", "features": [ "• Dismissed maintenance findings no longer reappear on next scan — dedup check now includes dismissed status", @@ -21382,7 +21382,7 @@ def get_version_info(): ] }, { - "title": "🗺️ Artist Map — Visualize Your Music Universe", + "title": "Artist Map — Visualize Your Music Universe", "description": "Three interactive canvas-based visualization modes on the Discover page", "features": [ "• Watchlist Constellation — your watched artists as large nodes with similar artists orbiting around them", @@ -21401,7 +21401,7 @@ def get_version_info(): "usage_note": "Navigate to Discover and click the Artist Map section. Choose Watchlist, Genre, or Explorer mode." }, { - "title": "⚡ Wing It — Download or Sync Without Discovery", + "title": "Wing It — Download or Sync Without Discovery", "description": "Bypass metadata discovery and use raw track names directly", "features": [ "• Wing It button on all discovery modals and ListenBrainz Discover page cards", @@ -21412,10 +21412,10 @@ def get_version_info(): "• Live sync progress displayed inline just like normal sync", "• Download creates a bubble on the dashboard for progress tracking" ], - "usage_note": "Click the ⚡ Wing It button next to Start Discovery or Download Missing in any playlist modal." + "usage_note": "Click the Wing It button next to Start Discovery or Download Missing in any playlist modal." }, { - "title": "🔍 Global Search Bar — Search From Anywhere", + "title": "Global Search Bar — Search From Anywhere", "description": "Spotlight-style search bar accessible from every page", "features": [ "• Persistent search bar at the bottom of the screen — faded when idle, expands on focus", @@ -21429,7 +21429,7 @@ def get_version_info(): "usage_note": "Press / or Ctrl+K from any page, or click the search bar at the bottom of the screen." }, { - "title": "🔔 Redesigned Notification System", + "title": "Redesigned Notification System", "description": "Modern compact toasts with notification history and bell button", "features": [ "• Compact pill-shaped toasts in the bottom-right — one at a time, auto-dismiss after 3.5s", @@ -21442,7 +21442,7 @@ def get_version_info(): ] }, { - "title": "🔄 Track Redownload — Fix Mismatched Downloads", + "title": "Track Redownload — Fix Mismatched Downloads", "description": "Replace wrong downloads with the correct version using manual source selection", "features": [ "• Redownload button (↻) on each track in the enhanced library view", @@ -21456,10 +21456,10 @@ def get_version_info(): "• Download Blacklist: block specific sources from the Source Info popover — blacklisted sources skipped in all future downloads", "• Blacklist viewer on dashboard Tools section with remove capability" ], - "usage_note": "In the enhanced library view, click ↻ to redownload, ℹ for source info, or ✕ to delete." + "usage_note": "In the enhanced library view, click ↻ to redownload, ℹ for source info, or to delete." }, { - "title": "⚡ Spotify API Rate Limit Improvements", + "title": "Spotify API Rate Limit Improvements", "description": "Reduced Spotify API usage through caching and smart worker management", "features": [ "• get_artist_albums now cached — discography views, completion badges hit cache instead of API", @@ -21472,7 +21472,7 @@ def get_version_info(): ] }, { - "title": "🔧 Additional Fixes", + "title": "Additional Fixes", "description": "Bug fixes and quality-of-life improvements", "features": [ "• $discnum template variable — unpadded disc number for multi-disc album path templates", @@ -21485,7 +21485,7 @@ def get_version_info(): "• Genius API interval increased from 1.5s to 2s to reduce 429 rate limits", "• MusicBrainz cache now visible in Cache Browser with browse, clear, and clear-failed-only options", "• Cache Health popup shows MusicBrainz alongside other sources, 'Failed Lookups' clarified as MB-specific", - "• Block artists from discovery — hover any track in a discovery playlist and click ✕ to permanently exclude that artist", + "• Block artists from discovery — hover any track in a discovery playlist and click to permanently exclude that artist", "• Configurable concurrent downloads (1-10) — Settings → Downloads, Soulseek albums stay at 1", "• Streaming search sources — Apple Music results load progressively instead of blocking for 9+ seconds", "• API Rate Monitor — real-time speedometer gauges for all services on Dashboard, click for 24h history", @@ -21508,7 +21508,7 @@ def get_version_info(): ] }, { - "title": "🖥️ Server Playlist Manager — Compare & Fix Matches", + "title": "Server Playlist Manager — Compare & Fix Matches", "description": "Review and fix track matches between your source playlists and media server", "features": [ "• New Server Playlists tab (default on Sync page) — shows server playlists that match your mirrored playlists", @@ -21526,7 +21526,7 @@ def get_version_info(): "usage_note": "Navigate to Sync → Server Playlists tab. Click any playlist card to open the comparison editor." }, { - "title": "📊 Sync History Dashboard with Per-Track Details", + "title": "Sync History Dashboard with Per-Track Details", "description": "Dashboard shows recent syncs as visual cards with full per-track match data", "features": [ "• Recent Syncs section on dashboard with scrolling cards showing match percentage and health indicators", @@ -21537,7 +21537,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Japanese Song Searches Producing Gibberish", + "title": "Fix Japanese Song Searches Producing Gibberish", "description": "CJK text no longer mangled by unidecode in Soulseek search queries", "features": [ "• Japanese kanji, hiragana, katakana, and Korean hangul preserved in search queries", @@ -21546,7 +21546,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Partial Name Matching False Positives (#225)", + "title": "Fix Partial Name Matching False Positives (#225)", "description": "Track ownership check no longer falsely matches prefix/suffix variations", "features": [ "• 'Believe' no longer matches 'Believe In Me' — length ratio penalty prevents partial title matches", @@ -21555,7 +21555,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Pipeline Stops When Metadata Match Fails (#224)", + "title": "Fix Pipeline Stops When Metadata Match Fails (#224)", "description": "Playlist sync no longer drops tracks that failed iTunes/Apple Music discovery", "features": [ "• Tracks that fail metadata discovery now continue through the pipeline using original playlist data", @@ -21564,7 +21564,7 @@ def get_version_info(): ] }, { - "title": "🌳 Playlist Explorer — Visual Discovery Tree", + "title": "Playlist Explorer — Visual Discovery Tree", "description": "Use playlists as seeds to discover full albums and discographies", "features": [ "• New Explorer page with interactive tree visualization", @@ -21578,7 +21578,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix .LRC Files Written Without Timestamps", + "title": "Fix .LRC Files Written Without Timestamps", "description": "Plain lyrics now saved as .txt instead of invalid .lrc files", "features": [ "• Synced (timestamped) lyrics → .lrc file — valid format for Plex, Navidrome, Jellyfin", @@ -21588,7 +21588,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Collaborative Album Artist Not Applied to Singles (#215)", + "title": "Fix Collaborative Album Artist Not Applied to Singles (#215)", "description": "Single path template now respects the First Listed Artist setting", "features": [ "• Single downloads now include structured artists list for collab artist extraction", @@ -21597,7 +21597,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Enrichment Overwriting Manual Matches (#221)", + "title": "Fix Enrichment Overwriting Manual Matches (#221)", "description": "Enriching an entity that was manually matched no longer reverts the status to not_found", "features": [ "• Genius and AudioDB workers now check for existing service IDs before searching by name", @@ -21608,7 +21608,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Spotify OAuth ERR_EMPTY_RESPONSE in Docker (#220)", + "title": "Fix Spotify OAuth ERR_EMPTY_RESPONSE in Docker (#220)", "description": "OAuth callback server hardened for Docker/SSH tunnel setups", "features": [ "• Top-level error handler ensures an HTTP response is always sent (no more ERR_EMPTY_RESPONSE)", @@ -21619,7 +21619,7 @@ def get_version_info(): ] }, { - "title": "📊 Show All Services on Dashboard (#219)", + "title": "Show All Services on Dashboard (#219)", "description": "Dashboard now shows connection status for all external services, not just the core three", "features": [ "• Enrichment services shown as color-coded chips below core service cards", @@ -21632,7 +21632,7 @@ def get_version_info(): ] }, { - "title": "🔧 Add Qobuz to Connections Tab (#218)", + "title": "Add Qobuz to Connections Tab (#218)", "description": "Qobuz credentials now available on the Connections tab for metadata enrichment", "features": [ "• New Qobuz section on Settings → Connections tab for enrichment auth", @@ -21641,7 +21641,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Enrichment Widget Showing 'Running' When Rate Limited", + "title": "Fix Enrichment Widget Showing 'Running' When Rate Limited", "description": "Enrichment tooltip now shows Rate Limited or Daily Limit Reached instead of stuck on Running", "features": [ "• Shows 'Rate Limited' with countdown when Spotify rate limit is active", @@ -21650,7 +21650,7 @@ def get_version_info(): ] }, { - "title": "🧹 Metadata Cache Maintenance", + "title": "Metadata Cache Maintenance", "description": "The cache evictor now runs four maintenance phases to keep the metadata cache clean", "features": [ "• Input validation prevents junk entities (Unknown Artist, empty names) from being cached", @@ -21661,7 +21661,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Wishlist Download Selection Ignoring Checkboxes", + "title": "Fix Wishlist Download Selection Ignoring Checkboxes", "description": "Download Selection now respects which tracks are checked in the wishlist overview", "features": [ "• Selected track IDs are collected before closing the overview modal", @@ -21670,7 +21670,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Tidal OAuth Redirect URI in Docker", + "title": "Fix Tidal OAuth Redirect URI in Docker", "description": "Tidal OAuth now uses the configured redirect URI instead of the Docker container hostname", "features": [ "• Respects the redirect URI set in Settings instead of overriding with request hostname", @@ -21679,7 +21679,7 @@ def get_version_info(): ] }, { - "title": "🎨 High-Resolution Cover Art from Cover Art Archive", + "title": "High-Resolution Cover Art from Cover Art Archive", "description": "Album art now sourced from Cover Art Archive when available — often 1200x1200+ original quality", "features": [ "• Tries Cover Art Archive first using MusicBrainz release ID (full resolution)", @@ -21688,7 +21688,7 @@ def get_version_info(): ] }, { - "title": "🎵 Embedded Lyrics in Audio Files", + "title": "Embedded Lyrics in Audio Files", "description": "Lyrics are now embedded directly in audio file tags alongside the .lrc sidecar file", "features": [ "• Lyrics embedded as USLT (MP3), lyrics (FLAC/OGG), or ©lyr (M4A) tags", @@ -21697,7 +21697,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix AcoustID False Positives for Non-English Tracks", + "title": "Fix AcoustID False Positives for Non-English Tracks", "description": "AcoustID no longer quarantines correct files when titles are in different languages", "features": [ "• High-confidence fingerprint matches (95%+) now SKIP instead of FAIL when title/artist don't match", @@ -21706,7 +21706,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Soulseek Junk Tags Surviving Post-Processing", + "title": "Fix Soulseek Junk Tags Surviving Post-Processing", "description": "Tags from Soulseek source files are now wiped to disk immediately, before metadata enhancement", "features": [ "• Clears and saves tags before any API calls or metadata extraction", @@ -21716,7 +21716,7 @@ def get_version_info(): ] }, { - "title": "👁️ Watch All Unwatched Preview Modal", + "title": "Watch All Unwatched Preview Modal", "description": "The Watch All Unwatched button now opens a modal showing exactly which artists will be added", "features": [ "• Preview list shows all eligible artists with images, track counts, and matched sources", @@ -21727,7 +21727,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Watch All Unwatched Skipping Deezer Artists", + "title": "Fix Watch All Unwatched Skipping Deezer Artists", "description": "Watch All Unwatched now supports Deezer as an ID source", "features": [ "• Added Deezer ID support to the bulk watchlist add flow", @@ -21736,7 +21736,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Library Maintenance Path Fixes Failing Silently", + "title": "Fix Library Maintenance Path Fixes Failing Silently", "description": "Path mismatch fixes now use fresh config and report errors to the UI", "features": [ "• Transfer folder path is re-read from config before each fix attempt", @@ -21745,7 +21745,7 @@ def get_version_info(): ] }, { - "title": "🔧 Fix Spotify Manual Match Storing Wrong IDs", + "title": "Fix Spotify Manual Match Storing Wrong IDs", "description": "Manual match modals no longer store iTunes/Deezer IDs in Spotify ID columns", "features": [ "• Detects actual provider from result IDs — Spotify IDs are alphanumeric, iTunes/Deezer are numeric", @@ -21755,7 +21755,7 @@ def get_version_info(): ] }, { - "title": "🛡️ Spotify Enrichment Daily Budget", + "title": "Spotify Enrichment Daily Budget", "description": "The background enrichment worker now caps itself at 3,000 items per day to prevent rate limit bans", "features": [ "• Worker-only daily budget — user-initiated searches, playlist operations, etc. are unaffected", @@ -21765,7 +21765,7 @@ def get_version_info(): ] }, { - "title": "🎧 Deezer Download Source", + "title": "Deezer Download Source", "description": "Download music directly from Deezer with ARL authentication", "features": [ "• New download source: Deezer joins Soulseek, YouTube, Tidal, Qobuz, and HiFi", @@ -21777,7 +21777,7 @@ def get_version_info(): ] }, { - "title": "🔍 Cache-Powered Discovery", + "title": "Cache-Powered Discovery", "description": "Five new discover sections mined from your metadata cache — zero API calls", "features": [ "• Undiscovered Albums: albums by your most-played artists that aren't in your library", @@ -21789,7 +21789,7 @@ def get_version_info(): ] }, { - "title": "🎶 Genre Deep Dive Modal", + "title": "Genre Deep Dive Modal", "description": "Tap any genre pill to explore artists, tracks, and albums in that genre", "features": [ "• Artists section with scaled avatars — top artist gets largest, 'In Library' badges", @@ -21802,7 +21802,7 @@ def get_version_info(): ] }, { - "title": "💾 Database Storage Visualization", + "title": "Database Storage Visualization", "description": "See how your database space is distributed across tables", "features": [ "• Donut chart on Stats page showing storage breakdown by table", @@ -21812,7 +21812,7 @@ def get_version_info(): ] }, { - "title": "⚡ Library Page Performance", + "title": "Library Page Performance", "description": "Library artist grid loads significantly faster with smoother animations", "features": [ "• innerHTML batch rendering replaces per-card DOM manipulation — near-instant grid population", @@ -21822,7 +21822,7 @@ def get_version_info(): ] }, { - "title": "🔄 Per-Artist Enrichment Rings", + "title": "Per-Artist Enrichment Rings", "description": "See metadata coverage for each artist on their detail page", "features": [ "• SVG ring indicators for all 9 enrichment services below the album/EP/singles bars", @@ -21832,7 +21832,7 @@ def get_version_info(): ] }, { - "title": "📱 Mobile Responsive Overhaul", + "title": "Mobile Responsive Overhaul", "description": "Comprehensive mobile layout fixes across all pages", "features": [ "• Stats, Automations, Hydrabase, Issues, Help pages now fully mobile responsive", @@ -21842,7 +21842,7 @@ def get_version_info(): ] }, { - "title": "🎵 Album Split Fix (Navidrome)", + "title": "Album Split Fix (Navidrome)", "description": "Prevent deluxe/standard editions from splitting into separate albums", "features": [ "• MusicBrainz release cache key normalized — strips edition suffixes (Deluxe, Remastered, etc.)", @@ -21852,7 +21852,7 @@ def get_version_info(): ] }, { - "title": "🏷️ Picard-Style Album Tagging", + "title": "Picard-Style Album Tagging", "description": "All tracks in an album now get the same MusicBrainz release ID automatically", "features": [ "• Pre-flight MB release lookup before album tracks start downloading", @@ -21862,7 +21862,7 @@ def get_version_info(): ] }, { - "title": "🛠️ Enrichment & Repair Fixes", + "title": "Enrichment & Repair Fixes", "description": "Critical fixes for background workers and maintenance jobs", "features": [ "• All 9 enrichment workers: error status items no longer auto-retry in infinite loops", @@ -21873,7 +21873,7 @@ def get_version_info(): ] }, { - "title": "🔗 Automation Signal Chain Fix", + "title": "Automation Signal Chain Fix", "description": "Event-triggered automations now receive playlist context properly", "features": [ "• playlist_id forwarded from events to action handlers (fixes silent 'No playlist specified')", @@ -21883,7 +21883,7 @@ def get_version_info(): ] }, { - "title": "🎨 Unified Glass UI Redesign", + "title": "Unified Glass UI Redesign", "description": "Consistent visual style across all cards, modals, and buttons", "features": [ "• Dashboard tool cards, service cards, and stat cards: unified glass style", @@ -21894,7 +21894,7 @@ def get_version_info(): ] }, { - "title": "🎧 Scrobbling to Last.fm & ListenBrainz", + "title": "Scrobbling to Last.fm & ListenBrainz", "description": "Automatically scrobble your plays from Plex, Jellyfin, or Navidrome", "features": [ "• Listen on your media server — SoulSync automatically scrobbles to Last.fm and/or ListenBrainz", @@ -21903,7 +21903,7 @@ def get_version_info(): ] }, { - "title": "🧠 Personalized Discovery + Listening Stats", + "title": "Personalized Discovery + Listening Stats", "description": "Discovery playlists use your listening history, plus a full stats dashboard", "features": [ "• Release Radar, Discovery Weekly, and Because You Listen To: personalized by play history", @@ -21913,7 +21913,7 @@ def get_version_info(): ] }, { - "title": "❓ Interactive Help System", + "title": "Interactive Help System", "description": "Full contextual help platform accessible from the floating ? button", "features": [ "• 200+ contextual help entries — click any UI element to learn what it does", @@ -21929,7 +21929,7 @@ def get_version_info(): ] }, { - "title": "🎤 Rich Artist Profiles", + "title": "Rich Artist Profiles", "description": "Full-bleed hero section on the Artists page with deep metadata", "features": [ "• Large portrait image with blurred background, glassmorphic design", @@ -21939,7 +21939,7 @@ def get_version_info(): ] }, { - "title": "📚 Enhanced Library Manager", + "title": "Enhanced Library Manager", "description": "Inline metadata editing and tag writing from the library view", "features": [ "• Toggle between Standard and Enhanced view on any artist detail page", @@ -21950,7 +21950,7 @@ def get_version_info(): ] }, { - "title": "🏷️ In Library Badges + Search Improvements", + "title": "In Library Badges + Search Improvements", "description": "Know what you already own before downloading", "features": [ "• 'In Library' badges on enhanced search album and track results", @@ -21960,7 +21960,7 @@ def get_version_info(): ] }, { - "title": "🎵 FLAC Bit Depth + Quality Filter", + "title": "FLAC Bit Depth + Quality Filter", "description": "Finer control over audio quality preferences", "features": [ "• Quality profile enforces 16-bit vs 24-bit FLAC preference", @@ -21970,7 +21970,7 @@ def get_version_info(): ] }, { - "title": "🔧 Enrichment Worker Improvements", + "title": "Enrichment Worker Improvements", "description": "Better name matching and quieter logs across all 8+ workers", "features": [ "• Dash-suffix normalization: 'Title - Remix' now matches 'Title (Remix)' across all workers", @@ -21981,7 +21981,7 @@ def get_version_info(): ] }, { - "title": "🔒 Launch PIN Lock Screen", + "title": "Launch PIN Lock Screen", "description": "Protect SoulSync access with a PIN on every page load", "features": [ "• Toggle in Settings → Advanced → Security to require PIN on launch", @@ -21992,7 +21992,7 @@ def get_version_info(): ] }, { - "title": "🎵 Stream Source Setting", + "title": "Stream Source Setting", "description": "Choose where track previews come from — independent of download source", "features": [ "• New dropdown in Settings → Downloads: YouTube (instant, default) or Active Download Source", @@ -22001,7 +22001,7 @@ def get_version_info(): ] }, { - "title": "🔧 YouTube Download Fix", + "title": "YouTube Download Fix", "description": "Fixed 'Requested format not available' errors affecting all YouTube downloads", "features": [ "• Removed stale player_client and HLS/DASH skip overrides that blocked audio formats", @@ -22010,7 +22010,7 @@ def get_version_info(): ] }, { - "title": "📊 Accurate Album Completion Badges", + "title": "Accurate Album Completion Badges", "description": "Album completion now uses exact track counts instead of percentage rounding", "features": [ "• Exact match: 'Complete' only when all tracks are present — no more 90% rounding", @@ -22020,7 +22020,7 @@ def get_version_info(): ] }, { - "title": "👥 Collaborative Album Handling", + "title": "Collaborative Album Handling", "description": "Smart folder naming and matching for albums with multiple artists", "features": [ "• New setting: Collaborative Album Artist — use first listed artist or all combined", @@ -22030,7 +22030,7 @@ def get_version_info(): ] }, { - "title": "🔄 Per-Artist Library Sync", + "title": "Per-Artist Library Sync", "description": "Validate and clean up individual artist library entries", "features": [ "• New 'Sync' button on enhanced library view", @@ -22040,7 +22040,7 @@ def get_version_info(): ] }, { - "title": "🛠️ Stability & Bug Fixes", + "title": "Stability & Bug Fixes", "description": "Various fixes for crashes, data integrity, and UX", "features": [ "• Enrichment worker pause state persists across restarts", @@ -22067,7 +22067,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔊 Lossy Codec Expansion + Retroactive Converter", + "title": "Lossy Codec Expansion + Retroactive Converter", "description": "Opus and AAC support for post-download conversion, plus a repair job for existing files", "features": [ "• Lossy copy now supports MP3, Opus, and AAC (M4A) — configurable codec and bitrate", @@ -22079,7 +22079,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📥 Smarter Staging Import", + "title": "Smarter Staging Import", "description": "Tag-first matching and auto-grouping for the import workflow", "features": [ "• Tags take priority over filename parsing — no more '08' as artist name", @@ -22089,7 +22089,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎨 Library Artist Hero Redesign", + "title": "Library Artist Hero Redesign", "description": "Expanded artist detail section with Last.fm integration", "features": [ "• Horizontal service badge row with hover lift animations", @@ -22100,7 +22100,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🌐 Hydrabase Search & Routing", + "title": "Hydrabase Search & Routing", "description": "Hydrabase shows as a search tab with proper ID routing", "features": [ "• Hydrabase appears as a source tab on enhanced search when connected", @@ -22110,7 +22110,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔧 Orphan File Detector + MusicBrainz Fixes", + "title": "Orphan File Detector + MusicBrainz Fixes", "description": "Better orphan detection and album version matching", "features": [ "• Orphan detector: normalized tag matching strips feat./parentheticals to reduce false positives", @@ -22120,7 +22120,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📅 Release Year Collection", + "title": "Release Year Collection", "description": "Post-processing now collects release year from all metadata sources", "features": [ "• Year extracted from MusicBrainz, Deezer, Tidal, Qobuz during post-processing", @@ -22129,7 +22129,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔍 Multi-Source Search Tabs", + "title": "Multi-Source Search Tabs", "description": "View search results from Spotify, iTunes, and Deezer side by side", "features": [ "• Enhanced search now fires parallel queries against all available metadata sources", @@ -22141,7 +22141,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "👥 Per-Profile Service Credentials", + "title": "Per-Profile Service Credentials", "description": "Each profile can connect their own Spotify, Tidal, and media server library", "features": [ "• Non-admin profiles can enter their own Spotify credentials and authenticate their own account", @@ -22153,7 +22153,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "⚙️ Modern Settings Redesign", + "title": "Modern Settings Redesign", "description": "Settings page rebuilt with tabbed single-column layout", "features": [ "• Horizontal tab bar: Connections, Downloads, Library, Appearance, Advanced", @@ -22164,7 +22164,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔄 Hybrid N-Source Download Priority", + "title": "Hybrid N-Source Download Priority", "description": "Hybrid mode now supports all 5 download sources with drag-to-reorder priority", "features": [ "• Enable/disable any combination of Soulseek, YouTube, Tidal, Qobuz, and HiFi", @@ -22175,7 +22175,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🚀 Automation Hub Pipelines", + "title": "Automation Hub Pipelines", "description": "One-click deployment of multi-automation pipelines", "features": [ "• 11 pre-built pipelines: Release Radar, Discovery Weekly, Playlist Auto-Sync, Nightly Operations, and more", @@ -22186,7 +22186,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📂 Staging Folder Pre-Download Check", + "title": "Staging Folder Pre-Download Check", "description": "Check your staging folder for existing files before downloading", "features": [ "• Before searching Soulseek/YouTube, checks the staging folder for a matching file", @@ -22196,7 +22196,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🛡️ Library Safety & Repair Fixes", + "title": "Library Safety & Repair Fixes", "description": "Critical fixes for library maintenance jobs and safety guards", "features": [ "• Mass orphan safety guard — 'witness me' confirmation required when >50% of files flagged as orphans", @@ -22213,7 +22213,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎵 Deezer Metadata Source", + "title": "Deezer Metadata Source", "description": "Deezer added as a configurable free metadata fallback alongside iTunes/Apple Music", "features": [ "• New setting to choose between iTunes and Deezer as your fallback metadata source — switch anytime from Settings", @@ -22225,7 +22225,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📜 Library History", + "title": "Library History", "description": "Persistent record of every download and server import — viewable from the dashboard", "features": [ "• History button next to Recent Activity opens a modal with Downloads and Server Imports tabs", @@ -22236,7 +22236,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔬 MusicBrainz MBID Mismatch Repair", + "title": "MusicBrainz MBID Mismatch Repair", "description": "New repair job to detect and fix wrong MusicBrainz recording IDs on library tracks", "features": [ "• Detects tracks where the stored MusicBrainz recording ID resolves to a different title than expected", @@ -22245,7 +22245,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎵 HiFi Download Source", + "title": "HiFi Download Source", "description": "Free lossless downloads via public hifi-api instances — no account or subscription required", "features": [ "• New download mode alongside Soulseek, YouTube, Tidal, and Qobuz — select HiFi Only or use in hybrid mode", @@ -22256,7 +22256,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔗 Spotify Link (No API Credentials)", + "title": "Spotify Link (No API Credentials)", "description": "Scrape Spotify playlists and albums by URL without needing Spotify API credentials", "features": [ "• New Spotify Link tab on the playlist sync page — paste any public Spotify playlist or album URL", @@ -22266,7 +22266,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔧 Library Maintenance Suite", + "title": "Library Maintenance Suite", "description": "Full-featured library repair system with 9 automated jobs, fix actions, and rich findings UI", "features": [ "• 9 repair jobs: track number mismatch, dead files, duplicates, metadata gaps, album completeness, missing cover art, AcoustID scanner, orphan files, fake lossless detection", @@ -22279,7 +22279,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🏷️ Post-Processing Enhancements", + "title": "Post-Processing Enhancements", "description": "Granular control over post-processing and richer file tagging", "features": [ "• Granular toggles for each post-processing step — enable/disable metadata services, cover art, and lyrics individually", @@ -22288,7 +22288,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🧠 Per-Profile ListenBrainz", + "title": "Per-Profile ListenBrainz", "description": "Each profile can connect their own ListenBrainz account for personalized playlists", "features": [ "• Personal settings modal with ListenBrainz connect/disconnect flow", @@ -22298,7 +22298,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "⬆️ Quality Enhance", + "title": "Quality Enhance", "description": "Upgrade existing library tracks to higher quality versions", "features": [ "• Quality enhance button on library tracks — find and download a higher quality version", @@ -22307,7 +22307,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📀 Hi-Res FLAC Downsampling", + "title": "Hi-Res FLAC Downsampling", "description": "Automatically convert 24-bit hi-res downloads to 16-bit/44.1kHz CD quality", "features": [ "• New toggle in Settings → Post-Download Conversion: downsample hi-res FLAC to CD quality after download", @@ -22319,7 +22319,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🐛 Recent Bug Fixes & Improvements", + "title": "Recent Bug Fixes & Improvements", "description": "Stability fixes, UX improvements, and edge case handling", "features": [ "• Fix $year template variable empty for playlist/sync downloads — album metadata now backfilled from Spotify API", @@ -22351,7 +22351,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📋 Library Issue Reporting", + "title": "Library Issue Reporting", "description": "Report and track issues for tracks, albums, and artists directly from the library", "features": [ "• Report issues on any library item — tracks, albums, or artists — with category, priority, and notes", @@ -22362,7 +22362,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📁 Album File Reorganization", + "title": "Album File Reorganization", "description": "Reorganize album files on disk from the Enhanced Library Manager", "features": [ "• Move and rename album files to match your configured folder template", @@ -22372,7 +22372,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📖 Interactive REST API Docs", + "title": "Interactive REST API Docs", "description": "Full API documentation with a built-in endpoint tester", "features": [ "• Comprehensive docs for all API endpoints organized by category", @@ -22382,7 +22382,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "👁️ Watchlist Improvements", + "title": "Watchlist Improvements", "description": "Smarter cross-provider matching, manual artist linking, and scan timestamp fixes", "features": [ "• Cross-provider artist matching now uses fuzzy name comparison instead of blindly taking the first result", @@ -22394,7 +22394,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔊 AcoustID Verification Fix", + "title": "AcoustID Verification Fix", "description": "More accurate audio file verification with broader title normalization", "features": [ "• Strip ALL parentheticals in title normalization — fixes false mismatches for parody, soundtrack, and featured artist suffixes", @@ -22402,7 +22402,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎧 Deezer Playlist Sync", + "title": "Deezer Playlist Sync", "description": "Full Deezer integration for playlist sync alongside Spotify, Tidal, and YouTube", "features": [ "• Import and sync Deezer playlists with full track matching and discovery", @@ -22412,7 +22412,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🚀 Discovery Page Improvements", + "title": "Discovery Page Improvements", "description": "Better playlist generation, caching, and iTunes parity", "features": [ "• iTunes discovery playlists now produce quality results — synthetic popularity scoring replaces broken 0-popularity tiering", @@ -22426,7 +22426,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🛡️ Rate Limit Detection Fix", + "title": "Rate Limit Detection Fix", "description": "Rate limit handling completely overhauled — escalating bans, no more rate limit loops", "features": [ "• Fixed rate limits going undetected in get_album, get_artist, and batch artist enrichment", @@ -22439,7 +22439,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📦 Download & Matching Fixes", + "title": "Download & Matching Fixes", "description": "Accuracy improvements for album downloads and track matching", "features": [ "• Album download pre-flight search finds complete album folders before track-by-track downloading", @@ -22450,7 +22450,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔒 Security & Config", + "title": "Security & Config", "description": "Encryption at rest and config improvements", "features": [ "• Sensitive config values (API keys, passwords, tokens) encrypted at rest with Fernet", @@ -22459,7 +22459,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🐛 Recent Bug Fixes", + "title": "Recent Bug Fixes", "description": "Stability and UX fixes", "features": [ "• Fix sync stuck at 80% — serialize datetime in SyncResult for WebSocket emit", @@ -22473,7 +22473,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎵 Tidal & Qobuz Enrichment Workers", + "title": "Tidal & Qobuz Enrichment Workers", "description": "Two new background enrichment workers for Tidal and Qobuz metadata", "features": [ "• Tidal worker enriches artists, albums, and tracks with Tidal IDs, thumbnails, and metadata", @@ -22487,7 +22487,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎧 Full Qobuz Support", + "title": "Full Qobuz Support", "description": "Qobuz added as a first-class download source alongside Tidal and Soulseek", "features": [ "• Search, browse, and download from Qobuz with quality selection up to Hi-Res 24-bit/192kHz", @@ -22497,7 +22497,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔀 Hybrid Mode Redesign", + "title": "Hybrid Mode Redesign", "description": "Overhauled download source selection and priority system", "features": [ "• Redesigned hybrid mode with drag-and-drop source priority ordering", @@ -22507,7 +22507,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🛡️ Spotify Rate Limit Protection", + "title": "Spotify Rate Limit Protection", "description": "Smart detection and handling of Spotify API rate limits with escalating bans", "features": [ "• Automatic detection of long rate limit bans (Retry-After > 60s) from Spotify", @@ -22522,7 +22522,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "👤 Profile Permissions & Page Access Control", + "title": "Profile Permissions & Page Access Control", "description": "Granular admin controls over what each profile can see and do", "features": [ "• Admin can control which sidebar pages each profile can access", @@ -22533,7 +22533,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🎵 Now Playing Overhaul", + "title": "Now Playing Overhaul", "description": "Redesigned media player with expanded Now Playing modal and smart radio", "features": [ "• Expanded Now Playing modal — click the sidebar player to open a full-screen experience", @@ -22546,7 +22546,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "📚 Enhanced Library Manager", + "title": "Enhanced Library Manager", "description": "Professional-grade library management with tag writing and server sync", "features": [ "• Toggle between Standard and Enhanced views on any artist's discography", @@ -22563,7 +22563,7 @@ _OLD_V2_NOTES = r""" "usage_note": "Open any artist's detail page and click 'Enhanced' in the view toggle to access the library manager." }, { - "title": "🎶 Last.fm & Genius Enrichment Workers", + "title": "Last.fm & Genius Enrichment Workers", "description": "Background enrichment workers for Last.fm and Genius metadata", "features": [ "• Last.fm worker enriches artists, albums, and tracks with listener counts, play counts, tags, and bios", @@ -22574,7 +22574,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "✨ UI & Visual Overhaul", + "title": "UI & Visual Overhaul", "description": "Per-page particle animations, sidebar visualizer, watchlist redesign, and design refresh", "features": [ "• Per-page particle animations with unique themes for each page", @@ -22590,7 +22590,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🔧 Tidal Download Improvements", + "title": "Tidal Download Improvements", "description": "Stability and accuracy fixes for Tidal downloads", "features": [ "• Tidal download validation — detect and clean up unplayable hi-res stubs", @@ -22600,7 +22600,7 @@ _OLD_V2_NOTES = r""" ] }, { - "title": "🐛 Bug Fixes & Stability", + "title": "Bug Fixes & Stability", "description": "Reliability improvements across the board", "features": [ "• Fix Genius search blindly matching wrong artists — all bad matches auto-reset", @@ -22618,7 +22618,7 @@ _OLD_V2_NOTES = r""" def _simple_monitor_task(): """The actual monitoring task that runs in the background thread. Search cleanup and download cleanup are now handled by system automations.""" - print("🔄 Simple background monitor started") + print("Simple background monitor started") while True: try: @@ -22639,12 +22639,12 @@ def _simple_monitor_task(): if current_time - data['first_attempt'] > 60 ] for key in stale_keys: - print(f"🧹 Cleaning up stale retry attempt: {key}") + print(f"Cleaning up stale retry attempt: {key}") del _download_retry_attempts[key] time.sleep(1) except Exception as e: - print(f"❌ Simple monitor error: {e}") + print(f"Simple monitor error: {e}") time.sleep(10) def start_simple_background_monitor(): @@ -22663,7 +22663,7 @@ def _sanitize_track_data_for_processing(track_data): Preserves album dict to retain full metadata (images, id, etc.) and normalizes artist field. """ if not isinstance(track_data, dict): - print(f"⚠️ [Sanitize] Unexpected track data type: {type(track_data)}") + print(f"[Sanitize] Unexpected track data type: {type(track_data)}") return track_data # Create a copy to avoid modifying original data @@ -22688,7 +22688,7 @@ def _sanitize_track_data_for_processing(track_data): processed_artists.append(str(artist)) sanitized['artists'] = processed_artists else: - print(f"⚠️ [Sanitize] Unexpected artists format: {type(raw_artists)}") + print(f"[Sanitize] Unexpected artists format: {type(raw_artists)}") sanitized['artists'] = [str(raw_artists)] if raw_artists else [] return sanitized @@ -22711,7 +22711,7 @@ def check_and_recover_stuck_flags(): time_stuck = current_time - wishlist_auto_processing_timestamp if time_stuck > stuck_timeout: stuck_minutes = time_stuck / 60 - print(f"⚠️ [Stuck Detection] Wishlist auto-processing flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING") + print(f"[Stuck Detection] Wishlist auto-processing flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING") with wishlist_timer_lock: wishlist_auto_processing = False wishlist_auto_processing_timestamp = 0 @@ -22723,7 +22723,7 @@ def check_and_recover_stuck_flags(): time_stuck = current_time - watchlist_auto_scanning_timestamp if time_stuck > stuck_timeout: stuck_minutes = time_stuck / 60 - print(f"⚠️ [Stuck Detection] Watchlist auto-scanning flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING") + print(f"[Stuck Detection] Watchlist auto-scanning flag has been stuck for {stuck_minutes:.1f} minutes - RESETTING") with watchlist_timer_lock: watchlist_auto_scanning = False watchlist_auto_scanning_timestamp = 0 @@ -22748,7 +22748,7 @@ def is_wishlist_actually_processing(): # If more than 15 minutes, flag is stuck - auto-recover and return False if time_since_start > 900: # 15 minutes stuck_minutes = time_since_start / 60 - print(f"⚠️ [Stuck Detection] Wishlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering") + print(f"[Stuck Detection] Wishlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering") check_and_recover_stuck_flags() return False @@ -22771,7 +22771,7 @@ def is_watchlist_actually_scanning(): # If more than 15 minutes, flag is stuck - auto-recover and return False if time_since_start > 900: # 15 minutes stuck_minutes = time_since_start / 60 - print(f"⚠️ [Stuck Detection] Watchlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering") + print(f"[Stuck Detection] Watchlist flag stuck for {stuck_minutes:.1f} minutes - auto-recovering") check_and_recover_stuck_flags() return False @@ -22818,13 +22818,13 @@ def _process_wishlist_automatically(automation_id=None): """Main automatic processing logic that runs in background thread.""" global wishlist_auto_processing, wishlist_auto_processing_timestamp - print("🤖 [Auto-Wishlist] Timer triggered - starting automatic wishlist processing...") + print("[Auto-Wishlist] Timer triggered - starting automatic wishlist processing...") try: # CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock # This prevents deadlock and handles stuck flags (2-hour timeout) if is_wishlist_actually_processing(): - print("⚠️ [Auto-Wishlist] Already processing (verified with stuck detection), skipping.") + print("[Auto-Wishlist] Already processing (verified with stuck detection), skipping.") return # Check conditions and set flag @@ -22833,7 +22833,7 @@ def _process_wishlist_automatically(automation_id=None): with wishlist_timer_lock: # Re-check inside lock to handle race conditions if wishlist_auto_processing: - print("⚠️ [Auto-Wishlist] Already processing (race condition check), skipping.") + print("[Auto-Wishlist] Already processing (race condition check), skipping.") should_skip_already_running = True else: @@ -22841,7 +22841,7 @@ def _process_wishlist_automatically(automation_id=None): import time wishlist_auto_processing = True wishlist_auto_processing_timestamp = time.time() - print(f"🔒 [Auto-Wishlist] Flag set at timestamp {wishlist_auto_processing_timestamp}") + print(f"[Auto-Wishlist] Flag set at timestamp {wishlist_auto_processing_timestamp}") if should_skip_already_running: return @@ -22855,7 +22855,7 @@ def _process_wishlist_automatically(automation_id=None): database = get_database() all_profiles = database.get_all_profiles() count = sum(wishlist_service.get_wishlist_count(profile_id=p['id']) for p in all_profiles) - print(f"🔍 [Auto-Wishlist] Wishlist count check: {count} tracks found across {len(all_profiles)} profiles") + print(f"[Auto-Wishlist] Wishlist count check: {count} tracks found across {len(all_profiles)} profiles") _update_automation_progress(automation_id, progress=10, phase='Checking wishlist', log_line=f'{count} tracks across {len(all_profiles)} profiles', log_type='info') if count == 0: @@ -22865,7 +22865,7 @@ def _process_wishlist_automatically(automation_id=None): wishlist_auto_processing_timestamp = 0 return - print(f"🎵 [Auto-Wishlist] Found {count} tracks in wishlist, starting automatic processing...") + print(f"[Auto-Wishlist] Found {count} tracks in wishlist, starting automatic processing...") # Check if wishlist processing is already active (auto or manual) playlist_id = "wishlist" @@ -22875,7 +22875,7 @@ def _process_wishlist_automatically(automation_id=None): # Check for both auto ('wishlist') and manual ('wishlist_manual') batches if (batch_playlist_id in ['wishlist', 'wishlist_manual'] and batch_data.get('phase') not in ['complete', 'error', 'cancelled']): - print(f"⚠️ Wishlist processing already active in another batch ({batch_playlist_id}), skipping automatic start") + print(f"Wishlist processing already active in another batch ({batch_playlist_id}), skipping automatic start") with wishlist_timer_lock: wishlist_auto_processing = False return @@ -22885,15 +22885,15 @@ def _process_wishlist_automatically(automation_id=None): from database.music_database import MusicDatabase db = MusicDatabase() - print("🧹 [Auto-Wishlist] Cleaning duplicate tracks before processing...") + print("[Auto-Wishlist] Cleaning duplicate tracks before processing...") for p in all_profiles: duplicates_removed = db.remove_wishlist_duplicates(profile_id=p['id']) if duplicates_removed > 0: - print(f"🧹 [Auto-Wishlist] Removed {duplicates_removed} duplicate tracks from profile {p['id']}") + print(f"[Auto-Wishlist] Removed {duplicates_removed} duplicate tracks from profile {p['id']}") # CLEANUP: Remove tracks from wishlist that already exist in library # This prevents wasting bandwidth on tracks we already have - print("🧼 [Auto-Wishlist] Checking wishlist against library for already-owned tracks...") + print("[Auto-Wishlist] Checking wishlist against library for already-owned tracks...") cleanup_tracks = [] for p in all_profiles: cleanup_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id'])) @@ -22938,12 +22938,12 @@ def _process_wishlist_automatically(automation_id=None): removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True) if removed: cleanup_removed += 1 - print(f"🧼 [Auto-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}") + print(f"[Auto-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}") except Exception as remove_error: - print(f"⚠️ [Auto-Wishlist] Error removing track from wishlist: {remove_error}") + print(f"[Auto-Wishlist] Error removing track from wishlist: {remove_error}") if cleanup_removed > 0: - print(f"✅ [Auto-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist") + print(f"[Auto-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist") _update_automation_progress(automation_id, progress=25, phase='Cleaned up duplicates', log_line=f'Removed {cleanup_removed} already-owned tracks', log_type='success') else: @@ -22955,7 +22955,7 @@ def _process_wishlist_automatically(automation_id=None): for p in all_profiles: raw_wishlist_tracks.extend(wishlist_service.get_wishlist_tracks_for_download(profile_id=p['id'])) if not raw_wishlist_tracks: - print("⚠️ No tracks returned from wishlist service.") + print("No tracks returned from wishlist service.") with wishlist_timer_lock: wishlist_auto_processing = False wishlist_auto_processing_timestamp = 0 @@ -22979,8 +22979,8 @@ def _process_wishlist_automatically(automation_id=None): seen_track_ids_sanitation.add(spotify_track_id) if duplicates_found > 0: - print(f"⚠️ [Auto-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization") - print(f"🔧 [Auto-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service") + print(f"[Auto-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization") + print(f"[Auto-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service") # CYCLE FILTERING: Get current cycle and filter tracks by category with db._get_connection() as conn: @@ -23018,8 +23018,8 @@ def _process_wishlist_automatically(automation_id=None): # No ID - can't deduplicate safely, always add filtered_tracks.append(track) - print(f"🔄 [Auto-Wishlist] Current cycle: {current_cycle}") - print(f"📊 [Auto-Wishlist] Filtered {len(filtered_tracks)}/{len(wishlist_tracks)} tracks for '{current_cycle}' category") + print(f"[Auto-Wishlist] Current cycle: {current_cycle}") + print(f"[Auto-Wishlist] Filtered {len(filtered_tracks)}/{len(wishlist_tracks)} tracks for '{current_cycle}' category") _update_automation_progress(automation_id, progress=40, phase=f'Processing {current_cycle}', log_line=f'Cycle: {current_cycle} — {len(filtered_tracks)} tracks to process', log_type='info') @@ -23036,7 +23036,7 @@ def _process_wishlist_automatically(automation_id=None): VALUES ('wishlist_cycle', ?, CURRENT_TIMESTAMP) """, (next_cycle,)) conn.commit() - print(f"🔄 [Auto-Wishlist] Cycle toggled: {current_cycle} → {next_cycle}") + print(f"[Auto-Wishlist] Cycle toggled: {current_cycle} → {next_cycle}") with wishlist_timer_lock: wishlist_auto_processing = False @@ -23079,7 +23079,7 @@ def _process_wishlist_automatically(automation_id=None): 'profile_id': 1 } - print(f"🚀 Starting automatic wishlist batch {batch_id} with {len(wishlist_tracks)} tracks") + print(f"Starting automatic wishlist batch {batch_id} with {len(wishlist_tracks)} tracks") _update_automation_progress(automation_id, progress=50, phase=f'Downloading {len(wishlist_tracks)} tracks', log_line=f'Started batch: {len(wishlist_tracks)} {current_cycle}', log_type='success') @@ -23089,7 +23089,7 @@ def _process_wishlist_automatically(automation_id=None): # Don't mark auto_processing as False here - let completion handler do it except Exception as e: - print(f"❌ Error in automatic wishlist processing: {e}") + print(f"Error in automatic wishlist processing: {e}") import traceback traceback.print_exc() _update_automation_progress(automation_id, log_line=f'Error: {str(e)}', log_type='error') @@ -23104,7 +23104,7 @@ def _process_wishlist_automatically(automation_id=None): # =============================== def _db_update_progress_callback(current_item, processed, total, percentage): - print(f"📊 [DB Progress] {current_item} - {processed}/{total} ({percentage:.1f}%)") + print(f"[DB Progress] {current_item} - {processed}/{total} ({percentage:.1f}%)") with db_update_lock: db_update_state.update({ "current_item": current_item, @@ -23117,7 +23117,7 @@ def _db_update_progress_callback(current_item, processed, total, percentage): current_item=current_item) def _db_update_phase_callback(phase): - print(f"🔄 [DB Phase] {phase}") + print(f"[DB Phase] {phase}") with db_update_lock: db_update_state["phase"] = phase _update_automation_progress(_db_update_automation_id, phase=phase) @@ -23172,7 +23172,7 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ summary = f"{total_tracks} tracks, {total_albums} albums, {total_artists} artists processed" if removed_artists > 0 or removed_albums > 0: summary += f" | {removed_artists} artists, {removed_albums} albums removed" - add_activity_item("✅", "Database Update Complete", summary, "Now") + add_activity_item("", "Database Update Complete", summary, "Now") try: if automation_engine: @@ -23189,17 +23189,17 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ inv_db = get_database() cleared = inv_db.invalidate_sync_match_cache() if cleared: - logger.info(f"🗑️ Cleared {cleared} sync match cache entries after database update") + logger.info(f"Cleared {cleared} sync match cache entries after database update") except Exception: pass # WISHLIST CLEANUP: Automatically clean up wishlist after database update try: - print("📋 [DB Update] Database update completed, starting automatic wishlist cleanup...") + print("[DB Update] Database update completed, starting automatic wishlist cleanup...") # Run cleanup in background to avoid blocking the UI missing_download_executor.submit(_automatic_wishlist_cleanup_after_db_update) except Exception as cleanup_error: - print(f"⚠️ [DB Update] Error starting automatic wishlist cleanup: {cleanup_error}") + print(f"[DB Update] Error starting automatic wishlist cleanup: {cleanup_error}") def _db_update_error_callback(error_message): global _db_update_automation_id @@ -23214,7 +23214,7 @@ def _db_update_error_callback(error_message): _db_update_automation_id = None # Add activity for database update error - add_activity_item("❌", "Database Update Failed", error_message, "Now") + add_activity_item("", "Database Update Failed", error_message, "Now") _workers_paused_by_scan = set() # Track which workers WE paused (don't resume manually-paused ones) @@ -23233,7 +23233,7 @@ def _pause_workers_for_scan(): w.pause() _workers_paused_by_scan.add(name) if _workers_paused_by_scan: - print(f"⏸️ Paused {len(_workers_paused_by_scan)} workers during database scan: {', '.join(_workers_paused_by_scan)}") + print(f"Paused {len(_workers_paused_by_scan)} workers during database scan: {', '.join(_workers_paused_by_scan)}") def _resume_workers_after_scan(): """Resume only the workers that WE paused (don't resume manually-paused ones).""" @@ -23250,7 +23250,7 @@ def _resume_workers_after_scan(): w.resume() resumed += 1 if resumed: - print(f"▶️ Resumed {resumed} workers after database scan") + print(f"Resumed {resumed} workers after database scan") _workers_paused_by_scan = set() def _run_db_update_task(full_refresh, server_type): @@ -23515,7 +23515,7 @@ def set_wishlist_cycle(): """, (cycle,)) conn.commit() - print(f"✅ Wishlist cycle set to: {cycle}") + print(f"Wishlist cycle set to: {cycle}") return jsonify({"success": True, "cycle": cycle}) except Exception as e: @@ -23602,7 +23602,7 @@ def set_discovery_lookback_period(): conn.commit() - print(f"✅ Discovery lookback period set to: {period}") + print(f"Discovery lookback period set to: {period}") return jsonify({"success": True, "period": period}) except Exception as e: @@ -23682,9 +23682,9 @@ def get_wishlist_tracks(): db = MusicDatabase() duplicates_removed = db.remove_wishlist_duplicates(profile_id=get_current_profile_id()) if duplicates_removed > 0: - print(f"🧹 Cleaned {duplicates_removed} duplicate tracks from wishlist") + print(f"Cleaned {duplicates_removed} duplicate tracks from wishlist") else: - print(f"⏸️ Skipping wishlist duplicate cleanup - download in progress") + print(f"Skipping wishlist duplicate cleanup - download in progress") wishlist_service = get_wishlist_service() raw_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=get_current_profile_id()) @@ -23707,7 +23707,7 @@ def get_wishlist_tracks(): seen_track_ids_sanitation.add(spotify_track_id) if duplicates_found > 0: - print(f"⚠️ [API-Wishlist-Tracks] Found and removed {duplicates_found} duplicate tracks during sanitization") + print(f"[API-Wishlist-Tracks] Found and removed {duplicates_found} duplicate tracks during sanitization") # FILTER by category if specified if category: @@ -23736,7 +23736,7 @@ def get_wishlist_tracks(): # Count total in category (quick scan — no heavy processing, just classification) total_in_category = sum(1 for t in sanitized_tracks if _classify_wishlist_track(t) == category) - print(f"📊 Wishlist filter: {len(filtered_tracks)}/{total_in_category} tracks in '{category}' category (limit: {limit or 'none'})") + print(f"Wishlist filter: {len(filtered_tracks)}/{total_in_category} tracks in '{category}' category (limit: {limit or 'none'})") return jsonify({"tracks": filtered_tracks, "category": category, "total": total_in_category}) # Apply limit to non-filtered results @@ -23776,16 +23776,16 @@ def start_wishlist_missing_downloads(): # CRITICAL: Clean duplicates BEFORE fetching tracks to prevent count mismatches # This prevents the "11 tracks shown but 12 counted" bug - print("🧹 [Manual-Wishlist] Cleaning duplicate tracks before download...") + print("[Manual-Wishlist] Cleaning duplicate tracks before download...") db = MusicDatabase() manual_profile_id = get_current_profile_id() duplicates_removed = db.remove_wishlist_duplicates(profile_id=manual_profile_id) if duplicates_removed > 0: - print(f"🧹 [Manual-Wishlist] Removed {duplicates_removed} duplicate tracks") + print(f"[Manual-Wishlist] Removed {duplicates_removed} duplicate tracks") # CLEANUP: Remove tracks from wishlist that already exist in library # This prevents wasting bandwidth on tracks we already have - print("🧼 [Manual-Wishlist] Checking wishlist against library for already-owned tracks...") + print("[Manual-Wishlist] Checking wishlist against library for already-owned tracks...") cleanup_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=manual_profile_id) cleanup_removed = 0 @@ -23832,12 +23832,12 @@ def start_wishlist_missing_downloads(): removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True) if removed: cleanup_removed += 1 - print(f"🧼 [Manual-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}") + print(f"[Manual-Wishlist] Removed already-owned track: '{track_name}' by {artist_name}") except Exception as remove_error: - print(f"⚠️ [Manual-Wishlist] Error removing track from wishlist: {remove_error}") + print(f"[Manual-Wishlist] Error removing track from wishlist: {remove_error}") if cleanup_removed > 0: - print(f"✅ [Manual-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist") + print(f"[Manual-Wishlist] Cleaned up {cleanup_removed} already-owned tracks from wishlist") # Get wishlist tracks formatted for download modal (after cleanup) raw_wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=manual_profile_id) @@ -23862,8 +23862,8 @@ def start_wishlist_missing_downloads(): seen_track_ids_sanitation.add(spotify_track_id) if duplicates_found > 0: - print(f"⚠️ [Manual-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization") - print(f"🔧 [Manual-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service") + print(f"[Manual-Wishlist] Found and removed {duplicates_found} duplicate tracks during sanitization") + print(f"[Manual-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service") # FILTER BY TRACK IDs if specified (prioritized - prevents race conditions) if track_ids: @@ -23887,7 +23887,7 @@ def start_wishlist_missing_downloads(): seen_track_ids.add(tid) wishlist_tracks = filtered_tracks - print(f"🎯 [Manual-Wishlist] Filtered to {len(wishlist_tracks)} specific tracks by ID (preserving frontend display order)") + print(f"[Manual-Wishlist] Filtered to {len(wishlist_tracks)} specific tracks by ID (preserving frontend display order)") # FILTER BY CATEGORY if specified and no track_ids (backward compatibility) elif category: @@ -23938,14 +23938,14 @@ def start_wishlist_missing_downloads(): filtered_tracks.append(track) wishlist_tracks = filtered_tracks - print(f"🔍 [Manual-Wishlist] Filtered to {len(wishlist_tracks)} tracks for category: {category}") + print(f"[Manual-Wishlist] Filtered to {len(wishlist_tracks)} tracks for category: {category}") # Stamp original index on each track so task indices match frontend row order for i, track in enumerate(wishlist_tracks): track['_original_index'] = i # Add activity for wishlist download start - add_activity_item("📥", "Wishlist Download Started", f"{len(wishlist_tracks)} tracks", "Now") + add_activity_item("", "Wishlist Download Started", f"{len(wishlist_tracks)} tracks", "Now") batch_id = str(uuid.uuid4()) @@ -24016,7 +24016,7 @@ def cleanup_wishlist(): db = MusicDatabase() active_server = config_manager.get_active_media_server() - print("📋 [Wishlist Cleanup] Starting wishlist cleanup process...") + print("[Wishlist Cleanup] Starting wishlist cleanup process...") # Get wishlist tracks for current profile cleanup_profile_id = get_current_profile_id() @@ -24024,7 +24024,7 @@ def cleanup_wishlist(): if not wishlist_tracks: return jsonify({"success": True, "message": "No tracks in wishlist to clean up", "removed_count": 0}) - print(f"📋 [Wishlist Cleanup] Found {len(wishlist_tracks)} tracks in wishlist") + print(f"[Wishlist Cleanup] Found {len(wishlist_tracks)} tracks in wishlist") removed_count = 0 processed_count = 0 @@ -24040,7 +24040,7 @@ def cleanup_wishlist(): if not track_name or not artists or not spotify_track_id: continue - print(f"📋 [Wishlist Cleanup] Checking track {processed_count}/{len(wishlist_tracks)}: '{track_name}'") + print(f"[Wishlist Cleanup] Checking track {processed_count}/{len(wishlist_tracks)}: '{track_name}'") # Check each artist found_in_db = False @@ -24063,11 +24063,11 @@ def cleanup_wishlist(): if db_track and confidence >= 0.7: found_in_db = True - print(f"📋 [Wishlist Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})") + print(f"[Wishlist Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})") break except Exception as db_error: - print(f"⚠️ [Wishlist Cleanup] Error checking database for track '{track_name}': {db_error}") + print(f"[Wishlist Cleanup] Error checking database for track '{track_name}': {db_error}") continue # If found in database, remove from wishlist @@ -24076,13 +24076,13 @@ def cleanup_wishlist(): removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True) if removed: removed_count += 1 - print(f"✅ [Wishlist Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})") + print(f"[Wishlist Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})") else: - print(f"⚠️ [Wishlist Cleanup] Failed to remove track from wishlist: '{track_name}' ({spotify_track_id})") + print(f"[Wishlist Cleanup] Failed to remove track from wishlist: '{track_name}' ({spotify_track_id})") except Exception as remove_error: - print(f"❌ [Wishlist Cleanup] Error removing track from wishlist: {remove_error}") + print(f"[Wishlist Cleanup] Error removing track from wishlist: {remove_error}") - print(f"📋 [Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist") + print(f"[Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist") return jsonify({ "success": True, @@ -24304,20 +24304,20 @@ def add_album_track_to_wishlist(): ) if success: - print(f"✅ Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist") + print(f"Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist") return jsonify({ "success": True, "message": f"Added '{track.get('name')}' to wishlist" }) else: - print(f"❌ Failed to add track '{track.get('name')}' to wishlist") + print(f"Failed to add track '{track.get('name')}' to wishlist") return jsonify({ "success": False, "error": "Failed to add track to wishlist" }) except Exception as e: - print(f"❌ Error adding track to wishlist: {e}") + print(f"Error adding track to wishlist: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -24343,7 +24343,7 @@ def start_database_update(): # Add activity for database update start update_type = "Full" if full_refresh else "Incremental" server_name = active_server.capitalize() - add_activity_item("🗄️", "Database Update", f"Starting {update_type.lower()} update from {server_name}...", "Now") + add_activity_item("", "Database Update", f"Starting {update_type.lower()} update from {server_name}...", "Now") # Submit the worker function to the executor db_update_executor.submit(_run_db_update_task, full_refresh, active_server) @@ -24356,7 +24356,7 @@ def get_database_update_status(): with db_update_lock: # Debug: Log current state occasionally if db_update_state["status"] == "running": - print(f"📊 [Status Check] {db_update_state['processed']}/{db_update_state['total']} ({db_update_state['progress']:.1f}%) - {db_update_state['phase']}") + print(f"[Status Check] {db_update_state['processed']}/{db_update_state['total']} ({db_update_state['progress']:.1f}%) - {db_update_state['phase']}") return jsonify(db_update_state) @app.route('/api/database/update/stop', methods=['POST']) @@ -25087,7 +25087,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): quality_scanner_state["results"] = [] quality_scanner_state["error_message"] = "" - print(f"🔍 [Quality Scanner] Starting scan with scope: {scope}") + print(f"[Quality Scanner] Starting scan with scope: {scope}") # Get database instance db = MusicDatabase() @@ -25112,7 +25112,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): tier_num = QUALITY_TIERS[tier_name]['tier'] min_acceptable_tier = min(min_acceptable_tier, tier_num) - print(f"🎵 [Quality Scanner] Minimum acceptable tier: {min_acceptable_tier}") + print(f"[Quality Scanner] Minimum acceptable tier: {min_acceptable_tier}") # Get tracks to scan based on scope with quality_scanner_lock: @@ -25126,12 +25126,12 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): quality_scanner_state["status"] = "finished" quality_scanner_state["phase"] = "No watchlist artists found" quality_scanner_state["error_message"] = "Please add artists to watchlist first" - print(f"⚠️ [Quality Scanner] No watchlist artists found") + print(f"[Quality Scanner] No watchlist artists found") return # Get artist names from watchlist artist_names = [artist.artist_name for artist in watchlist_artists] - print(f"📋 [Quality Scanner] Scanning {len(artist_names)} watchlist artists") + print(f"[Quality Scanner] Scanning {len(artist_names)} watchlist artists") # Get all tracks for these artists by name conn = db._get_connection() @@ -25161,7 +25161,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): conn.close() total_tracks = len(tracks_to_scan) - print(f"📊 [Quality Scanner] Found {total_tracks} tracks to scan") + print(f"[Quality Scanner] Found {total_tracks} tracks to scan") with quality_scanner_lock: quality_scanner_state["total"] = total_tracks @@ -25173,7 +25173,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): quality_scanner_state["status"] = "error" quality_scanner_state["phase"] = "Spotify not authenticated" quality_scanner_state["error_message"] = "Please authenticate with Spotify first" - print(f"❌ [Quality Scanner] Spotify not authenticated") + print(f"[Quality Scanner] Spotify not authenticated") return wishlist_service = get_wishlist_service() @@ -25182,7 +25182,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): for idx, track_row in enumerate(tracks_to_scan, 1): # Check for stop request if quality_scanner_state.get('status') != 'running': - print(f"🛑 [Quality Scanner] Stop requested, halting at track {idx}/{total_tracks}") + print(f"[Quality Scanner] Stop requested, halting at track {idx}/{total_tracks}") break try: @@ -25208,7 +25208,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): with quality_scanner_lock: quality_scanner_state["low_quality"] += 1 - print(f"🔍 [Quality Scanner] Low quality: {artist_name} - {title} ({tier_name}, {file_path})") + print(f"[Quality Scanner] Low quality: {artist_name} - {title} ({tier_name}, {file_path})") # Attempt to match to Spotify using matching_engine matched = False @@ -25223,7 +25223,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): })() search_queries = matching_engine.generate_download_queries(temp_track) - print(f"🔍 [Quality Scanner] Generated {len(search_queries)} search queries for {artist_name} - {title}") + print(f"[Quality Scanner] Generated {len(search_queries)} search queries for {artist_name} - {title}") # Find best match using confidence scoring best_match = None @@ -25267,31 +25267,31 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): elif _at == 'ep': combined_confidence += 0.01 - print(f"🔍 [Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})") + print(f"[Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})") # Update best match if this is better if combined_confidence > best_confidence and combined_confidence >= min_confidence: best_confidence = combined_confidence best_match = spotify_track - print(f"✅ [Quality Scanner] New best match: {spotify_track.artists[0]} - {spotify_track.name} (confidence: {combined_confidence:.3f})") + print(f"[Quality Scanner] New best match: {spotify_track.artists[0]} - {spotify_track.name} (confidence: {combined_confidence:.3f})") except Exception as e: - print(f"❌ [Quality Scanner] Error scoring result: {e}") + print(f"[Quality Scanner] Error scoring result: {e}") continue # If we found a very high confidence match, stop searching if best_confidence >= 0.9: - print(f"🎯 [Quality Scanner] High confidence match found ({best_confidence:.3f}), stopping search") + print(f"[Quality Scanner] High confidence match found ({best_confidence:.3f}), stopping search") break except Exception as e: - print(f"❌ [Quality Scanner] Error searching with query '{search_query}': {e}") + print(f"[Quality Scanner] Error searching with query '{search_query}': {e}") continue # Process best match if best_match: matched = True - print(f"✅ [Quality Scanner] Final match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})") + print(f"[Quality Scanner] Final match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})") # Build full Spotify track data for wishlist matched_track_data = { @@ -25331,14 +25331,14 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): if success: with quality_scanner_lock: quality_scanner_state["matched"] += 1 - print(f"✅ [Quality Scanner] Matched and added to wishlist: {artist_name} - {title}") + print(f"[Quality Scanner] Matched and added to wishlist: {artist_name} - {title}") else: - print(f"⚠️ [Quality Scanner] Failed to add to wishlist: {artist_name} - {title}") + print(f"[Quality Scanner] Failed to add to wishlist: {artist_name} - {title}") else: - print(f"⚠️ [Quality Scanner] No suitable match found (best confidence: {best_confidence:.3f}, required: {min_confidence:.3f})") + print(f"[Quality Scanner] No suitable match found (best confidence: {best_confidence:.3f}, required: {min_confidence:.3f})") except Exception as matching_error: - print(f"❌ [Quality Scanner] Matching error for {artist_name} - {title}: {matching_error}") + print(f"[Quality Scanner] Matching error for {artist_name} - {title}: {matching_error}") # Store result result_entry = { @@ -25357,10 +25357,10 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): quality_scanner_state["results"].append(result_entry) if not matched: - print(f"⚠️ [Quality Scanner] No Spotify match found for: {artist_name} - {title}") + print(f"[Quality Scanner] No Spotify match found for: {artist_name} - {title}") except Exception as track_error: - print(f"❌ [Quality Scanner] Error processing track: {track_error}") + print(f"[Quality Scanner] Error processing track: {track_error}") continue # Scan complete (don't overwrite if already stopped by user) @@ -25371,11 +25371,11 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): if not was_stopped: quality_scanner_state["phase"] = "Scan complete" - print(f"✅ [Quality Scanner] Scan {'stopped' if was_stopped else 'complete'}: {quality_scanner_state['processed']} processed, " + print(f"[Quality Scanner] Scan {'stopped' if was_stopped else 'complete'}: {quality_scanner_state['processed']} processed, " f"{quality_scanner_state['low_quality']} low quality, {quality_scanner_state['matched']} matched to Spotify") # Add activity - add_activity_item("🔍", "Quality Scan Complete", + add_activity_item("", "Quality Scan Complete", f"{quality_scanner_state['matched']} tracks added to wishlist", "Now") try: @@ -25389,7 +25389,7 @@ def _run_quality_scanner(scope='watchlist', profile_id=1): pass except Exception as e: - print(f"❌ [Quality Scanner] Critical error: {e}") + print(f"[Quality Scanner] Critical error: {e}") import traceback traceback.print_exc() @@ -25418,7 +25418,7 @@ def _run_duplicate_cleaner(): duplicate_cleaner_state["space_freed"] = 0 duplicate_cleaner_state["error_message"] = "" - print(f"🧹 [Duplicate Cleaner] Starting duplicate scan...") + print(f"[Duplicate Cleaner] Starting duplicate scan...") # Get Transfer folder path from config transfer_folder = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer')) @@ -25427,13 +25427,13 @@ def _run_duplicate_cleaner(): duplicate_cleaner_state["status"] = "error" duplicate_cleaner_state["phase"] = "Transfer folder not configured or does not exist" duplicate_cleaner_state["error_message"] = "Please configure Transfer folder in settings" - print(f"❌ [Duplicate Cleaner] Transfer folder not found: {transfer_folder}") + print(f"[Duplicate Cleaner] Transfer folder not found: {transfer_folder}") return # Create deleted folder if it doesn't exist deleted_folder = os.path.join(transfer_folder, 'deleted') os.makedirs(deleted_folder, exist_ok=True) - print(f"📁 [Duplicate Cleaner] Deleted folder: {deleted_folder}") + print(f"[Duplicate Cleaner] Deleted folder: {deleted_folder}") # Phase 1: Count total files for progress tracking with duplicate_cleaner_lock: @@ -25446,7 +25446,7 @@ def _run_duplicate_cleaner(): dirs.remove('deleted') total_files += len(files) - print(f"📊 [Duplicate Cleaner] Found {total_files} total files to scan") + print(f"[Duplicate Cleaner] Found {total_files} total files to scan") with duplicate_cleaner_lock: duplicate_cleaner_state["total_files"] = total_files @@ -25513,7 +25513,7 @@ def _run_duplicate_cleaner(): continue duplicates_found += len(file_versions) - 1 # Count all but the one we keep - print(f"🔍 [Duplicate Cleaner] Found {len(file_versions)} versions of '{filename}' in {directory}") + print(f"[Duplicate Cleaner] Found {len(file_versions)} versions of '{filename}' in {directory}") # Sort by priority: best format first, then largest size def sort_key(f): @@ -25525,7 +25525,7 @@ def _run_duplicate_cleaner(): # Keep the first one (best quality), delete the rest best_version = sorted_versions[0] - print(f"✅ [Duplicate Cleaner] Keeping: {os.path.basename(best_version['full_path'])} " + print(f"[Duplicate Cleaner] Keeping: {os.path.basename(best_version['full_path'])} " f"({best_version['extension']}, {best_version['size']} bytes)") for duplicate_file in sorted_versions[1:]: @@ -25544,7 +25544,7 @@ def _run_duplicate_cleaner(): deleted_count += 1 space_freed += duplicate_file['size'] - print(f"🗑️ [Duplicate Cleaner] Moved to deleted: {os.path.basename(duplicate_file['full_path'])} " + print(f"[Duplicate Cleaner] Moved to deleted: {os.path.basename(duplicate_file['full_path'])} " f"({duplicate_file['extension']}, {duplicate_file['size']} bytes)") # Update stats @@ -25554,7 +25554,7 @@ def _run_duplicate_cleaner(): duplicate_cleaner_state["duplicates_found"] = duplicates_found except Exception as e: - print(f"❌ [Duplicate Cleaner] Error moving file {duplicate_file['full_path']}: {e}") + print(f"[Duplicate Cleaner] Error moving file {duplicate_file['full_path']}: {e}") continue # Scan complete @@ -25564,12 +25564,12 @@ def _run_duplicate_cleaner(): duplicate_cleaner_state["phase"] = "Cleaning complete" space_mb = space_freed / (1024 * 1024) - print(f"✅ [Duplicate Cleaner] Scan complete: {files_scanned} files scanned, " + print(f"[Duplicate Cleaner] Scan complete: {files_scanned} files scanned, " f"{duplicates_found} duplicates found, {deleted_count} files moved to deleted folder, " f"{space_mb:.2f} MB freed") # Add activity - add_activity_item("🧹", "Duplicate Cleaner Complete", + add_activity_item("", "Duplicate Cleaner Complete", f"{deleted_count} files removed, {space_mb:.1f} MB freed", "Now") try: @@ -25583,7 +25583,7 @@ def _run_duplicate_cleaner(): pass except Exception as e: - print(f"❌ [Duplicate Cleaner] Critical error: {e}") + print(f"[Duplicate Cleaner] Critical error: {e}") import traceback traceback.print_exc() @@ -25602,7 +25602,7 @@ def start_quality_scan(): data = request.get_json() or {} scope = data.get('scope', 'watchlist') # 'watchlist' or 'all' - print(f"🔍 [Quality Scanner API] Starting scan with scope: {scope}") + print(f"[Quality Scanner API] Starting scan with scope: {scope}") # Reset state quality_scanner_state["status"] = "running" @@ -25620,7 +25620,7 @@ def start_quality_scan(): scan_profile_id = get_current_profile_id() quality_scanner_executor.submit(_run_quality_scanner, scope, scan_profile_id) - add_activity_item("🔍", "Quality Scan Started", f"Scanning {scope} tracks", "Now") + add_activity_item("", "Quality Scan Started", f"Scanning {scope} tracks", "Now") return jsonify({"success": True, "message": "Quality scan started"}) @@ -25648,7 +25648,7 @@ def start_duplicate_cleaner(): if duplicate_cleaner_state["status"] == "running": return jsonify({"success": False, "error": "A scan is already in progress"}), 409 - print(f"🧹 [Duplicate Cleaner API] Starting duplicate cleaner...") + print(f"[Duplicate Cleaner API] Starting duplicate cleaner...") # Reset state duplicate_cleaner_state["status"] = "running" @@ -25664,7 +25664,7 @@ def start_duplicate_cleaner(): # Submit worker duplicate_cleaner_executor.submit(_run_duplicate_cleaner) - add_activity_item("🧹", "Duplicate Cleaner Started", "Scanning Transfer folder", "Now") + add_activity_item("", "Duplicate Cleaner Started", "Scanning Transfer folder", "Now") return jsonify({"success": True, "message": "Duplicate cleaner started"}) @@ -25739,7 +25739,7 @@ def search_retag_albums(): }) return jsonify({"success": True, "albums": albums}) except Exception as e: - print(f"❌ [Retag] Album search error: {e}") + print(f"[Retag] Album search error: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/retag/execute', methods=['POST']) @@ -25896,15 +25896,15 @@ def get_valid_candidates(results, spotify_track, query): # Sort by confidence (best match first) scored.sort(key=lambda x: x.confidence, reverse=True) best = scored[0] - print(f"🎵 [{source_label}] {len(scored)}/{len(results)} candidates passed validation " + print(f"[{source_label}] {len(scored)}/{len(results)} candidates passed validation " f"(best: {best.confidence:.2f} '{best.artist} - {best.title}')") return scored else: if results[0].username == 'youtube': - print(f"⚠️ [{source_label}] No streaming results passed validation — falling through to filename matching") + print(f"[{source_label}] No streaming results passed validation — falling through to filename matching") # YouTube artist data is unreliable, allow fallback to filename-based matching else: - print(f"⚠️ [{source_label}] No streaming results passed validation (threshold: 0.60, artist gate: 0.40) — rejecting all candidates") + print(f"[{source_label}] No streaming results passed validation (threshold: 0.60, artist gate: 0.40) — rejecting all candidates") return [] # Tidal/Qobuz/HiFi/Deezer have structured metadata; don't fall back to filename matching # Uses the existing, powerful matching engine for scoring (Soulseek P2P results) @@ -25918,7 +25918,7 @@ def get_valid_candidates(results, spotify_track, query): if is_streaming_source: source_label = initial_candidates[0].username.title() - print(f"🎵 [{source_label}] Skipping quality filter - streaming source handles quality internally") + print(f"[{source_label}] Skipping quality filter - streaming source handles quality internally") quality_filtered_candidates = initial_candidates else: # Filter by user's quality profile before artist verification (Soulseek only) @@ -25930,7 +25930,7 @@ def get_valid_candidates(results, spotify_track, query): # and no results match, we should fail the download rather than force a fallback. # The quality filter already has its own fallback logic controlled by the user's settings. if not quality_filtered_candidates: - print(f"⚠️ [Quality Filter] No candidates match quality profile - download will fail per user preferences") + print(f"[Quality Filter] No candidates match quality profile - download will fail per user preferences") return [] verified_candidates = [] @@ -25989,18 +25989,18 @@ def _recover_worker_slot(batch_id, task_id): This prevents permanent worker slot leaks that cause modal to show wrong worker counts. """ try: - print(f"🚨 [Worker Recovery] Attempting to recover worker slot for batch {batch_id}, task {task_id}") + print(f"[Worker Recovery] Attempting to recover worker slot for batch {batch_id}, task {task_id}") # Acquire lock with timeout to prevent deadlock lock_acquired = tasks_lock.acquire(timeout=3.0) if not lock_acquired: - print(f"💀 [Worker Recovery] FATAL: Could not acquire lock for recovery - worker slot LEAKED") + print(f"[Worker Recovery] FATAL: Could not acquire lock for recovery - worker slot LEAKED") return False try: # Verify batch still exists if batch_id not in download_batches: - print(f"⚠️ [Worker Recovery] Batch {batch_id} not found - nothing to recover") + print(f"[Worker Recovery] Batch {batch_id} not found - nothing to recover") return True batch = download_batches[batch_id] @@ -26010,11 +26010,11 @@ def _recover_worker_slot(batch_id, task_id): if old_active > 0: batch['active_count'] -= 1 new_active = batch['active_count'] - print(f"✅ [Worker Recovery] Recovered worker slot - Active count: {old_active} → {new_active}") + print(f"[Worker Recovery] Recovered worker slot - Active count: {old_active} → {new_active}") # Try to start next worker if queue isn't empty if batch['queue_index'] < len(batch['queue']) and new_active < batch['max_concurrent']: - print(f"🔄 [Worker Recovery] Attempting to start replacement worker") + print(f"[Worker Recovery] Attempting to start replacement worker") # Release lock temporarily to avoid deadlock in _start_next_batch_of_downloads tasks_lock.release() try: @@ -26025,14 +26025,14 @@ def _recover_worker_slot(batch_id, task_id): return True else: - print(f"⚠️ [Worker Recovery] Active count already 0 - no recovery needed") + print(f"[Worker Recovery] Active count already 0 - no recovery needed") return True finally: tasks_lock.release() except Exception as recovery_error: - print(f"💀 [Worker Recovery] FATAL ERROR in recovery: {recovery_error}") + print(f"[Worker Recovery] FATAL ERROR in recovery: {recovery_error}") return False def _get_batch_lock(batch_id): @@ -26051,7 +26051,7 @@ def _start_next_batch_of_downloads(batch_id): with batch_lock: # Prevent starting new tasks if shutting down if IS_SHUTTING_DOWN: - print(f"🛑 [Batch Manager] Server shutting down - skipping new tasks for batch {batch_id}") + print(f"[Batch Manager] Server shutting down - skipping new tasks for batch {batch_id}") return with tasks_lock: @@ -26064,7 +26064,7 @@ def _start_next_batch_of_downloads(batch_id): queue_index = batch['queue_index'] active_count = batch['active_count'] - print(f"🔍 [Batch Lock] Starting workers for {batch_id}: active={active_count}, max={max_concurrent}, queue_pos={queue_index}/{len(queue)}") + print(f"[Batch Lock] Starting workers for {batch_id}: active={active_count}, max={max_concurrent}, queue_pos={queue_index}/{len(queue)}") # Start downloads up to the concurrent limit while active_count < max_concurrent and queue_index < len(queue): @@ -26074,7 +26074,7 @@ def _start_next_batch_of_downloads(batch_id): if task_id in download_tasks: current_status = download_tasks[task_id]['status'] if current_status == 'cancelled': - print(f"⏭️ [Batch Lock] Skipping cancelled task {task_id} (queue position {queue_index + 1})") + print(f"[Batch Lock] Skipping cancelled task {task_id} (queue position {queue_index + 1})") download_batches[batch_id]['queue_index'] += 1 queue_index += 1 continue # Skip to next task without consuming worker slot @@ -26083,9 +26083,9 @@ def _start_next_batch_of_downloads(batch_id): # Must be done INSIDE the lock to prevent race conditions with status polling download_tasks[task_id]['status'] = 'searching' download_tasks[task_id]['status_change_time'] = time.time() - print(f"🔧 [Batch Manager] Set task {task_id} status to 'searching'") + print(f"[Batch Manager] Set task {task_id} status to 'searching'") else: - print(f"⚠️ [Batch Lock] Task {task_id} not found in download_tasks - skipping") + print(f"[Batch Lock] Task {task_id} not found in download_tasks - skipping") download_batches[batch_id]['queue_index'] += 1 queue_index += 1 continue @@ -26099,26 +26099,26 @@ def _start_next_batch_of_downloads(batch_id): download_batches[batch_id]['active_count'] += 1 download_batches[batch_id]['queue_index'] += 1 - print(f"🔄 [Batch Lock] Started download {queue_index + 1}/{len(queue)} - Active: {active_count + 1}/{max_concurrent}") + print(f"[Batch Lock] Started download {queue_index + 1}/{len(queue)} - Active: {active_count + 1}/{max_concurrent}") # Update local counters for next iteration active_count += 1 queue_index += 1 except Exception as submit_error: - print(f"❌ [Batch Lock] CRITICAL: Failed to submit task {task_id} to executor: {submit_error}") - print(f"🚨 [Batch Lock] Worker slot NOT consumed - preventing ghost worker") + print(f"[Batch Lock] CRITICAL: Failed to submit task {task_id} to executor: {submit_error}") + print(f"[Batch Lock] Worker slot NOT consumed - preventing ghost worker") # Reset task status since worker never started if task_id in download_tasks: download_tasks[task_id]['status'] = 'failed' - print(f"🔧 [Batch Lock] Set task {task_id} status to 'failed' due to submit failure") + print(f"[Batch Lock] Set task {task_id} status to 'failed' due to submit failure") # Don't increment counters - no worker was actually started # This prevents the "ghost worker" issue where active_count is incremented but no actual worker runs break # Stop trying to start more workers if executor is failing - print(f"✅ [Batch Lock] Finished starting workers for {batch_id}: final_active={download_batches[batch_id]['active_count']}, max={max_concurrent}") + print(f"[Batch Lock] Finished starting workers for {batch_id}: final_active={download_batches[batch_id]['active_count']}, max={max_concurrent}") def _get_track_artist_name(track_info): """Extract artist name from track info, handling different data formats (replicating sync.py)""" @@ -26224,11 +26224,11 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): from core.wishlist_service import get_wishlist_service from datetime import datetime - print(f"🔍 [Wishlist Processing] Starting wishlist processing for batch {batch_id}") + print(f"[Wishlist Processing] Starting wishlist processing for batch {batch_id}") with tasks_lock: if batch_id not in download_batches: - print(f"⚠️ [Wishlist Processing] Batch {batch_id} not found") + print(f"[Wishlist Processing] Batch {batch_id} not found") return {'tracks_added': 0, 'errors': 0} batch = download_batches[batch_id] @@ -26236,13 +26236,13 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): # Wing It mode — skip wishlist entirely for failed tracks if batch.get('wing_it'): failed_count = len(batch.get('permanently_failed_tracks', [])) - print(f"⚡ [Wing It] Skipping wishlist for {failed_count} failed tracks (wing it mode)") + print(f"[Wing It] Skipping wishlist for {failed_count} failed tracks (wing it mode)") return {'tracks_added': 0, 'errors': 0} permanently_failed_tracks = batch.get('permanently_failed_tracks', []) cancelled_tracks = batch.get('cancelled_tracks', set()) # STEP 0: Remove completed tracks from wishlist (THIS WAS MISSING!) - print(f"🔍 [Wishlist Processing] Checking completed tracks for wishlist removal") + print(f"[Wishlist Processing] Checking completed tracks for wishlist removal") for task_id in batch.get('queue', []): if task_id in download_tasks: task = download_tasks[task_id] @@ -26252,12 +26252,12 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): context = {'track_info': track_info, 'original_search_result': track_info} _check_and_remove_from_wishlist(context) except Exception as e: - print(f"⚠️ [Wishlist Processing] Error removing completed track from wishlist: {e}") + print(f"[Wishlist Processing] Error removing completed track from wishlist: {e}") # STEP 1: Add cancelled tracks that were missing to permanently_failed_tracks (replicating sync.py) # This matches sync.py's logic for adding cancelled missing tracks to the failed list if cancelled_tracks: - print(f"🔍 [Wishlist Processing] Processing {len(cancelled_tracks)} cancelled tracks") + print(f"[Wishlist Processing] Processing {len(cancelled_tracks)} cancelled tracks") # Process cancelled tracks with safeguard to prevent infinite loops processed_count = 0 @@ -26291,9 +26291,9 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): if not any(t.get('table_index') == track_index for t in permanently_failed_tracks): permanently_failed_tracks.append(cancelled_track_info) processed_count += 1 - print(f"🚫 [Wishlist Processing] Added cancelled missing track {cancelled_track_info['track_name']} to failed list for wishlist") + print(f"[Wishlist Processing] Added cancelled missing track {cancelled_track_info['track_name']} to failed list for wishlist") - print(f"🔍 [Wishlist Processing] Processed {processed_count} cancelled tracks") + print(f"[Wishlist Processing] Processed {processed_count} cancelled tracks") # STEP 1.5: Recover any failed/not_found tasks not captured in permanently_failed_tracks. # Stuck detection (in _on_download_completed, _check_batch_completion_v2, and the Safety Valve) @@ -26319,14 +26319,14 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): 'candidates': task.get('cached_candidates', []) } permanently_failed_tracks.append(recovered_track_info) - print(f"📋 [Wishlist Processing] Recovered uncaptured failed track for wishlist: {recovered_track_info['track_name']}") + print(f"[Wishlist Processing] Recovered uncaptured failed track for wishlist: {recovered_track_info['track_name']}") # STEP 2: Add permanently failed tracks to wishlist (exact sync.py logic) failed_count = len(permanently_failed_tracks) wishlist_added_count = 0 error_count = 0 - print(f"🔍 [Wishlist Processing] Processing {failed_count} failed tracks for wishlist") + print(f"[Wishlist Processing] Processing {failed_count} failed tracks for wishlist") if permanently_failed_tracks: try: @@ -26345,7 +26345,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): for i, failed_track_info in enumerate(permanently_failed_tracks[:max_failed_tracks]): try: track_name = failed_track_info.get('track_name', f'Track {i+1}') - print(f"🔍 [Wishlist Processing] Adding track {i+1}/{max_failed_tracks}: {track_name}") + print(f"[Wishlist Processing] Adding track {i+1}/{max_failed_tracks}: {track_name}") success = wishlist_service.add_failed_track_from_modal( track_info=failed_track_info, @@ -26355,7 +26355,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): ) if success: wishlist_added_count += 1 - print(f"✅ [Wishlist Processing] Added {track_name} to wishlist") + print(f"[Wishlist Processing] Added {track_name} to wishlist") try: if automation_engine: automation_engine.emit('wishlist_item_added', { @@ -26366,17 +26366,17 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): except Exception: pass else: - print(f"⚠️ [Wishlist Processing] Failed to add {track_name} to wishlist") + print(f"[Wishlist Processing] Failed to add {track_name} to wishlist") except Exception as e: error_count += 1 - print(f"❌ [Wishlist Processing] Exception adding track to wishlist: {e}") + print(f"[Wishlist Processing] Exception adding track to wishlist: {e}") - print(f"✨ [Wishlist Processing] Added {wishlist_added_count}/{failed_count} failed tracks to wishlist (errors: {error_count})") + print(f"[Wishlist Processing] Added {wishlist_added_count}/{failed_count} failed tracks to wishlist (errors: {error_count})") except Exception as e: error_count = len(permanently_failed_tracks) - print(f"❌ [Wishlist Processing] Critical error adding failed tracks to wishlist: {e}") + print(f"[Wishlist Processing] Critical error adding failed tracks to wishlist: {e}") import traceback traceback.print_exc() else: @@ -26395,26 +26395,26 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): download_batches[batch_id]['wishlist_processing_complete'] = True # Phase already set to 'complete' in _on_download_completed - print(f"✅ [Wishlist Processing] Completed wishlist processing for batch {batch_id}") + print(f"[Wishlist Processing] Completed wishlist processing for batch {batch_id}") # Auto-cleanup: Clear completed downloads from slskd try: - logger.info(f"🧹 [Auto-Cleanup] Clearing completed downloads from slskd after batch {batch_id}") + logger.info(f"[Auto-Cleanup] Clearing completed downloads from slskd after batch {batch_id}") run_async(soulseek_client.clear_all_completed_downloads()) - logger.info(f"✅ [Auto-Cleanup] Completed downloads cleared from slskd") + logger.info(f"[Auto-Cleanup] Completed downloads cleared from slskd") except Exception as cleanup_error: - logger.warning(f"⚠️ [Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}") + logger.warning(f"[Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}") # Sweep empty directories left behind by this batch's downloads try: _sweep_empty_download_directories() except Exception as sweep_error: - logger.warning(f"⚠️ [Auto-Cleanup] Failed to sweep empty directories: {sweep_error}") + logger.warning(f"[Auto-Cleanup] Failed to sweep empty directories: {sweep_error}") return completion_summary except Exception as e: - print(f"❌ [Wishlist Processing] CRITICAL ERROR in wishlist processing: {e}") + print(f"[Wishlist Processing] CRITICAL ERROR in wishlist processing: {e}") import traceback traceback.print_exc() @@ -26432,7 +26432,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id): } download_batches[batch_id]['wishlist_processing_complete'] = True except Exception as lock_error: - print(f"❌ [Wishlist Processing] Failed to update batch after error: {lock_error}") + print(f"[Wishlist Processing] Failed to update batch after error: {lock_error}") return {'tracks_added': 0, 'errors': 1, 'total_failed': 0} @@ -26444,7 +26444,7 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id): global wishlist_auto_processing try: - print(f"🤖 [Auto-Wishlist] Processing completion for auto-initiated batch {batch_id}") + print(f"[Auto-Wishlist] Processing completion for auto-initiated batch {batch_id}") # Run standard wishlist processing completion_summary = _process_failed_tracks_to_wishlist_exact(batch_id) @@ -26452,11 +26452,11 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id): # Log auto-processing completion tracks_added = completion_summary.get('tracks_added', 0) total_failed = completion_summary.get('total_failed', 0) - print(f"🎉 [Auto-Wishlist] Background processing complete: {tracks_added} added to wishlist, {total_failed} failed") + print(f"[Auto-Wishlist] Background processing complete: {tracks_added} added to wishlist, {total_failed} failed") # Add activity for wishlist processing if tracks_added > 0: - add_activity_item("⭐", "Wishlist Updated", f"{tracks_added} failed tracks added to wishlist", "Now") + add_activity_item("", "Wishlist Updated", f"{tracks_added} failed tracks added to wishlist", "Now") # TOGGLE CYCLE: Switch to next category for next run try: @@ -26478,9 +26478,9 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id): """, (next_cycle,)) conn.commit() - print(f"🔄 [Auto-Wishlist] Cycle toggled after completion: {current_cycle} → {next_cycle}") + print(f"[Auto-Wishlist] Cycle toggled after completion: {current_cycle} → {next_cycle}") except Exception as cycle_error: - print(f"⚠️ [Auto-Wishlist] Error toggling cycle: {cycle_error}") + print(f"[Auto-Wishlist] Error toggling cycle: {cycle_error}") # Mark auto-processing as complete and reset timestamp with wishlist_timer_lock: @@ -26500,7 +26500,7 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id): return completion_summary except Exception as e: - print(f"❌ [Auto-Wishlist] Error in auto-completion processing: {e}") + print(f"[Auto-Wishlist] Error in auto-completion processing: {e}") import traceback traceback.print_exc() @@ -26515,7 +26515,7 @@ def _on_download_completed(batch_id, task_id, success=True): """Called when a download completes to start the next one in queue""" with tasks_lock: if batch_id not in download_batches: - print(f"⚠️ [Batch Manager] Batch {batch_id} not found for completed task {task_id}") + print(f"[Batch Manager] Batch {batch_id} not found for completed task {task_id}") return # Guard against double-calling: track which tasks have already been completed @@ -26528,7 +26528,7 @@ def _on_download_completed(batch_id, task_id, success=True): completed_tasks = download_batches[batch_id].setdefault('_completed_task_ids', set()) _is_duplicate_completion = task_id in completed_tasks if _is_duplicate_completion: - print(f"⚠️ [Batch Manager] Task {task_id} already completed — skipping decrement, still checking batch completion") + print(f"[Batch Manager] Task {task_id} already completed — skipping decrement, still checking batch completion") # Set terminal status so the monitor loop stops re-processing this task if task_id in download_tasks and download_tasks[task_id].get('status') in ('downloading', 'queued'): download_tasks[task_id]['status'] = 'completed' @@ -26561,16 +26561,16 @@ def _on_download_completed(batch_id, task_id, success=True): if task_status == 'cancelled': download_batches[batch_id]['cancelled_tracks'].add(task.get('track_index', 0)) - print(f"🚫 [Batch Manager] Added cancelled track to batch tracking: {track_info['track_name']}") - add_activity_item("🚫", "Download Cancelled", f"'{track_info['track_name']}'", "Now") + print(f"[Batch Manager] Added cancelled track to batch tracking: {track_info['track_name']}") + add_activity_item("", "Download Cancelled", f"'{track_info['track_name']}'", "Now") elif task_status in ('failed', 'not_found'): download_batches[batch_id]['permanently_failed_tracks'].append(track_info) if task_status == 'not_found': - print(f"🔇 [Batch Manager] Added not-found track to batch tracking: {track_info['track_name']}") - add_activity_item("🔇", "Not Found", f"'{track_info['track_name']}'", "Now") + print(f"[Batch Manager] Added not-found track to batch tracking: {track_info['track_name']}") + add_activity_item("", "Not Found", f"'{track_info['track_name']}'", "Now") else: - print(f"❌ [Batch Manager] Added failed track to batch tracking: {track_info['track_name']}") - add_activity_item("❌", "Download Failed", f"'{track_info['track_name']}'", "Now") + print(f"[Batch Manager] Added failed track to batch tracking: {track_info['track_name']}") + add_activity_item("", "Download Failed", f"'{track_info['track_name']}'", "Now") try: if automation_engine: @@ -26587,7 +26587,7 @@ def _on_download_completed(batch_id, task_id, success=True): try: task = download_tasks[task_id] track_info = task.get('track_info', {}) - print(f"📋 [Batch Manager] Successful download - checking wishlist removal for task {task_id}") + print(f"[Batch Manager] Successful download - checking wishlist removal for task {task_id}") # Add activity for successful download track_name = track_info.get('name', 'Unknown Track') @@ -26602,7 +26602,7 @@ def _on_download_completed(batch_id, task_id, success=True): else: artist_name = 'Unknown Artist' - add_activity_item("📥", "Download Complete", f"'{track_name}' by {artist_name}", "Now") + add_activity_item("", "Download Complete", f"'{track_name}' by {artist_name}", "Now") # Try to remove from wishlist using track info if track_info: @@ -26613,18 +26613,18 @@ def _on_download_completed(batch_id, task_id, success=True): } _check_and_remove_from_wishlist(context) except Exception as wishlist_error: - print(f"⚠️ [Batch Manager] Error checking wishlist removal for successful download: {wishlist_error}") + print(f"[Batch Manager] Error checking wishlist removal for successful download: {wishlist_error}") # Decrement active count old_active = download_batches[batch_id]['active_count'] download_batches[batch_id]['active_count'] -= 1 new_active = download_batches[batch_id]['active_count'] - print(f"🔄 [Batch Manager] Task {task_id} completed ({'success' if success else 'failed/cancelled'}). Active workers: {old_active} → {new_active}/{download_batches[batch_id]['max_concurrent']}") + print(f"[Batch Manager] Task {task_id} completed ({'success' if success else 'failed/cancelled'}). Active workers: {old_active} → {new_active}/{download_batches[batch_id]['max_concurrent']}") # ENHANCED: Always check batch completion after any task completes (including duplicate calls) # This ensures completion is detected even when mixing normal downloads with cancelled tasks - print(f"🔍 [Batch Manager] Checking batch completion after task {task_id} completed") + print(f"[Batch Manager] Checking batch completion after task {task_id} completed") # FIXED: Check if batch is truly complete (all tasks finished, not just workers freed) batch = download_batches[batch_id] @@ -26665,19 +26665,19 @@ def _on_download_completed(batch_id, task_id, success=True): finished_count += 1 else: # Task ID in queue but not in download_tasks - treat as completed to prevent blocking - print(f"⚠️ [Orphaned Task] Task {task_id} in queue but not in download_tasks - counting as finished") + print(f"[Orphaned Task] Task {task_id} in queue but not in download_tasks - counting as finished") finished_count += 1 all_tasks_truly_finished = finished_count >= len(queue) has_retrying_tasks = retrying_count > 0 if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks: - print(f"🎉 [Batch Manager] Batch {batch_id} truly complete - all {finished_count}/{len(queue)} tasks finished - processing failed tracks to wishlist") + print(f"[Batch Manager] Batch {batch_id} truly complete - all {finished_count}/{len(queue)} tasks finished - processing failed tracks to wishlist") elif all_tasks_started and no_active_workers and has_retrying_tasks: - print(f"🔄 [Batch Manager] Batch {batch_id}: all workers free but {retrying_count} tasks retrying - continuing monitoring") + print(f"[Batch Manager] Batch {batch_id}: all workers free but {retrying_count} tasks retrying - continuing monitoring") elif all_tasks_started and no_active_workers: # This used to incorrectly mark batch as complete! - print(f"📊 [Batch Manager] Batch {batch_id}: all workers free but only {finished_count}/{len(queue)} tasks finished - continuing monitoring") + print(f"[Batch Manager] Batch {batch_id}: all workers free but only {finished_count}/{len(queue)} tasks finished - continuing monitoring") if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks: @@ -26697,7 +26697,7 @@ def _on_download_completed(batch_id, task_id, success=True): playlist_name = batch.get('playlist_name', 'Unknown Playlist') failed_count = len(batch.get('permanently_failed_tracks', [])) successful_downloads = finished_count - failed_count - add_activity_item("✅", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now") + add_activity_item("", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now") # Emit batch_complete event for automation engine (only if something downloaded) if successful_downloads > 0: @@ -26718,30 +26718,30 @@ def _on_download_completed(batch_id, task_id, success=True): url_hash = playlist_id.replace('youtube_', '') if url_hash in youtube_playlist_states: youtube_playlist_states[url_hash]['phase'] = 'download_complete' - print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase") + print(f"Updated YouTube playlist {url_hash} to download_complete phase") # Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist if playlist_id and playlist_id.startswith('tidal_'): tidal_playlist_id = playlist_id.replace('tidal_', '') if tidal_playlist_id in tidal_discovery_states: tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete' - print(f"📋 Updated Tidal playlist {tidal_playlist_id} to download_complete phase") + print(f"Updated Tidal playlist {tidal_playlist_id} to download_complete phase") # Update Deezer playlist phase to 'download_complete' if this is a Deezer playlist if playlist_id and playlist_id.startswith('deezer_'): deezer_playlist_id = playlist_id.replace('deezer_', '') if deezer_playlist_id in deezer_discovery_states: deezer_discovery_states[deezer_playlist_id]['phase'] = 'download_complete' - print(f"📋 Updated Deezer playlist {deezer_playlist_id} to download_complete phase") + print(f"Updated Deezer playlist {deezer_playlist_id} to download_complete phase") # Update Spotify Public playlist phase to 'download_complete' if this is a Spotify Public playlist if playlist_id and playlist_id.startswith('spotify_public_'): spotify_public_url_hash = playlist_id.replace('spotify_public_', '') if spotify_public_url_hash in spotify_public_discovery_states: spotify_public_discovery_states[spotify_public_url_hash]['phase'] = 'download_complete' - print(f"📋 Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase") + print(f"Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase") - print(f"🎉 [Batch Manager] Batch {batch_id} complete - stopping monitor") + print(f"[Batch Manager] Batch {batch_id} complete - stopping monitor") download_monitor.stop_monitoring(batch_id) # REPAIR: Scan all album folders from this batch for track number issues @@ -26759,12 +26759,12 @@ def _on_download_completed(batch_id, task_id, success=True): # For manual batches, use standard wishlist processing missing_download_executor.submit(_process_failed_tracks_to_wishlist_exact, batch_id) else: - print(f"✅ [Batch Manager] Batch {batch_id} already marked complete - skipping duplicate processing") + print(f"[Batch Manager] Batch {batch_id} already marked complete - skipping duplicate processing") return # Don't start next batch if we're done # Start next downloads in queue - print(f"🔄 [Batch Manager] Starting next batch for {batch_id}") + print(f"[Batch Manager] Starting next batch for {batch_id}") _start_next_batch_of_downloads(batch_id) def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): @@ -26799,7 +26799,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): batch_artist_context = download_batches[batch_id].get('artist_context') if force_download_all: - print(f"🔄 [Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing") + print(f"[Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing") # PREFLIGHT: Pre-populate MusicBrainz release cache for album downloads. # This ensures ALL tracks in the album use the same release MBID during @@ -26824,12 +26824,12 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): # Also cache the full release detail for tag extraction with _mb_release_detail_cache_lock: _mb_release_detail_cache[release_mbid] = release - print(f"🏷️ [Preflight] Pre-cached MB release for '{album_name_pf}': " + print(f"[Preflight] Pre-cached MB release for '{album_name_pf}': " f"'{release.get('title', '')}' ({release_mbid[:8]}...)") else: - print(f"⚠️ [Preflight] No MB release found for '{album_name_pf}' — per-track lookup will be used") + print(f"[Preflight] No MB release found for '{album_name_pf}' — per-track lookup will be used") except Exception as pf_err: - print(f"⚠️ [Preflight] MB release preflight failed: {pf_err}") + print(f"[Preflight] MB release preflight failed: {pf_err}") # ALBUM FAST PATH: If this is an album download, try to find the album in the DB first # and match tracks within it — faster and more accurate than N global searches @@ -26850,11 +26850,11 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): db_album_tracks = db.get_tracks_by_album(db_album.id) for t in db_album_tracks: album_tracks_map[t.title.lower().strip()] = t - print(f"📀 [Album Analysis] Found album '{db_album.title}' in DB with {len(db_album_tracks)} tracks (confidence: {album_confidence:.2f})") + print(f"[Album Analysis] Found album '{db_album.title}' in DB with {len(db_album_tracks)} tracks (confidence: {album_confidence:.2f})") else: - print(f"📀 [Album Analysis] Album '{album_name}' not found in DB — falling back to per-track search") + print(f"[Album Analysis] Album '{album_name}' not found in DB — falling back to per-track search") except Exception as album_err: - print(f"⚠️ [Album Analysis] Album lookup error: {album_err} — falling back to per-track search") + print(f"[Album Analysis] Album lookup error: {album_err} — falling back to per-track search") for i, track_data in enumerate(tracks_json): # Use original table index if provided (for partial track selection), @@ -26866,7 +26866,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): # Skip database check if force download is enabled if force_download_all: - print(f"🔄 [Force Download] Skipping database check for '{track_name}' - treating as missing") + print(f"[Force Download] Skipping database check for '{track_name}' - treating as missing") found, confidence = False, 0.0 elif album_tracks_map: # Album-scoped matching: check against known album tracks first @@ -26924,7 +26924,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): try: _check_and_remove_track_from_wishlist_by_metadata(track_data) except Exception as wishlist_error: - print(f"⚠️ [Analysis] Error checking wishlist removal for found track: {wishlist_error}") + print(f"[Analysis] Error checking wishlist removal for found track: {wishlist_error}") with tasks_lock: if batch_id in download_batches: @@ -26940,7 +26940,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): missing_tracks = [res for res in missing_tracks if not _is_explicit_blocked(res.get('track', {}))] skipped = before_count - len(missing_tracks) if skipped > 0: - print(f"🚫 [Content Filter] Filtered out {skipped} explicit track(s) from download queue") + print(f"[Content Filter] Filtered out {skipped} explicit track(s) from download queue") with tasks_lock: if batch_id in download_batches: @@ -26948,7 +26948,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): # PHASE 2: TRANSITION TO DOWNLOAD (if necessary) if not missing_tracks: - print(f"✅ Analysis for batch {batch_id} complete. No missing tracks.") + print(f"Analysis for batch {batch_id} complete. No missing tracks.") # Record sync history — all tracks found, nothing to download tracks_found = sum(1 for r in analysis_results if r.get('found')) @@ -26999,32 +26999,32 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): url_hash = playlist_id.replace('youtube_', '') if url_hash in youtube_playlist_states: youtube_playlist_states[url_hash]['phase'] = 'download_complete' - print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase (no missing tracks)") + print(f"Updated YouTube playlist {url_hash} to download_complete phase (no missing tracks)") # Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist if playlist_id.startswith('tidal_'): tidal_playlist_id = playlist_id.replace('tidal_', '') if tidal_playlist_id in tidal_discovery_states: tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete' - print(f"📋 Updated Tidal playlist {tidal_playlist_id} to download_complete phase (no missing tracks)") + print(f"Updated Tidal playlist {tidal_playlist_id} to download_complete phase (no missing tracks)") # Update Deezer playlist phase to 'download_complete' if this is a Deezer playlist if playlist_id.startswith('deezer_'): deezer_playlist_id = playlist_id.replace('deezer_', '') if deezer_playlist_id in deezer_discovery_states: deezer_discovery_states[deezer_playlist_id]['phase'] = 'download_complete' - print(f"📋 Updated Deezer playlist {deezer_playlist_id} to download_complete phase (no missing tracks)") + print(f"Updated Deezer playlist {deezer_playlist_id} to download_complete phase (no missing tracks)") # Update Spotify Public playlist phase to 'download_complete' if this is a Spotify Public playlist if playlist_id.startswith('spotify_public_'): spotify_public_url_hash = playlist_id.replace('spotify_public_', '') if spotify_public_url_hash in spotify_public_discovery_states: spotify_public_discovery_states[spotify_public_url_hash]['phase'] = 'download_complete' - print(f"📋 Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase (no missing tracks)") + print(f"Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase (no missing tracks)") # Handle auto-initiated wishlist completion even when no missing tracks if is_auto_batch and playlist_id == 'wishlist': - print("🤖 [Auto-Wishlist] No missing tracks found - calling auto-completion handler to toggle cycle and reschedule") + print("[Auto-Wishlist] No missing tracks found - calling auto-completion handler to toggle cycle and reschedule") missing_download_executor.submit(_process_failed_tracks_to_wishlist_exact_with_auto_completion, batch_id) return @@ -27058,7 +27058,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): try: _sr = source_reuse_logger _sr.info(f"[Album Pre-flight] Searching for '{artist_name} {album_name}'") - print(f"🔎 [Album Pre-flight] Searching Soulseek for complete album: '{artist_name} - {album_name}'") + print(f"[Album Pre-flight] Searching Soulseek for complete album: '{artist_name} - {album_name}'") slsk = soulseek_client.soulseek if hasattr(soulseek_client, 'soulseek') else soulseek_client @@ -27097,7 +27097,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): _sr.info(f"[Album Pre-flight] Best album result: {best_album.username}:{best_album.album_path} " f"({best_album.track_count} tracks, quality={best_album.dominant_quality})") - print(f"📀 [Album Pre-flight] Found album folder: {best_album.username} — " + print(f"[Album Pre-flight] Found album folder: {best_album.username} — " f"{best_album.track_count} tracks ({best_album.dominant_quality})") # Browse the user's folder to get all tracks (may have more than search returned) @@ -27113,7 +27113,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): } preflight_tracks = folder_tracks _sr.info(f"[Album Pre-flight] Browsed folder: {len(folder_tracks)} audio tracks available") - print(f"✅ [Album Pre-flight] Cached {len(folder_tracks)} tracks from {best_album.username} for source reuse") + print(f"[Album Pre-flight] Cached {len(folder_tracks)} tracks from {best_album.username} for source reuse") else: _sr.info(f"[Album Pre-flight] Browse returned files but no audio tracks") else: @@ -27124,16 +27124,16 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): 'folder_path': best_album.album_path } preflight_tracks = best_album.tracks - print(f"✅ [Album Pre-flight] Using {len(best_album.tracks)} tracks from search results (browse unavailable)") + print(f"[Album Pre-flight] Using {len(best_album.tracks)} tracks from search results (browse unavailable)") else: _sr.info(f"[Album Pre-flight] No album results passed quality filter") - print(f"⚠️ [Album Pre-flight] No album results matched quality preferences") + print(f"[Album Pre-flight] No album results matched quality preferences") else: _sr.info(f"[Album Pre-flight] Search returned no album results (got {len(track_results)} individual tracks)") - print(f"⚠️ [Album Pre-flight] No complete album folders found, falling back to track-by-track search") + print(f"[Album Pre-flight] No complete album folders found, falling back to track-by-track search") except Exception as preflight_err: - print(f"⚠️ [Album Pre-flight] Search failed (non-fatal, falling back to track-by-track): {preflight_err}") + print(f"[Album Pre-flight] Search failed (non-fatal, falling back to track-by-track): {preflight_err}") source_reuse_logger.info(f"[Album Pre-flight] Exception: {preflight_err}") with tasks_lock: @@ -27146,7 +27146,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): download_batches[batch_id]['last_good_source'] = preflight_source download_batches[batch_id]['source_folder_tracks'] = preflight_tracks download_batches[batch_id]['failed_sources'] = set() - print(f"📦 [Album Pre-flight] Pre-loaded source reuse data on batch {batch_id}") + print(f"[Album Pre-flight] Pre-loaded source reuse data on batch {batch_id}") # Compute total_discs for multi-disc album subfolder support # Use ALL tracks (tracks_json), not just missing ones, to correctly detect multi-disc @@ -27155,7 +27155,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): total_discs = max((t.get('disc_number', 1) for t in tracks_json), default=1) batch_album_context['total_discs'] = total_discs if total_discs > 1: - print(f"💿 [Multi-Disc] Detected {total_discs} discs for album '{batch_album_context.get('name')}'") + print(f"[Multi-Disc] Detected {total_discs} discs for album '{batch_album_context.get('name')}'") # Pre-compute per-album data for wishlist tracks (grouped by album ID) # Wishlist tracks aren't batch_is_album but each track has disc_number in spotify_data @@ -27216,7 +27216,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): else: _fallback_name = t.get('artist', '') wishlist_album_artist_map[album_id] = {'name': _fallback_name or 'Unknown Artist'} - print(f"🔗 [Wishlist Album Grouping] Album '{_wl_album.get('name', album_id)}' → artist: '{wishlist_album_artist_map[album_id].get('name', '?')}'") + print(f"[Wishlist Album Grouping] Album '{_wl_album.get('name', album_id)}' → artist: '{wishlist_album_artist_map[album_id].get('name', '?')}'") @@ -27229,7 +27229,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): track_info['_explicit_album_context'] = batch_album_context track_info['_explicit_artist_context'] = batch_artist_context track_info['_is_explicit_album_download'] = True - print(f"🎵 [Task Creation] Added explicit album context for: {track_info.get('name')}") + print(f"[Task Creation] Added explicit album context for: {track_info.get('name')}") # SPECIAL WISHLIST HANDLING: Inject album context if available to force grouping elif playlist_id == 'wishlist': @@ -27288,16 +27288,16 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): track_info['_explicit_album_context'] = album_ctx track_info['_explicit_artist_context'] = artist_ctx track_info['_is_explicit_album_download'] = True - print(f"🎵 [Wishlist] Added album context for: '{track_info.get('name')}' -> '{album_ctx['name']}'") + print(f"[Wishlist] Added album context for: '{track_info.get('name')}' -> '{album_ctx['name']}'") # Add playlist folder mode flag for sync page playlists if batch_playlist_folder_mode: track_info['_playlist_folder_mode'] = True track_info['_playlist_name'] = batch_playlist_name - print(f"📁 [Task Creation] Added playlist folder mode for: {track_info.get('name')} → {batch_playlist_name}") + print(f"[Task Creation] Added playlist folder mode for: {track_info.get('name')} → {batch_playlist_name}") else: - print(f"🔍 [Debug] Task Creation - playlist folder mode NOT enabled for: {track_info.get('name')}") + print(f"[Debug] Task Creation - playlist folder mode NOT enabled for: {track_info.get('name')}") download_tasks[task_id] = { 'status': 'pending', 'track_info': track_info, @@ -27313,7 +27313,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): _start_next_batch_of_downloads(batch_id) except Exception as e: - print(f"❌ Master worker for batch {batch_id} failed: {e}") + print(f"Master worker for batch {batch_id} failed: {e}") import traceback traceback.print_exc() @@ -27329,11 +27329,11 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): url_hash = playlist_id.replace('youtube_', '') if url_hash in youtube_playlist_states: youtube_playlist_states[url_hash]['phase'] = 'discovered' - print(f"📋 Reset YouTube playlist {url_hash} to discovered phase (error)") + print(f"Reset YouTube playlist {url_hash} to discovered phase (error)") # Handle auto-initiated wishlist errors - reset flag if is_auto_batch and playlist_id == 'wishlist': - print("❌ [Auto-Wishlist] Master worker error - resetting auto-processing flag") + print("[Auto-Wishlist] Master worker error - resetting auto-processing flag") global wishlist_auto_processing, wishlist_auto_processing_timestamp with wishlist_timer_lock: wishlist_auto_processing = False @@ -27345,21 +27345,21 @@ def _run_post_processing_worker(task_id, batch_id): after successful file verification and processing. This matches sync.py's reliability. """ try: - print(f"🔧 [Post-Processing] Starting verification for task {task_id}") + print(f"[Post-Processing] Starting verification for task {task_id}") # Retrieve task details from global state with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Post-Processing] Task {task_id} not found in download_tasks") + print(f"[Post-Processing] Task {task_id} not found in download_tasks") return task = download_tasks[task_id].copy() # Check if task was cancelled or already completed during post-processing if task['status'] == 'cancelled': - print(f"❌ [Post-Processing] Task {task_id} was cancelled, skipping verification") + print(f"[Post-Processing] Task {task_id} was cancelled, skipping verification") return if task['status'] == 'completed' or task.get('stream_processed'): - print(f"✅ [Post-Processing] Task {task_id} already completed by stream processor, skipping verification") + print(f"[Post-Processing] Task {task_id} already completed by stream processor, skipping verification") return # Extract file information for verification @@ -27368,7 +27368,7 @@ def _run_post_processing_worker(task_id, batch_id): task_username = task.get('username') or track_info.get('username') if not task_filename or not task_username: - print(f"❌ [Post-Processing] Missing filename or username for task {task_id}") + print(f"[Post-Processing] Missing filename or username for task {task_id}") with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['status'] = 'failed' @@ -27384,39 +27384,39 @@ def _run_post_processing_worker(task_id, batch_id): context_key = _make_context_key(task_username, task_filename) expected_final_filename = None - print(f"🔍 [Post-Processing] Looking up context with key: {context_key}") + print(f"[Post-Processing] Looking up context with key: {context_key}") with matched_context_lock: context = matched_downloads_context.get(context_key) # Debug: Show all available context keys available_keys = list(matched_downloads_context.keys()) - print(f"🔍 [Post-Processing] Available context keys: {available_keys[:10]}...") # Show first 10 keys + print(f"[Post-Processing] Available context keys: {available_keys[:10]}...") # Show first 10 keys if context: - print(f"✅ [Post-Processing] Found context for key: {context_key}") + print(f"[Post-Processing] Found context for key: {context_key}") try: original_search = context.get("original_search_result", {}) - print(f"🔍 [Post-Processing] original_search keys: {list(original_search.keys())}") + print(f"[Post-Processing] original_search keys: {list(original_search.keys())}") spotify_clean_title = original_search.get('spotify_clean_title') track_number = original_search.get('track_number') - print(f"🔍 [Post-Processing] spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}") + print(f"[Post-Processing] spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}") if spotify_clean_title and track_number: # Generate expected final filename that stream processor would create # Pattern: f"{track_number:02d} - {clean_title}.flac" sanitized_title = spotify_clean_title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_') expected_final_filename = f"{track_number:02d} - {sanitized_title}.flac" - print(f"🎯 [Post-Processing] Generated expected final filename: {expected_final_filename}") + print(f"[Post-Processing] Generated expected final filename: {expected_final_filename}") else: - print(f"❌ [Post-Processing] Missing required data - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}") + print(f"[Post-Processing] Missing required data - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}") except Exception as e: - print(f"⚠️ [Post-Processing] Error generating expected filename: {e}") + print(f"[Post-Processing] Error generating expected filename: {e}") import traceback traceback.print_exc() else: - print(f"❌ [Post-Processing] No context found for key: {context_key}") + print(f"[Post-Processing] No context found for key: {context_key}") # Try fuzzy matching with similar keys containing the filename # SAFETY: Constrain to same Soulseek username to prevent cross-album # metadata contamination during mass downloads (e.g., two albums both @@ -27428,35 +27428,35 @@ def _run_post_processing_worker(task_id, batch_id): # Use the first similar key found fuzzy_key = similar_keys[0] context = matched_downloads_context.get(fuzzy_key) - print(f"✅ [Post-Processing] Found context using fuzzy key matching: {fuzzy_key}") + print(f"[Post-Processing] Found context using fuzzy key matching: {fuzzy_key}") # Generate expected final filename using the found context try: original_search = context.get("original_search_result", {}) - print(f"🔍 [Post-Processing] fuzzy context original_search keys: {list(original_search.keys())}") + print(f"[Post-Processing] fuzzy context original_search keys: {list(original_search.keys())}") spotify_clean_title = original_search.get('spotify_clean_title') track_number = original_search.get('track_number') - print(f"🔍 [Post-Processing] fuzzy context spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}") + print(f"[Post-Processing] fuzzy context spotify_clean_title: '{spotify_clean_title}', track_number: {track_number}") if spotify_clean_title and track_number: # Generate expected final filename that stream processor would create # Pattern: f"{track_number:02d} - {clean_title}.flac" sanitized_title = spotify_clean_title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_') expected_final_filename = f"{track_number:02d} - {sanitized_title}.flac" - print(f"🎯 [Post-Processing] Generated expected final filename from fuzzy match: {expected_final_filename}") + print(f"[Post-Processing] Generated expected final filename from fuzzy match: {expected_final_filename}") else: - print(f"❌ [Post-Processing] Missing required data from fuzzy match - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}") + print(f"[Post-Processing] Missing required data from fuzzy match - spotify_clean_title: {bool(spotify_clean_title)}, track_number: {bool(track_number)}") except Exception as e: - print(f"⚠️ [Post-Processing] Error generating expected filename from fuzzy match: {e}") + print(f"[Post-Processing] Error generating expected filename from fuzzy match: {e}") import traceback traceback.print_exc() else: - print(f"🔍 [Post-Processing] No similar keys found containing '{task_basename}'") + print(f"[Post-Processing] No similar keys found containing '{task_basename}'") # Show a sample of what keys actually exist for debugging sample_keys = list(matched_downloads_context.keys())[:5] - print(f"🔍 [Post-Processing] Sample of existing keys: {sample_keys}") + print(f"[Post-Processing] Sample of existing keys: {sample_keys}") # RESILIENT FILE-FINDING LOOP: Try up to 3 times with delays found_file = None @@ -27465,7 +27465,7 @@ def _run_post_processing_worker(task_id, batch_id): # CRITICAL FIX: For YouTube downloads, the filename in task is 'id||title' (metadata), # but the actual file on disk is 'Title.mp3'. We must ask the client for the real path. if (task.get('username') == 'youtube' or '||' in str(task_filename)) and not found_file: - logger.info(f"🔧 [Post-Processing] Detected YouTube download task: {task_id}") + logger.info(f"[Post-Processing] Detected YouTube download task: {task_id}") try: # Query the download orchestrator for the status which contains the real file path # CRITICAL FIX: Use the actual download_id designated by the client, not the internal task_id @@ -27499,63 +27499,63 @@ def _run_post_processing_worker(task_id, batch_id): # runs with CWD as project root, not download dir. found_file = real_path - logger.info(f"✅ [Post-Processing] Resolved actual YouTube filename: {found_file} (Location: {file_location})") + logger.info(f"[Post-Processing] Resolved actual YouTube filename: {found_file} (Location: {file_location})") else: - logger.warning(f"⚠️ [Post-Processing] YouTube status reported path but file missing: {real_path}") + logger.warning(f"[Post-Processing] YouTube status reported path but file missing: {real_path}") else: - logger.warning(f"⚠️ [Post-Processing] YouTube status returned no file_path for task {task_id}") + logger.warning(f"[Post-Processing] YouTube status returned no file_path for task {task_id}") except Exception as e: - logger.error(f"⚠️ [Post-Processing] Failed to retrieve YouTube task status: {e}") + logger.error(f"[Post-Processing] Failed to retrieve YouTube task status: {e}") _file_search_max_retries = 5 for retry_count in range(_file_search_max_retries): # If we already resolved the file (e.g. via YouTube status), skip searching if found_file: - print(f"🎯 [Post-Processing] Skipping search loop, file already resolved: {found_file}") + print(f"[Post-Processing] Skipping search loop, file already resolved: {found_file}") break # Check if stream processor already completed this task while we were waiting with tasks_lock: if task_id in download_tasks: if download_tasks[task_id].get('stream_processed') or download_tasks[task_id]['status'] == 'completed': - print(f"✅ [Post-Processing] Task {task_id} was completed by stream processor during file search - done") + print(f"[Post-Processing] Task {task_id} was completed by stream processor during file search - done") return - print(f"🔍 [Post-Processing] Attempt {retry_count + 1}/{_file_search_max_retries} to find file") - print(f"🔍 [Post-Processing] Original filename: {task_basename}") + print(f"[Post-Processing] Attempt {retry_count + 1}/{_file_search_max_retries} to find file") + print(f"[Post-Processing] Original filename: {task_basename}") if expected_final_filename: - print(f"🔍 [Post-Processing] Expected final filename: {expected_final_filename}") + print(f"[Post-Processing] Expected final filename: {expected_final_filename}") else: - print(f"⚠️ [Post-Processing] No expected final filename available") + print(f"[Post-Processing] No expected final filename available") # Strategy 1: Try with original filename in both downloads and transfer - print(f"🔍 [Post-Processing] Strategy 1: Searching with original filename...") + print(f"[Post-Processing] Strategy 1: Searching with original filename...") found_file, file_location = _find_completed_file_robust(download_dir, task_filename, transfer_dir) if found_file: - print(f"✅ [Post-Processing] Strategy 1 SUCCESS: Found file with original filename in {file_location}: {found_file}") + print(f"[Post-Processing] Strategy 1 SUCCESS: Found file with original filename in {file_location}: {found_file}") else: - print(f"❌ [Post-Processing] Strategy 1 FAILED: Original filename not found in either location") + print(f"[Post-Processing] Strategy 1 FAILED: Original filename not found in either location") # Strategy 2: If not found and we have an expected final filename, try that in transfer folder if not found_file and expected_final_filename: - print(f"🔍 [Post-Processing] Strategy 2: Searching transfer folder with expected final filename...") + print(f"[Post-Processing] Strategy 2: Searching transfer folder with expected final filename...") found_result = _find_completed_file_robust(transfer_dir, expected_final_filename) if found_result and found_result[0]: found_file, file_location = found_result[0], 'transfer' - print(f"✅ [Post-Processing] Strategy 2 SUCCESS: Found file with expected final filename: {found_file}") + print(f"[Post-Processing] Strategy 2 SUCCESS: Found file with expected final filename: {found_file}") else: - print(f"❌ [Post-Processing] Strategy 2 FAILED: Expected final filename not found in transfer folder") + print(f"[Post-Processing] Strategy 2 FAILED: Expected final filename not found in transfer folder") elif not expected_final_filename: - print(f"⏭️ [Post-Processing] Strategy 2 SKIPPED: No expected final filename available") + print(f"[Post-Processing] Strategy 2 SKIPPED: No expected final filename available") if found_file: - print(f"🎯 [Post-Processing] FILE FOUND after {retry_count + 1} attempts in {file_location}: {found_file}") + print(f"[Post-Processing] FILE FOUND after {retry_count + 1} attempts in {file_location}: {found_file}") break else: - print(f"❌ [Post-Processing] All search strategies failed on attempt {retry_count + 1}/{_file_search_max_retries}") + print(f"[Post-Processing] All search strategies failed on attempt {retry_count + 1}/{_file_search_max_retries}") if retry_count < _file_search_max_retries - 1: # Don't sleep on final attempt - print(f"⏳ [Post-Processing] Waiting 5 seconds before next attempt...") + print(f"[Post-Processing] Waiting 5 seconds before next attempt...") time.sleep(5) if not found_file: @@ -27565,7 +27565,7 @@ def _run_post_processing_worker(task_id, batch_id): with tasks_lock: if task_id in download_tasks: if download_tasks[task_id].get('stream_processed') or download_tasks[task_id]['status'] == 'completed': - print(f"✅ [Post-Processing] Task {task_id} was completed by stream processor - not marking as failed") + print(f"[Post-Processing] Task {task_id} was completed by stream processor - not marking as failed") return download_tasks[task_id]['status'] = 'failed' download_tasks[task_id]['error_message'] = f'File not found on disk after {_file_search_max_retries} search attempts. Expected: {os.path.basename(task_filename)}' @@ -27574,7 +27574,7 @@ def _run_post_processing_worker(task_id, batch_id): # Handle file found in transfer folder - already completed by stream processor if file_location == 'transfer': - print(f"🎯 [Post-Processing] File found in transfer folder - already completed by stream processor: {found_file}") + print(f"[Post-Processing] File found in transfer folder - already completed by stream processor: {found_file}") # Check if metadata enhancement was completed metadata_enhanced = False @@ -27583,7 +27583,7 @@ def _run_post_processing_worker(task_id, batch_id): metadata_enhanced = download_tasks[task_id].get('metadata_enhanced', False) if not metadata_enhanced: - print(f"⚠️ [Post-Processing] File in transfer folder missing metadata enhancement - completing now") + print(f"[Post-Processing] File in transfer folder missing metadata enhancement - completing now") # Attempt to complete metadata enhancement using context if context and expected_final_filename: try: @@ -27602,13 +27602,13 @@ def _run_post_processing_worker(task_id, batch_id): # If no track number in context, extract from filename if track_number == 1 and found_file: - print(f"⚠️ [Verification] No track_number in context, extracting from filename: {os.path.basename(found_file)}") + print(f"[Verification] No track_number in context, extracting from filename: {os.path.basename(found_file)}") track_number = _extract_track_number_from_filename(found_file) print(f" -> Extracted track number: {track_number}") # Ensure track_number is valid if not isinstance(track_number, int) or track_number < 1: - print(f"⚠️ [Verification] Invalid track number ({track_number}), defaulting to 1") + print(f"[Verification] Invalid track number ({track_number}), defaulting to 1") track_number = 1 # Get clean track name @@ -27641,34 +27641,34 @@ def _run_post_processing_worker(task_id, batch_id): consistent_album_name = _resolve_album_group(spotify_artist, album_info, original_album_ctx) album_info['album_name'] = consistent_album_name except Exception as group_err: - print(f"⚠️ [Verification] Album grouping failed, using raw name: {group_err}") + print(f"[Verification] Album grouping failed, using raw name: {group_err}") else: - print(f"🎯 [Verification] Explicit album download - preserving Spotify album name: '{album_info['album_name']}'") + print(f"[Verification] Explicit album download - preserving Spotify album name: '{album_info['album_name']}'") - print(f"🎯 [Verification] Created proper album_info - track_number: {track_number}, album: {album_info['album_name']}") + print(f"[Verification] Created proper album_info - track_number: {track_number}, album: {album_info['album_name']}") - print(f"🎵 [Post-Processing] Attempting metadata enhancement for: {found_file}") - print(f"🔍 [Metadata Input] Verification worker - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})") - print(f"🔍 [Metadata Input] Verification worker - album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}") + print(f"[Post-Processing] Attempting metadata enhancement for: {found_file}") + print(f"[Metadata Input] Verification worker - artist: '{spotify_artist.get('name', 'MISSING')}' (id: {spotify_artist.get('id', 'MISSING')})") + print(f"[Metadata Input] Verification worker - album: '{album_info.get('album_name', 'MISSING')}', track#: {album_info.get('track_number', 'MISSING')}, source: {album_info.get('source', 'unknown')}") enhancement_success = _enhance_file_metadata(found_file, context, spotify_artist, album_info) if enhancement_success: with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['metadata_enhanced'] = True - print(f"✅ [Post-Processing] Successfully completed metadata enhancement for: {os.path.basename(found_file)}") + print(f"[Post-Processing] Successfully completed metadata enhancement for: {os.path.basename(found_file)}") else: - print(f"⚠️ [Post-Processing] Metadata enhancement returned False for: {os.path.basename(found_file)}") + print(f"[Post-Processing] Metadata enhancement returned False for: {os.path.basename(found_file)}") else: - print(f"⚠️ [Post-Processing] Missing spotify_artist or spotify_album in context") - print(f"⚠️ [Post-Processing] spotify_artist: {spotify_artist is not None}, spotify_album: {spotify_album is not None}") + print(f"[Post-Processing] Missing spotify_artist or spotify_album in context") + print(f"[Post-Processing] spotify_artist: {spotify_artist is not None}, spotify_album: {spotify_album is not None}") except Exception as enhancement_error: import traceback - print(f"❌ [Post-Processing] Error during metadata enhancement: {enhancement_error}\n{traceback.format_exc()}") + print(f"[Post-Processing] Error during metadata enhancement: {enhancement_error}\n{traceback.format_exc()}") else: - print(f"⚠️ [Post-Processing] Cannot complete metadata enhancement - missing context or expected filename") + print(f"[Post-Processing] Cannot complete metadata enhancement - missing context or expected filename") else: - print(f"✅ [Post-Processing] File already has metadata enhancement completed") + print(f"[Post-Processing] File already has metadata enhancement completed") with tasks_lock: if task_id in download_tasks: @@ -27679,7 +27679,7 @@ def _run_post_processing_worker(task_id, batch_id): with matched_context_lock: if context_key in matched_downloads_context: del matched_downloads_context[context_key] - print(f"🗑️ [Verification] Cleaned up context after successful verification: {context_key}") + print(f"[Verification] Cleaned up context after successful verification: {context_key}") _on_download_completed(batch_id, task_id, success=True) return @@ -27694,12 +27694,12 @@ def _run_post_processing_worker(task_id, batch_id): context = matched_downloads_context.get(context_key) if context: - print(f"🎯 [Post-Processing] Found matched context, running full post-processing for: {context_key}") + print(f"[Post-Processing] Found matched context, running full post-processing for: {context_key}") # Run the existing post-processing logic with verification _post_process_matched_download_with_verification(context_key, context, found_file, task_id, batch_id) else: # No matched context - just mark as completed since file exists - print(f"📁 [Post-Processing] No matched context, marking as completed: {os.path.basename(found_file)}") + print(f"[Post-Processing] No matched context, marking as completed: {os.path.basename(found_file)}") with tasks_lock: if task_id in download_tasks: track_info = download_tasks[task_id].get('track_info') @@ -27709,13 +27709,13 @@ def _run_post_processing_worker(task_id, batch_id): with matched_context_lock: if context_key in matched_downloads_context: del matched_downloads_context[context_key] - print(f"🗑️ [Verification] Cleaned up leftover context: {context_key}") + print(f"[Verification] Cleaned up leftover context: {context_key}") # Call completion callback since there's no other post-processing to handle it _on_download_completed(batch_id, task_id, success=True) except Exception as processing_error: - print(f"❌ [Post-Processing] Processing failed for task {task_id}: {processing_error}") + print(f"[Post-Processing] Processing failed for task {task_id}: {processing_error}") with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['status'] = 'failed' @@ -27723,7 +27723,7 @@ def _run_post_processing_worker(task_id, batch_id): _on_download_completed(batch_id, task_id, success=False) except Exception as e: - print(f"❌ [Post-Processing] Critical error in post-processing worker for task {task_id}: {e}") + print(f"[Post-Processing] Critical error in post-processing worker for task {task_id}: {e}") with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['status'] = 'failed' @@ -27740,33 +27740,33 @@ def _download_track_worker(task_id, batch_id=None): # Retrieve task details from global state with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Modal Worker] Task {task_id} not found in download_tasks") + print(f"[Modal Worker] Task {task_id} not found in download_tasks") return task = download_tasks[task_id].copy() # Cancellation Checkpoint 1: Before doing anything with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Modal Worker] Task {task_id} was deleted before starting") + print(f"[Modal Worker] Task {task_id} was deleted before starting") return if download_tasks[task_id]['status'] == 'cancelled': - print(f"❌ [Modal Worker] Task {task_id} cancelled before starting") + print(f"[Modal Worker] Task {task_id} cancelled before starting") # V2 FIX: Don't call _on_download_completed for cancelled V2 tasks # V2 system handles worker slot freeing in atomic cancel function task_playlist_id = download_tasks[task_id].get('playlist_id') if task_playlist_id: - print(f"⏭️ [Modal Worker] V2 task {task_id} cancelled - worker slot already freed by V2 system") + print(f"[Modal Worker] V2 task {task_id} cancelled - worker slot already freed by V2 system") return # V2 system already handled worker slot management elif batch_id: # Legacy system - use old completion callback - print(f"⏭️ [Modal Worker] Legacy task {task_id} cancelled - using legacy completion callback") + print(f"[Modal Worker] Legacy task {task_id} cancelled - using legacy completion callback") _on_download_completed(batch_id, task_id, success=False) return track_data = task['track_info'] track_name = track_data.get('name', 'Unknown Track') - print(f"🎯 [Modal Worker] Task {task_id} starting search for track: '{track_name}'") + print(f"[Modal Worker] Task {task_id} starting search for track: '{track_name}'") # Recreate a SpotifyTrack object for the matching engine # Handle both string format and Spotify API format for artists @@ -27797,7 +27797,7 @@ def _download_track_worker(task_id, batch_id=None): duration_ms=track_data.get('duration_ms', 0), popularity=track_data.get('popularity', 0) ) - print(f"📥 [Modal Worker] Starting download task for: {track.name} by {track.artists[0] if track.artists else 'Unknown'}") + print(f"[Modal Worker] Starting download task for: {track.name} by {track.artists[0] if track.artists else 'Unknown'}") # === SOURCE REUSE: Check batch's last good source before searching === if _try_source_reuse(task_id, batch_id, track): @@ -27871,8 +27871,8 @@ def _download_track_worker(task_id, batch_id=None): seen.add(query.lower()) search_queries = unique_queries - print(f"🔍 [Modal Worker] Generated {len(search_queries)} smart search queries for '{track.name}': {search_queries}") - print(f"🔍 [Modal Worker] About to start search loop for task {task_id} (track: '{track.name}')") + print(f"[Modal Worker] Generated {len(search_queries)} smart search queries for '{track.name}': {search_queries}") + print(f"[Modal Worker] About to start search loop for task {task_id} (track: '{track.name}')") # 2. Sequential Query Search (matches GUI's start_search_worker_parallel logic) search_diagnostics = [] # Track what happened per query for detailed error messages @@ -27881,29 +27881,29 @@ def _download_track_worker(task_id, batch_id=None): # Cancellation check before each query with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Modal Worker] Task {task_id} was deleted during query {query_index + 1}") + print(f"[Modal Worker] Task {task_id} was deleted during query {query_index + 1}") return if download_tasks[task_id]['status'] == 'cancelled': - print(f"❌ [Modal Worker] Task {task_id} cancelled during query {query_index + 1}") + print(f"[Modal Worker] Task {task_id} cancelled during query {query_index + 1}") # Don't call _on_download_completed for cancelled tasks as it can stop monitoring return download_tasks[task_id]['current_query_index'] = query_index - print(f"🔍 [Modal Worker] Query {query_index + 1}/{len(search_queries)}: '{query}'") - print(f"🔍 [DEBUG] About to call soulseek search for task {task_id}") + print(f"[Modal Worker] Query {query_index + 1}/{len(search_queries)}: '{query}'") + print(f"[DEBUG] About to call soulseek search for task {task_id}") try: # Perform search with timeout tracks_result, _ = run_async(soulseek_client.search(query, timeout=30)) - print(f"🔍 [DEBUG] Search completed for task {task_id}, got {len(tracks_result) if tracks_result else 0} results") + print(f"[DEBUG] Search completed for task {task_id}, got {len(tracks_result) if tracks_result else 0} results") # CRITICAL: Check cancellation immediately after search returns with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Modal Worker] Task {task_id} was deleted after search returned") + print(f"[Modal Worker] Task {task_id} was deleted after search returned") return if download_tasks[task_id]['status'] == 'cancelled': - print(f"❌ [Modal Worker] Task {task_id} cancelled after search returned - ignoring results") + print(f"[Modal Worker] Task {task_id} cancelled after search returned - ignoring results") # Don't call _on_download_completed for cancelled tasks as it can stop monitoring # The cancellation endpoint already handles batch management properly return @@ -27913,15 +27913,15 @@ def _download_track_worker(task_id, batch_id=None): # Validate candidates using GUI's get_valid_candidates logic candidates = get_valid_candidates(tracks_result, track, query) if candidates: - print(f"✅ [Modal Worker] Found {len(candidates)} valid candidates for query '{query}'") + print(f"[Modal Worker] Found {len(candidates)} valid candidates for query '{query}'") # CRITICAL: Check cancellation before processing candidates with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Modal Worker] Task {task_id} was deleted before processing candidates") + print(f"[Modal Worker] Task {task_id} was deleted before processing candidates") return if download_tasks[task_id]['status'] == 'cancelled': - print(f"❌ [Modal Worker] Task {task_id} cancelled before processing candidates") + print(f"[Modal Worker] Task {task_id} cancelled before processing candidates") # Don't call _on_download_completed for cancelled tasks as it can stop monitoring return # Store candidates for retry fallback (like GUI) @@ -27932,7 +27932,7 @@ def _download_track_worker(task_id, batch_id=None): if success: # Download initiated successfully - let the download monitoring system handle completion if batch_id: - print(f"✅ [Modal Worker] Download initiated successfully for task {task_id} - monitoring will handle completion") + print(f"[Modal Worker] Download initiated successfully for task {task_id} - monitoring will handle completion") # Store this source for batch reuse with tasks_lock: used_filename = download_tasks.get(task_id, {}).get('filename') @@ -27949,7 +27949,7 @@ def _download_track_worker(task_id, batch_id=None): search_diagnostics.append(f'"{query}": no results found') except Exception as e: - print(f"⚠️ [Modal Worker] Search failed for query '{query}': {e}") + print(f"[Modal Worker] Search failed for query '{query}': {e}") search_diagnostics.append(f'"{query}": search error — {e}') continue @@ -27981,7 +27981,7 @@ def _download_track_worker(task_id, batch_id=None): # harmless (streaming sources return fast). remaining_sources = [s for s in hybrid_order[1:] if s in source_clients and source_clients[s]] if remaining_sources: - print(f"🔄 [Hybrid Fallback] Primary source had no valid matches. Trying fallback sources: {remaining_sources}") + print(f"[Hybrid Fallback] Primary source had no valid matches. Trying fallback sources: {remaining_sources}") for fallback_source in remaining_sources: fb_client = source_clients[fallback_source] @@ -27991,27 +27991,27 @@ def _download_track_worker(task_id, batch_id=None): # Use first 2 queries only for speed for fb_query in search_queries[:2]: try: - print(f"🔄 [Hybrid Fallback] Trying {fallback_source}: '{fb_query}'") + print(f"[Hybrid Fallback] Trying {fallback_source}: '{fb_query}'") fb_results, _ = run_async(fb_client.search(fb_query, timeout=20)) if not fb_results: continue fb_candidates = get_valid_candidates(fb_results, track, fb_query) if fb_candidates: - print(f"✅ [Hybrid Fallback] {fallback_source} found {len(fb_candidates)} valid candidates!") + print(f"[Hybrid Fallback] {fallback_source} found {len(fb_candidates)} valid candidates!") success = _attempt_download_with_candidates(task_id, fb_candidates, track, batch_id) if success: return except Exception as e: - print(f"⚠️ [Hybrid Fallback] {fallback_source} search failed: {e}") + print(f"[Hybrid Fallback] {fallback_source} search failed: {e}") continue - print(f"⏭️ [Hybrid Fallback] {fallback_source} returned no valid candidates") + print(f"[Hybrid Fallback] {fallback_source} returned no valid candidates") except Exception as e: - print(f"⚠️ [Hybrid Fallback] Error in fallback logic: {e}") + print(f"[Hybrid Fallback] Error in fallback logic: {e}") # If we get here, all search queries and hybrid fallbacks failed - print(f"❌ [Modal Worker] No valid candidates found for '{track.name}' after trying all {len(search_queries)} queries.") + print(f"[Modal Worker] No valid candidates found for '{track.name}' after trying all {len(search_queries)} queries.") with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['status'] = 'not_found' @@ -28026,12 +28026,12 @@ def _download_track_worker(task_id, batch_id=None): try: _on_download_completed(batch_id, task_id, success=False) except Exception as completion_error: - print(f"❌ Error in batch completion callback for {task_id}: {completion_error}") + print(f"Error in batch completion callback for {task_id}: {completion_error}") except Exception as e: import traceback track_name_safe = locals().get('track_name', 'unknown') # Safe fallback for track_name - print(f"❌ CRITICAL ERROR in download task for '{track_name_safe}' (task_id: {task_id}): {e}") + print(f"CRITICAL ERROR in download task for '{track_name_safe}' (task_id: {task_id}): {e}") traceback.print_exc() # Update task status safely with timeout @@ -28042,27 +28042,27 @@ def _download_track_worker(task_id, batch_id=None): if task_id in download_tasks: download_tasks[task_id]['status'] = 'failed' download_tasks[task_id]['error_message'] = f'Unexpected error during download: {type(e).__name__}: {e}' - print(f"🔧 [Exception Recovery] Set task {task_id} status to 'failed'") + print(f"[Exception Recovery] Set task {task_id} status to 'failed'") finally: tasks_lock.release() else: - print(f"⚠️ [Exception Recovery] Could not acquire lock to update task {task_id} status") + print(f"[Exception Recovery] Could not acquire lock to update task {task_id} status") except Exception as status_error: - print(f"❌ Error updating task status in exception handler: {status_error}") + print(f"Error updating task status in exception handler: {status_error}") # Notify batch manager that this task completed (failed) - THREAD SAFE with RECOVERY if batch_id: try: _on_download_completed(batch_id, task_id, success=False) - print(f"✅ [Exception Recovery] Successfully freed worker slot for task {task_id}") + print(f"[Exception Recovery] Successfully freed worker slot for task {task_id}") except Exception as completion_error: - print(f"❌ [Exception Recovery] Error in batch completion callback for {task_id}: {completion_error}") + print(f"[Exception Recovery] Error in batch completion callback for {task_id}: {completion_error}") # CRITICAL: If batch completion fails, we need to manually recover the worker slot try: - print(f"🚨 [Exception Recovery] Attempting manual worker slot recovery for batch {batch_id}") + print(f"[Exception Recovery] Attempting manual worker slot recovery for batch {batch_id}") _recover_worker_slot(batch_id, task_id) except Exception as recovery_error: - print(f"💀 [Exception Recovery] FATAL: Could not recover worker slot: {recovery_error}") + print(f"[Exception Recovery] FATAL: Could not recover worker slot: {recovery_error}") def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None): """ @@ -28083,10 +28083,10 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) # Check cancellation before each attempt with tasks_lock: if task_id not in download_tasks: - print(f"❌ [Modal Worker] Task {task_id} was deleted during candidate {candidate_index + 1}") + print(f"[Modal Worker] Task {task_id} was deleted during candidate {candidate_index + 1}") return False if download_tasks[task_id]['status'] == 'cancelled': - print(f"❌ [Modal Worker] Task {task_id} cancelled during candidate {candidate_index + 1}") + print(f"[Modal Worker] Task {task_id} cancelled during candidate {candidate_index + 1}") # Don't call _on_download_completed for cancelled tasks as it can stop monitoring return False download_tasks[task_id]['current_candidate_index'] = candidate_index @@ -28094,14 +28094,14 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) # Create source key to avoid duplicate attempts (like GUI) source_key = f"{candidate.username}_{candidate.filename}" if source_key in used_sources: - print(f"⏭️ [Modal Worker] Skipping already tried source: {source_key}") + print(f"[Modal Worker] Skipping already tried source: {source_key}") continue # Blacklist check — skip sources the user has flagged as bad matches try: _bl_db = get_database() if _bl_db.is_blacklisted(candidate.username, candidate.filename): - print(f"⛔ [Modal Worker] Skipping blacklisted source: {source_key}") + print(f"[Modal Worker] Skipping blacklisted source: {source_key}") continue except Exception: pass @@ -28111,9 +28111,9 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['used_sources'].add(source_key) - print(f"🚫 [Modal Worker] Marked source as used before download attempt: {source_key}") + print(f"[Modal Worker] Marked source as used before download attempt: {source_key}") - print(f"🎯 [Modal Worker] Trying candidate {candidate_index + 1}/{len(candidates)}: {candidate.filename} (Confidence: {candidate.confidence:.2f})") + print(f"[Modal Worker] Trying candidate {candidate_index + 1}/{len(candidates)}: {candidate.filename} (Confidence: {candidate.confidence:.2f})") try: # Update task status to downloading @@ -28161,7 +28161,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) 'album_type': explicit_album.get('album_type', 'album'), 'artists': explicit_album.get('artists', [{'name': spotify_artist_context.get('name', '')}]) } - print(f"🎵 [Explicit Context] Using real album data: '{spotify_album_context['name']}' ({spotify_album_context['album_type']}, {spotify_album_context['total_discs']} disc(s))") + print(f"[Explicit Context] Using real album data: '{spotify_album_context['name']}' ({spotify_album_context['album_type']}, {spotify_album_context['total_discs']} disc(s))") else: # Fallback to generic context for playlists/wishlists # Extract album metadata from track_info if available (discovery enriches tracks with full album objects) @@ -28199,7 +28199,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) size = download_payload.get('size', 0) if not username or not filename: - print(f"❌ [Modal Worker] Invalid candidate data: missing username or filename") + print(f"[Modal Worker] Invalid candidate data: missing username or filename") continue # PROTECTION: Check if there's already an active download for this task @@ -28209,12 +28209,12 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) current_download_id = download_tasks[task_id].get('download_id') if current_download_id: - print(f"⚠️ [Modal Worker] Task {task_id} already has active download {current_download_id} - skipping new download attempt") - print(f"🔄 [Modal Worker] This prevents race condition where multiple retries start overlapping downloads") + print(f"[Modal Worker] Task {task_id} already has active download {current_download_id} - skipping new download attempt") + print(f"[Modal Worker] This prevents race condition where multiple retries start overlapping downloads") continue # Initiate download - print(f"🚀 [Modal Worker] Starting download: {username} / {os.path.basename(filename)}") + print(f"[Modal Worker] Starting download: {username} / {os.path.basename(filename)}") download_id = run_async(soulseek_client.download(username, filename, size)) if download_id: @@ -28233,7 +28233,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) enhanced_payload['spotify_clean_artist'] = track.artists[0] if track.artists else enhanced_payload.get('artist', '') # Preserve all artists for metadata tagging enhanced_payload['artists'] = [{'name': artist} for artist in track.artists] if track.artists else [] - print(f"✨ [Context] Using clean Spotify metadata - Album: '{track.album}', Title: '{track.name}'") + print(f"[Context] Using clean Spotify metadata - Album: '{track.album}', Title: '{track.name}'") # Get track_number and disc_number — prefer track data we already have, # fall back to detailed API call only if needed @@ -28246,14 +28246,14 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) enhanced_payload['track_number'] = tn enhanced_payload['disc_number'] = dn got_track_number = True - print(f"🔢 [Context] Added track_number from track_info: {tn}, disc_number: {dn}") + print(f"[Context] Added track_number from track_info: {tn}, disc_number: {dn}") # 2. Try the track object itself (from album tracks response) if not got_track_number and hasattr(track, 'track_number') and track.track_number: enhanced_payload['track_number'] = track.track_number enhanced_payload['disc_number'] = getattr(track, 'disc_number', 1) or 1 got_track_number = True - print(f"🔢 [Context] Added track_number from track object: {track.track_number}, disc_number: {enhanced_payload['disc_number']}") + print(f"[Context] Added track_number from track object: {track.track_number}, disc_number: {enhanced_payload['disc_number']}") # 3. Last resort — fetch from metadata source API if not got_track_number and hasattr(track, 'id') and track.id: @@ -28263,7 +28263,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) enhanced_payload['track_number'] = detailed_track['track_number'] enhanced_payload['disc_number'] = detailed_track.get('disc_number', 1) got_track_number = True - print(f"🔢 [Context] Added track_number from API: {detailed_track['track_number']}, disc_number: {enhanced_payload['disc_number']}") + print(f"[Context] Added track_number from API: {detailed_track['track_number']}, disc_number: {enhanced_payload['disc_number']}") # Backfill album metadata from detailed track when context # has incomplete data (missing release_date, total_tracks, etc.) @@ -28271,7 +28271,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) dt_album = detailed_track['album'] if not spotify_album_context.get('release_date') and dt_album.get('release_date'): spotify_album_context['release_date'] = dt_album['release_date'] - print(f"📅 [Context] Backfilled release_date from API: {dt_album['release_date']}") + print(f"[Context] Backfilled release_date from API: {dt_album['release_date']}") if not spotify_album_context.get('album_type') and dt_album.get('album_type'): spotify_album_context['album_type'] = dt_album['album_type'] if not spotify_album_context.get('total_tracks') and dt_album.get('total_tracks'): @@ -28281,18 +28281,18 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) if not spotify_album_context.get('image_url') and dt_album.get('images'): spotify_album_context['image_url'] = dt_album['images'][0].get('url', '') except Exception as e: - print(f"⚠️ [Context] API track details failed: {e}") + print(f"[Context] API track details failed: {e}") if not got_track_number: enhanced_payload.setdefault('track_number', 0) enhanced_payload.setdefault('disc_number', 1) - print(f"⚠️ [Context] No track_number found from any source") + print(f"[Context] No track_number found from any source") # Determine if this should be treated as album download # First check if we have explicit album context from artist page if has_explicit_context: is_album_context = True - print(f"✅ [Context] Using explicit album context flag from artist page") + print(f"[Context] Using explicit album context flag from artist page") else: # Fall back to guessing based on clean data is_album_context = ( @@ -28311,7 +28311,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) enhanced_payload['artists'] = [{'name': enhanced_payload['artist']}] enhanced_payload['track_number'] = track_info.get('track_number', 1) # Fallback when no clean Spotify data is_album_context = False - print(f"⚠️ [Context] Using fallback data - no clean Spotify metadata available, track_number={enhanced_payload['track_number']}") + print(f"[Context] Using fallback data - no clean Spotify metadata available, track_number={enhanced_payload['track_number']}") matched_downloads_context[context_key] = { "spotify_artist": spotify_artist_context, @@ -28325,21 +28325,21 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) "_download_username": username, # Source username for AcoustID skip logic } - print(f"🎯 [Context] Set is_album_download: {is_album_context} (has clean data: {has_clean_spotify_data})") - print(f"🔍 [Debug] Context creation - track_info: {track_info is not None}, playlist_folder_mode: {track_info.get('_playlist_folder_mode', False) if track_info else False}") + print(f"[Context] Set is_album_download: {is_album_context} (has clean data: {has_clean_spotify_data})") + print(f"[Debug] Context creation - track_info: {track_info is not None}, playlist_folder_mode: {track_info.get('_playlist_folder_mode', False) if track_info else False}") # Update task with successful download info with tasks_lock: if task_id in download_tasks: # PHASE 3: Final cancellation check after download started (GUI PARITY) if download_tasks[task_id]['status'] == 'cancelled': - print(f"🚫 [Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download") + print(f"[Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download") # Try to cancel the download immediately try: run_async(soulseek_client.cancel_download(download_id, username, remove=True)) - print(f"✅ Successfully cancelled active download {download_id}") + print(f"Successfully cancelled active download {download_id}") except Exception as cancel_error: - print(f"⚠️ Warning: Failed to cancel active download {download_id}: {cancel_error}") + print(f"Warning: Failed to cancel active download {download_id}: {cancel_error}") # Free worker slot if batch_id: @@ -28353,10 +28353,10 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) download_tasks[task_id]['username'] = username download_tasks[task_id]['filename'] = filename - print(f"✅ [Modal Worker] Download started successfully for '{filename}'. Download ID: {download_id}") + print(f"[Modal Worker] Download started successfully for '{filename}'. Download ID: {download_id}") return True # Success! else: - print(f"❌ [Modal Worker] Failed to start download for '{filename}'") + print(f"[Modal Worker] Failed to start download for '{filename}'") # Reset status back to searching for next attempt with tasks_lock: if task_id in download_tasks: @@ -28365,7 +28365,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) except Exception as e: import traceback - print(f"❌ [Modal Worker] Error attempting download for '{candidate.filename}': {e}") + print(f"[Modal Worker] Error attempting download for '{candidate.filename}': {e}") traceback.print_exc() # Reset status back to searching for next attempt with tasks_lock: @@ -28374,7 +28374,7 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) continue # All candidates failed - print(f"❌ [Modal Worker] All {len(candidates)} candidates failed for '{track.name}'") + print(f"[Modal Worker] All {len(candidates)} candidates failed for '{track.name}'") return False # ── Staging folder match cache (per-batch, avoids re-scanning for every track) ── @@ -28415,7 +28415,7 @@ def _get_staging_file_cache(batch_id): 'extension': ext, }) - print(f"📂 [Staging] Scanned {len(files)} audio files in staging folder") + print(f"[Staging] Scanned {len(files)} audio files in staging folder") with _staging_cache_lock: _staging_cache[batch_id] = files return files @@ -28483,7 +28483,7 @@ def _try_staging_match(task_id, batch_id, track): if not best_match or best_score < 0.75: return False - print(f"📂 [Staging] Match found for '{track_title}' by '{track_artist}': " + print(f"[Staging] Match found for '{track_title}' by '{track_artist}': " f"{os.path.basename(best_match['full_path'])} (score: {best_score:.2f})") # Copy the file to the transfer folder @@ -28500,7 +28500,7 @@ def _try_staging_match(task_id, batch_id, track): import shutil shutil.copy2(best_match['full_path'], dest_path) - print(f"📂 [Staging] Copied to transfer: {dest_path}") + print(f"[Staging] Copied to transfer: {dest_path}") # Mark task as completed with staging context with tasks_lock: @@ -28533,7 +28533,7 @@ def _try_staging_match(task_id, batch_id, track): return True except Exception as e: - print(f"❌ [Staging] Failed to use staging file: {e}") + print(f"[Staging] Failed to use staging file: {e}") return False @@ -28749,13 +28749,13 @@ def start_playlist_missing_downloads(playlist_id): missing_tracks = [t for t in missing_tracks if not _is_explicit_blocked(t.get('track', t))] skipped = before_count - len(missing_tracks) if skipped > 0: - print(f"🚫 [Content Filter] Filtered out {skipped} explicit track(s) from playlist download") + print(f"[Content Filter] Filtered out {skipped} explicit track(s) from playlist download") if not missing_tracks: return jsonify({"success": False, "error": "All tracks were filtered by explicit content setting"}), 400 # Add activity for playlist download missing start playlist_name = data.get('playlist_name', f'Playlist {playlist_id}') - add_activity_item("📥", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now") + add_activity_item("", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now") try: batch_id = str(uuid.uuid4()) @@ -28810,7 +28810,7 @@ def start_playlist_missing_downloads(playlist_id): return jsonify({"success": True, "batch_id": batch_id, "message": f"Queued {len(missing_tracks)} downloads for processing."}) except Exception as e: - print(f"❌ Error starting missing downloads: {e}") + print(f"Error starting missing downloads: {e}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/active-processes', methods=['GET']) @@ -28868,7 +28868,7 @@ def get_active_processes(): "download_process_id": state.get('download_process_id') # batch_id for download modal rehydration }) - print(f"📊 Active processes check: {len([p for p in active_processes if p['type'] == 'batch'])} download batches, {len([p for p in active_processes if p['type'] == 'youtube_playlist'])} YouTube playlists") + print(f"Active processes check: {len([p for p in active_processes if p['type'] == 'batch'])} download batches, {len([p for p in active_processes if p['type'] == 'youtube_playlist'])} YouTube playlists") return jsonify({"active_processes": active_processes}) def _build_batch_status_data(batch_id, batch, live_transfers_lookup): @@ -28918,13 +28918,13 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer')) found_file, file_location = _find_completed_file_robust(download_dir, task_filename, transfer_dir) if found_file: - print(f"✅ [Safety Valve] Task {task_id} stuck but file found in {file_location} — routing to post-processing") + print(f"[Safety Valve] Task {task_id} stuck but file found in {file_location} — routing to post-processing") task['status'] = 'post_processing' task['status_change_time'] = current_time missing_download_executor.submit(_run_post_processing_worker, task_id, batch_id) recovered = True except Exception as e: - print(f"⚠️ [Safety Valve] Error checking for completed file: {e}") + print(f"[Safety Valve] Error checking for completed file: {e}") if not recovered: if stuck_state == 'searching': @@ -28970,7 +28970,7 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): elif 'Failed' in state_str or 'Errored' in state_str or 'Rejected' in state_str or 'TimedOut' in state_str: # UNIFIED ERROR HANDLING: Let monitor handle errors for consistency # Monitor will detect errored state and trigger retry within 5 seconds - print(f"🔍 Task {task_id} API shows error state: {state_str} - letting monitor handle retry") + print(f"Task {task_id} API shows error state: {state_str} - letting monitor handle retry") # Keep task in current status (downloading/queued) so monitor can detect error # Don't mark as failed here - let the unified retry system handle it @@ -28986,13 +28986,13 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): if expected_size > 0 and transferred < expected_size: # State says complete but bytes don't match — keep current status task_status['status'] = task['status'] - print(f"⚠️ Task {task_id} state says complete but bytes incomplete ({transferred}/{expected_size})") + print(f"Task {task_id} state says complete but bytes incomplete ({transferred}/{expected_size})") # NEW VERIFICATION WORKFLOW: Use intermediate post_processing status # Only set this status once to prevent multiple worker submissions elif task['status'] != 'post_processing': task_status['status'] = 'post_processing' task['status'] = 'post_processing' - print(f"🔄 Task {task_id} API reports 'Succeeded' - starting post-processing verification") + print(f"Task {task_id} API reports 'Succeeded' - starting post-processing verification") # Submit post-processing worker to verify file and complete the task missing_download_executor.submit(_run_post_processing_worker, task_id, batch_id) @@ -29000,7 +29000,7 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): # FIXED: Always require verification workflow - no bypass for stream processed tasks # Stream processing only handles metadata, not file verification task_status['status'] = 'post_processing' - print(f"🔄 Task {task_id} waiting for verification worker to complete") + print(f"Task {task_id} waiting for verification worker to complete") elif 'InProgress' in state_str: task_status['status'] = 'downloading' else: @@ -29094,7 +29094,7 @@ def get_batched_download_statuses(): ) except Exception as batch_error: # Don't fail entire request if one batch has issues - print(f"❌ Error processing batch {batch_id}: {batch_error}") + print(f"Error processing batch {batch_id}: {batch_error}") response["batches"][batch_id] = {"error": str(batch_error)} # Add metadata for debugging/monitoring @@ -29123,12 +29123,12 @@ def get_batched_download_statuses(): response["debug_info"] = debug_info - print(f"📊 [Batched Status] Returning status for {len(response['batches'])} batches") + print(f"[Batched Status] Returning status for {len(response['batches'])} batches") # Log worker discrepancies for debugging discrepancies = [bid for bid, info in debug_info.items() if info.get("worker_discrepancy")] if discrepancies: - print(f"⚠️ [Batched Status] Worker count discrepancies in batches: {discrepancies}") + print(f"[Batched Status] Worker count discrepancies in batches: {discrepancies}") return jsonify(response) @@ -29160,7 +29160,7 @@ def cancel_download_task(): current_status = task.get('status', 'unknown') download_id = task.get('download_id') username = task.get('username') - print(f"🔍 [Cancel Debug] Task {task_id} - Current status: '{current_status}', download_id: {download_id}, username: {username}") + print(f"[Cancel Debug] Task {task_id} - Current status: '{current_status}', download_id: {download_id}, username: {username}") # Immediately mark as cancelled to prevent race conditions task['status'] = 'cancelled' @@ -29180,8 +29180,8 @@ def cancel_download_task(): # Free worker slot if there are active workers and task was actively running # This is more reliable than checking task status which can be inconsistent if active_count > 0 and current_status in ['pending', 'searching', 'downloading', 'queued']: - print(f"🔄 [Cancel] Task {task_id} (status: {current_status}) - freeing worker slot for batch {batch_id}") - print(f"🔄 [Cancel] Active count before: {active_count}") + print(f"[Cancel] Task {task_id} (status: {current_status}) - freeing worker slot for batch {batch_id}") + print(f"[Cancel] Active count before: {active_count}") # Use the completion callback with error handling _on_download_completed(batch_id, task_id, success=False) @@ -29189,35 +29189,35 @@ def cancel_download_task(): # Verify slot was actually freed new_active = download_batches[batch_id]['active_count'] - print(f"🔄 [Cancel] Active count after: {new_active}") + print(f"[Cancel] Active count after: {new_active}") elif active_count == 0: - print(f"🚫 [Cancel] Task {task_id} - no active workers to free") + print(f"[Cancel] Task {task_id} - no active workers to free") else: - print(f"🚫 [Cancel] Task {task_id} (status: {current_status}) - not actively running, no slot to free") + print(f"[Cancel] Task {task_id} (status: {current_status}) - not actively running, no slot to free") else: - print(f"🚫 [Cancel] Task {task_id} - batch {batch_id} not found") + print(f"[Cancel] Task {task_id} - batch {batch_id} not found") except Exception as slot_error: - print(f"❌ [Cancel] Error managing worker slot for {task_id}: {slot_error}") + print(f"[Cancel] Error managing worker slot for {task_id}: {slot_error}") # Attempt emergency recovery if normal completion failed if not worker_slot_freed: try: - print(f"🚨 [Cancel] Attempting emergency worker slot recovery") + print(f"[Cancel] Attempting emergency worker slot recovery") _recover_worker_slot(batch_id, task_id) except Exception as recovery_error: - print(f"💀 [Cancel] FATAL: Emergency recovery failed: {recovery_error}") + print(f"[Cancel] FATAL: Emergency recovery failed: {recovery_error}") else: - print(f"🚫 [Cancel] Task {task_id} cancelled (no batch_id - likely already completed)") + print(f"[Cancel] Task {task_id} cancelled (no batch_id - likely already completed)") # Optionally try to cancel the Soulseek download (don't block worker progression) if download_id and username: try: # This is an async call, so we run it and wait run_async(soulseek_client.cancel_download(download_id, username, remove=True)) - print(f"✅ Successfully cancelled Soulseek download {download_id} for task {task_id}") + print(f"Successfully cancelled Soulseek download {download_id} for task {task_id}") except Exception as e: - print(f"⚠️ Warning: Failed to cancel download on slskd, but worker already moved on. Error: {e}") + print(f"Warning: Failed to cancel download on slskd, but worker already moved on. Error: {e}") ### NEW LOGIC START: Add cancelled track to wishlist ### try: @@ -29288,11 +29288,11 @@ def cancel_download_task(): ) if success: - print(f"✅ Added cancelled track '{track_info.get('name')}' to wishlist.") + print(f"Added cancelled track '{track_info.get('name')}' to wishlist.") else: - print(f"❌ Failed to add cancelled track '{track_info.get('name')}' to wishlist.") + print(f"Failed to add cancelled track '{track_info.get('name')}' to wishlist.") except Exception as e: - print(f"❌ CRITICAL ERROR adding cancelled track to wishlist: {e}") + print(f"CRITICAL ERROR adding cancelled track to wishlist: {e}") ### NEW LOGIC END ### return jsonify({"success": True, "message": "Task cancelled and added to wishlist for retry."}) @@ -29335,7 +29335,7 @@ def _atomic_cancel_task(playlist_id, track_index): original_status = current_status # Store original status before changing it batch_id = task.get('batch_id') - print(f"🎯 [Atomic Cancel] Starting atomic cancel: playlist={playlist_id}, track={track_index}, task={task_id}, status={current_status}") + print(f"[Atomic Cancel] Starting atomic cancel: playlist={playlist_id}, track={track_index}, task={task_id}, status={current_status}") # Mark task as cancelled immediately (within same lock context) task['status'] = 'cancelled' @@ -29356,23 +29356,23 @@ def _atomic_cancel_task(playlist_id, track_index): # Free worker slot if task was consuming one # More precise check: only free if task was actually running if active_count > 0 and current_status in ['pending', 'searching', 'downloading', 'queued']: - print(f"🔄 [Atomic Cancel] Freeing worker slot for {task_id} (was {current_status})") + print(f"[Atomic Cancel] Freeing worker slot for {task_id} (was {current_status})") # CRITICAL: Direct worker slot management to prevent _on_download_completed race old_active = batch['active_count'] batch['active_count'] = max(0, old_active - 1) # Prevent negative counts worker_slot_freed = True - print(f"🔄 [Atomic Cancel] Worker count: {old_active} → {batch['active_count']}") + print(f"[Atomic Cancel] Worker count: {old_active} → {batch['active_count']}") # Try to start next task if available (still within lock) if (batch['queue_index'] < len(batch['queue']) and batch['active_count'] < batch['max_concurrent']): - print(f"🚀 [Atomic Cancel] Starting next task in queue") + print(f"[Atomic Cancel] Starting next task in queue") # Call the existing function to start next downloads # Note: This will be called outside the lock to prevent deadlock else: - print(f"🚫 [Atomic Cancel] No next task to start (queue_index: {batch['queue_index']}/{len(batch['queue'])}, active: {batch['active_count']}/{batch['max_concurrent']})") + print(f"[Atomic Cancel] No next task to start (queue_index: {batch['queue_index']}/{len(batch['queue'])}, active: {batch['active_count']}/{batch['max_concurrent']})") # Build result info task_info = { @@ -29385,11 +29385,11 @@ def _atomic_cancel_task(playlist_id, track_index): 'worker_slot_freed': worker_slot_freed } - print(f"✅ [Atomic Cancel] Successfully cancelled task {task_id}") + print(f"[Atomic Cancel] Successfully cancelled task {task_id}") return True, "Task cancelled successfully", task_info except Exception as e: - print(f"❌ [Atomic Cancel] Error in atomic cancel: {e}") + print(f"[Atomic Cancel] Error in atomic cancel: {e}") import traceback traceback.print_exc() return False, f"Internal error: {str(e)}", None @@ -29432,14 +29432,14 @@ def cancel_task_v2(): try: _start_next_batch_of_downloads(batch_id) except Exception as e: - print(f"⚠️ [Atomic Cancel] Warning: Could not start next downloads: {e}") + print(f"[Atomic Cancel] Warning: Could not start next downloads: {e}") # CRITICAL: Check for batch completion after V2 cancel # V2 system bypasses _on_download_completed, so we need to check completion manually try: _check_batch_completion_v2(batch_id) except Exception as e: - print(f"⚠️ [Atomic Cancel] Warning: Could not check batch completion: {e}") + print(f"[Atomic Cancel] Warning: Could not check batch completion: {e}") # Cancel Soulseek download if active (non-blocking) if task: @@ -29448,16 +29448,16 @@ def cancel_task_v2(): current_status = task.get('status') original_status = task_info.get('original_status', current_status) # Get original status from task_info - print(f"🔍 [Atomic Cancel] Task {task_id} state: status='{current_status}', original_status='{original_status}', download_id='{download_id}', username='{username}'") - print(f"🔍 [Atomic Cancel] Download ID type: {type(download_id)}, length: {len(str(download_id)) if download_id else 0}") + print(f"[Atomic Cancel] Task {task_id} state: status='{current_status}', original_status='{original_status}', download_id='{download_id}', username='{username}'") + print(f"[Atomic Cancel] Download ID type: {type(download_id)}, length: {len(str(download_id)) if download_id else 0}") backslash = '\\' - print(f"🔍 [Atomic Cancel] Download ID looks like filename: {download_id and ('/' in str(download_id) or backslash in str(download_id))}") + print(f"[Atomic Cancel] Download ID looks like filename: {download_id and ('/' in str(download_id) or backslash in str(download_id))}") if download_id and username: # Always try to cancel in slskd - doesn't matter what status it was # If it's not there or already done, the DELETE request will just fail harmlessly try: - print(f"🚫 [Atomic Cancel] Attempting to cancel Soulseek download:") + print(f"[Atomic Cancel] Attempting to cancel Soulseek download:") print(f" Username: {username}") print(f" Download ID: {download_id}") print(f" Base URL: {soulseek_client.base_url}") @@ -29468,7 +29468,7 @@ def cancel_task_v2(): real_download_id = None # Step 1: Always search for real download ID first - print(f"🔍 [Atomic Cancel] Searching slskd transfers for real download ID") + print(f"[Atomic Cancel] Searching slskd transfers for real download ID") try: all_transfers = run_async(soulseek_client._make_request('GET', 'transfers/downloads')) if all_transfers: @@ -29482,60 +29482,60 @@ def cancel_task_v2(): if (file_filename == download_id or __import__('os').path.basename(file_filename) == __import__('os').path.basename(str(download_id))): real_download_id = file_data.get('id') - print(f"🎯 [Atomic Cancel] Found real download ID: {real_download_id} for file: {file_filename}") + print(f"[Atomic Cancel] Found real download ID: {real_download_id} for file: {file_filename}") break if real_download_id: break if real_download_id: break except Exception as search_error: - print(f"⚠️ [Atomic Cancel] Error searching transfers: {search_error}") + print(f"[Atomic Cancel] Error searching transfers: {search_error}") # Step 2: Try cancellation with real ID if found if real_download_id: - print(f"🔄 [Atomic Cancel] Attempting cancel with real ID: {real_download_id}") + print(f"[Atomic Cancel] Attempting cancel with real ID: {real_download_id}") try: # Use EXACT format from slskd web UI: DELETE /api/v0/transfers/downloads/{username}/{download_id}?remove=false endpoint = f'transfers/downloads/{username}/{real_download_id}?remove=true' - print(f"🌐 [Atomic Cancel] Using slskd web UI format: {endpoint}") + print(f"[Atomic Cancel] Using slskd web UI format: {endpoint}") response = run_async(soulseek_client._make_request('DELETE', endpoint)) if response is not None: - print(f"✅ [Atomic Cancel] Successfully cancelled with slskd web UI format: {real_download_id}") + print(f"[Atomic Cancel] Successfully cancelled with slskd web UI format: {real_download_id}") success = True else: - print(f"⚠️ [Atomic Cancel] Web UI format failed, trying alternative formats") + print(f"[Atomic Cancel] Web UI format failed, trying alternative formats") # Fallback: Try without remove parameter endpoint2 = f'transfers/downloads/{username}/{real_download_id}' response2 = run_async(soulseek_client._make_request('DELETE', endpoint2)) if response2 is not None: - print(f"✅ [Atomic Cancel] Successfully cancelled without remove param: {real_download_id}") + print(f"[Atomic Cancel] Successfully cancelled without remove param: {real_download_id}") success = True else: # Final fallback: Try simple format (sync.py style) endpoint3 = f'transfers/downloads/{real_download_id}' response3 = run_async(soulseek_client._make_request('DELETE', endpoint3)) if response3 is not None: - print(f"✅ [Atomic Cancel] Successfully cancelled with simple format: {real_download_id}") + print(f"[Atomic Cancel] Successfully cancelled with simple format: {real_download_id}") success = True else: - print(f"⚠️ [Atomic Cancel] All DELETE formats failed for real ID: {real_download_id}") + print(f"[Atomic Cancel] All DELETE formats failed for real ID: {real_download_id}") except Exception as cancel_error: - print(f"⚠️ [Atomic Cancel] Exception cancelling real ID {real_download_id}: {cancel_error}") + print(f"[Atomic Cancel] Exception cancelling real ID {real_download_id}: {cancel_error}") else: - print(f"⚠️ [Atomic Cancel] Could not find real download ID in slskd transfers") - print(f"🔄 [Atomic Cancel] This might be a pending download not yet in slskd - relying on status='cancelled' to prevent it") + print(f"[Atomic Cancel] Could not find real download ID in slskd transfers") + print(f"[Atomic Cancel] This might be a pending download not yet in slskd - relying on status='cancelled' to prevent it") # For pending downloads, the status='cancelled' will prevent them from starting success = True # Consider this success since pending downloads are prevented if not success: - print(f"❌ [Atomic Cancel] Failed to cancel download in slskd API") + print(f"[Atomic Cancel] Failed to cancel download in slskd API") except Exception as e: - print(f"⚠️ [Atomic Cancel] Exception cancelling Soulseek download {download_id}: {e}") + print(f"[Atomic Cancel] Exception cancelling Soulseek download {download_id}: {e}") # Print more details about the error import traceback - print(f"⚠️ [Atomic Cancel] Cancel error traceback: {traceback.format_exc()}") + print(f"[Atomic Cancel] Cancel error traceback: {traceback.format_exc()}") else: print(f"ℹ️ [Atomic Cancel] No download_id or username available - skipping slskd cancel") @@ -29543,7 +29543,7 @@ def cancel_task_v2(): try: _add_cancelled_task_to_wishlist(task) except Exception as e: - print(f"⚠️ [Atomic Cancel] Warning: Could not add to wishlist: {e}") + print(f"[Atomic Cancel] Warning: Could not add to wishlist: {e}") return jsonify({ "success": True, @@ -29556,7 +29556,7 @@ def cancel_task_v2(): }) except Exception as e: - print(f"❌ [Cancel V2] Unexpected error: {e}") + print(f"[Cancel V2] Unexpected error: {e}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @@ -29571,7 +29571,7 @@ def _check_batch_completion_v2(batch_id): try: with tasks_lock: if batch_id not in download_batches: - print(f"⚠️ [Completion Check V2] Batch {batch_id} not found") + print(f"[Completion Check V2] Batch {batch_id} not found") return batch = download_batches[batch_id] @@ -29611,18 +29611,18 @@ def _check_batch_completion_v2(batch_id): finished_count += 1 else: # Task ID in queue but not in download_tasks - treat as completed to prevent blocking - print(f"⚠️ [Orphaned Task V2] Task {task_id} in queue but not in download_tasks - counting as finished") + print(f"[Orphaned Task V2] Task {task_id} in queue but not in download_tasks - counting as finished") finished_count += 1 all_tasks_truly_finished = finished_count >= len(queue) has_retrying_tasks = retrying_count > 0 - print(f"🔍 [Completion Check V2] Batch {batch_id}: tasks_started={all_tasks_started}, workers={no_active_workers}, finished={finished_count}/{len(queue)}, retrying={retrying_count}") + print(f"[Completion Check V2] Batch {batch_id}: tasks_started={all_tasks_started}, workers={no_active_workers}, finished={finished_count}/{len(queue)}, retrying={retrying_count}") if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks: # FIXED: Ensure batch is not already marked as complete to prevent duplicate processing if batch.get('phase') != 'complete': - print(f"🎉 [Completion Check V2] Batch {batch_id} is complete - marking as finished") + print(f"[Completion Check V2] Batch {batch_id} is complete - marking as finished") # Check if this is an auto-initiated batch is_auto_batch = batch.get('auto_initiated', False) @@ -29635,7 +29635,7 @@ def _check_batch_completion_v2(batch_id): playlist_name = batch.get('playlist_name', 'Unknown Playlist') failed_count = len(batch.get('permanently_failed_tracks', [])) successful_downloads = finished_count - failed_count - add_activity_item("✅", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now") + add_activity_item("", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now") # Emit batch_complete event for automation engine (only if something downloaded) if successful_downloads > 0: @@ -29650,7 +29650,7 @@ def _check_batch_completion_v2(batch_id): except Exception: pass else: - print(f"✅ [Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing") + print(f"[Completion Check V2] Batch {batch_id} already marked complete - skipping duplicate processing") return True # Already complete # Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist @@ -29659,30 +29659,30 @@ def _check_batch_completion_v2(batch_id): url_hash = playlist_id.replace('youtube_', '') if url_hash in youtube_playlist_states: youtube_playlist_states[url_hash]['phase'] = 'download_complete' - print(f"📋 [Completion Check V2] Updated YouTube playlist {url_hash} to download_complete phase") + print(f"[Completion Check V2] Updated YouTube playlist {url_hash} to download_complete phase") # Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist if playlist_id and playlist_id.startswith('tidal_'): tidal_playlist_id = playlist_id.replace('tidal_', '') if tidal_playlist_id in tidal_discovery_states: tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete' - print(f"📋 [Completion Check V2] Updated Tidal playlist {tidal_playlist_id} to download_complete phase") + print(f"[Completion Check V2] Updated Tidal playlist {tidal_playlist_id} to download_complete phase") # Update Deezer playlist phase to 'download_complete' if this is a Deezer playlist if playlist_id and playlist_id.startswith('deezer_'): deezer_playlist_id = playlist_id.replace('deezer_', '') if deezer_playlist_id in deezer_discovery_states: deezer_discovery_states[deezer_playlist_id]['phase'] = 'download_complete' - print(f"📋 [Completion Check V2] Updated Deezer playlist {deezer_playlist_id} to download_complete phase") + print(f"[Completion Check V2] Updated Deezer playlist {deezer_playlist_id} to download_complete phase") # Update Spotify Public playlist phase to 'download_complete' if this is a Spotify Public playlist if playlist_id and playlist_id.startswith('spotify_public_'): spotify_public_url_hash = playlist_id.replace('spotify_public_', '') if spotify_public_url_hash in spotify_public_discovery_states: spotify_public_discovery_states[spotify_public_url_hash]['phase'] = 'download_complete' - print(f"📋 [Completion Check V2] Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase") + print(f"[Completion Check V2] Updated Spotify Public playlist {spotify_public_url_hash} to download_complete phase") - print(f"🎉 [Completion Check V2] Batch {batch_id} complete - stopping monitor") + print(f"[Completion Check V2] Batch {batch_id} complete - stopping monitor") download_monitor.stop_monitoring(batch_id) # REPAIR: Scan all album folders from this batch for track number issues @@ -29693,21 +29693,21 @@ def _check_batch_completion_v2(batch_id): if all_tasks_started and no_active_workers and all_tasks_truly_finished and not has_retrying_tasks: # Call wishlist processing outside the lock if is_auto_batch: - print(f"🤖 [Completion Check V2] Processing auto-initiated batch completion") + print(f"[Completion Check V2] Processing auto-initiated batch completion") # Use the existing auto-completion function _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id) else: - print(f"📋 [Completion Check V2] Processing regular batch completion") + print(f"[Completion Check V2] Processing regular batch completion") # Use the regular completion function _process_failed_tracks_to_wishlist_exact(batch_id) return True # Batch was completed else: - print(f"📊 [Completion Check V2] Batch {batch_id} not yet complete: finished={finished_count}/{len(queue)}, retrying={retrying_count}, workers={batch['active_count']}") + print(f"[Completion Check V2] Batch {batch_id} not yet complete: finished={finished_count}/{len(queue)}, retrying={retrying_count}, workers={batch['active_count']}") return False # Batch still in progress except Exception as e: - print(f"❌ [Completion Check V2] Error checking batch completion: {e}") + print(f"[Completion Check V2] Error checking batch completion: {e}") import traceback traceback.print_exc() return False @@ -29783,12 +29783,12 @@ def _add_cancelled_task_to_wishlist(task): ) if success: - print(f"✅ [Atomic Cancel] Added '{track_info.get('name')}' to wishlist") + print(f"[Atomic Cancel] Added '{track_info.get('name')}' to wishlist") else: - print(f"❌ [Atomic Cancel] Failed to add '{track_info.get('name')}' to wishlist") + print(f"[Atomic Cancel] Failed to add '{track_info.get('name')}' to wishlist") except Exception as e: - print(f"❌ [Atomic Cancel] Critical error adding to wishlist: {e}") + print(f"[Atomic Cancel] Critical error adding to wishlist: {e}") @app.route('/api/playlists//cancel_batch', methods=['POST']) def cancel_batch(batch_id): @@ -29816,7 +29816,7 @@ def cancel_batch(batch_id): with wishlist_timer_lock: wishlist_auto_processing = False wishlist_auto_processing_timestamp = 0 - print(f"🔓 [Wishlist Cancel] Reset wishlist auto-processing flag for cancelled auto-batch") + print(f"[Wishlist Cancel] Reset wishlist auto-processing flag for cancelled auto-batch") else: print(f"ℹ️ [Wishlist Cancel] Manual wishlist batch cancelled (no flag reset needed)") @@ -29825,7 +29825,7 @@ def cancel_batch(batch_id): url_hash = playlist_id.replace('youtube_', '') if url_hash in youtube_playlist_states: youtube_playlist_states[url_hash]['phase'] = 'discovered' - print(f"📋 Reset YouTube playlist {url_hash} to discovered phase (batch cancelled)") + print(f"Reset YouTube playlist {url_hash} to discovered phase (batch cancelled)") # Cancel all individual tasks in the batch cancelled_count = 0 @@ -29838,13 +29838,13 @@ def cancel_batch(batch_id): # Add activity for batch cancellation playlist_name = download_batches[batch_id].get('playlist_name', 'Unknown Playlist') - add_activity_item("🚫", "Batch Cancelled", f"'{playlist_name}' - {cancelled_count} downloads cancelled", "Now") + add_activity_item("", "Batch Cancelled", f"'{playlist_name}' - {cancelled_count} downloads cancelled", "Now") - print(f"✅ Cancelled batch {batch_id} with {cancelled_count} tasks") + print(f"Cancelled batch {batch_id} with {cancelled_count} tasks") return jsonify({"success": True, "cancelled_tasks": cancelled_count}) except Exception as e: - print(f"❌ Error cancelling batch {batch_id}: {e}") + print(f"Error cancelling batch {batch_id}: {e}") return jsonify({"success": False, "error": str(e)}), 500 # NEW ENDPOINT: Add this function to web_server.py @@ -29869,7 +29869,7 @@ def cleanup_batch(): # This prevents a race condition where cleanup deletes the batch before # the wishlist processing thread can access it if batch.get('wishlist_processing_started') and not batch.get('wishlist_processing_complete'): - print(f"⏳ [Cleanup] Batch {batch_id} cleanup deferred - wishlist processing in progress") + print(f"[Cleanup] Batch {batch_id} cleanup deferred - wishlist processing in progress") return jsonify({ "success": False, "error": "Batch cleanup deferred - wishlist processing in progress", @@ -29887,15 +29887,15 @@ def cleanup_batch(): if task_id in download_tasks: del download_tasks[task_id] - print(f"✅ Cleaned up batch '{batch_id}' and its associated tasks from server state.") + print(f"Cleaned up batch '{batch_id}' and its associated tasks from server state.") return jsonify({"success": True, "message": f"Batch {batch_id} cleaned up."}) else: # It's not an error if the batch is already gone - print(f"⚠️ Cleanup requested for non-existent batch '{batch_id}'. Already cleaned up?") + print(f"Cleanup requested for non-existent batch '{batch_id}'. Already cleaned up?") return jsonify({"success": True, "message": "Batch already cleaned up."}) except Exception as e: - print(f"❌ Error during batch cleanup for '{batch_id}': {e}") + print(f"Error during batch cleanup for '{batch_id}': {e}") return jsonify({"success": False, "error": str(e)}), 500 # =============================== @@ -30738,12 +30738,12 @@ def start_missing_tracks_process(playlist_id): # Log album context if provided if is_album_download and album_context and artist_context: - print(f"🎵 [Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})") + print(f"[Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})") print(f" Release: {album_context.get('release_date', 'Unknown')}, Tracks: {album_context.get('total_tracks', len(tracks))}") # Log playlist folder mode if enabled if playlist_folder_mode: - print(f"📁 [Playlist Folder] Enabled for playlist: '{playlist_name}'") + print(f"[Playlist Folder] Enabled for playlist: '{playlist_name}'") # Limit concurrent analysis processes to prevent resource exhaustion with tasks_lock: @@ -30795,7 +30795,7 @@ def start_missing_tracks_process(playlist_id): youtube_playlist_states[url_hash]['download_process_id'] = batch_id youtube_playlist_states[url_hash]['phase'] = 'downloading' youtube_playlist_states[url_hash]['converted_spotify_playlist_id'] = playlist_id - print(f"🔗 Linked YouTube playlist {url_hash} to download process {batch_id} (converted ID: {playlist_id})") + print(f"Linked YouTube playlist {url_hash} to download process {batch_id} (converted ID: {playlist_id})") # Link Tidal playlist to download process if this is a Tidal playlist if playlist_id.startswith('tidal_'): @@ -30804,7 +30804,7 @@ def start_missing_tracks_process(playlist_id): tidal_discovery_states[tidal_playlist_id]['download_process_id'] = batch_id tidal_discovery_states[tidal_playlist_id]['phase'] = 'downloading' tidal_discovery_states[tidal_playlist_id]['converted_spotify_playlist_id'] = playlist_id - print(f"🔗 Linked Tidal playlist {tidal_playlist_id} to download process {batch_id} (converted ID: {playlist_id})") + print(f"Linked Tidal playlist {tidal_playlist_id} to download process {batch_id} (converted ID: {playlist_id})") # Link Spotify Public playlist to download process if this is a Spotify Public playlist if playlist_id.startswith('spotify_public_'): @@ -30813,7 +30813,7 @@ def start_missing_tracks_process(playlist_id): spotify_public_discovery_states[sp_url_hash]['download_process_id'] = batch_id spotify_public_discovery_states[sp_url_hash]['phase'] = 'downloading' spotify_public_discovery_states[sp_url_hash]['converted_spotify_playlist_id'] = playlist_id - print(f"🔗 Linked Spotify Public playlist {sp_url_hash} to download process {batch_id} (converted ID: {playlist_id})") + print(f"Linked Spotify Public playlist {sp_url_hash} to download process {batch_id} (converted ID: {playlist_id})") # Link Deezer playlist to download process if this is a Deezer playlist if playlist_id.startswith('deezer_'): @@ -30822,7 +30822,7 @@ def start_missing_tracks_process(playlist_id): deezer_discovery_states[deezer_playlist_id]['download_process_id'] = batch_id deezer_discovery_states[deezer_playlist_id]['phase'] = 'downloading' deezer_discovery_states[deezer_playlist_id]['converted_spotify_playlist_id'] = playlist_id - print(f"🔗 Linked Deezer playlist {deezer_playlist_id} to download process {batch_id} (converted ID: {playlist_id})") + print(f"Linked Deezer playlist {deezer_playlist_id} to download process {batch_id} (converted ID: {playlist_id})") # Stamp original index to keep task indices aligned with frontend row order for i, track in enumerate(tracks): @@ -30892,7 +30892,7 @@ def start_missing_downloads(): return jsonify({"success": True, "batch_id": batch_id, "message": f"Queued {len(missing_tracks)} downloads for processing."}) except Exception as e: - print(f"❌ Error starting missing downloads: {e}") + print(f"Error starting missing downloads: {e}") return jsonify({"success": False, "error": str(e)}), 500 # =============================== @@ -30909,7 +30909,7 @@ def _load_sync_status_file(): return data return {} except Exception as e: - print(f"❌ Error loading sync status: {e}") + print(f"Error loading sync status: {e}") return {} def _save_sync_status_file(sync_statuses): @@ -30918,7 +30918,7 @@ def _save_sync_status_file(sync_statuses): database = get_database() database.set_preference('sync_statuses', json.dumps(sync_statuses)) except Exception as e: - print(f"❌ Error saving sync status: {e}") + print(f"Error saving sync status: {e}") def _update_and_save_sync_status(playlist_id, playlist_name, playlist_owner, snapshot_id, **kwargs): """Updates the sync status for a given playlist and saves to file (same logic as GUI).""" @@ -30943,10 +30943,10 @@ def _update_and_save_sync_status(playlist_id, playlist_name, playlist_owner, sna # Save to file _save_sync_status_file(sync_statuses) - print(f"🔄 Updated sync status for playlist '{playlist_name}' (ID: {playlist_id})") + print(f"Updated sync status for playlist '{playlist_name}' (ID: {playlist_id})") except Exception as e: - print(f"❌ Error updating sync status for {playlist_id}: {e}") + print(f"Error updating sync status for {playlist_id}: {e}") @app.route('/api/spotify/playlists', methods=['GET']) def get_spotify_playlists(): @@ -30967,7 +30967,7 @@ def get_spotify_playlists(): # Handle snapshot_id safely - may not exist in core Playlist class playlist_snapshot = getattr(p, 'snapshot_id', '') - print(f"🔍 Processing playlist: {p.name} (ID: {p.id})") + print(f"Processing playlist: {p.name} (ID: {p.id})") print(f" - Playlist snapshot: '{playlist_snapshot}'") print(f" - Status info: {status_info}") @@ -31020,9 +31020,9 @@ def get_spotify_playlists(): "sync_status": sync_status, "snapshot_id": "" # Liked Songs doesn't have a snapshot_id }) - print(f"🔍 Added virtual 'Liked Songs' playlist with {liked_songs_count} tracks (count only)") + print(f"Added virtual 'Liked Songs' playlist with {liked_songs_count} tracks (count only)") except Exception as liked_error: - print(f"⚠️ Failed to add Liked Songs playlist: {liked_error}") + print(f"Failed to add Liked Songs playlist: {liked_error}") # Don't fail the entire request if Liked Songs fails return jsonify(playlist_data) @@ -31331,7 +31331,7 @@ def search_spotify(): return jsonify({'tracks': {'items': tracks_items}}) except Exception as e: - print(f"❌ Error searching Spotify: {e}") + print(f"Error searching Spotify: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify/search_tracks', methods=['GET']) @@ -31380,7 +31380,7 @@ def search_spotify_tracks(): return jsonify({'tracks': tracks_dict}) except Exception as e: - print(f"❌ Error searching Spotify tracks: {e}") + print(f"Error searching Spotify tracks: {e}") return jsonify({"error": str(e)}), 500 @@ -31430,7 +31430,7 @@ def search_itunes_tracks(): return jsonify({'tracks': tracks_dict}) except Exception as e: - print(f"❌ Error searching iTunes tracks: {e}") + print(f"Error searching iTunes tracks: {e}") return jsonify({"error": str(e)}), 500 @@ -31472,7 +31472,7 @@ def search_deezer_tracks(): return jsonify({'tracks': tracks_dict}) except Exception as e: - print(f"❌ Error searching Deezer tracks: {e}") + print(f"Error searching Deezer tracks: {e}") return jsonify({"error": str(e)}), 500 @@ -32046,7 +32046,7 @@ def get_tidal_playlists(): playlist_data.append(playlist_dict) - print(f"🎵 Loaded {len(playlist_data)} Tidal playlists with track data") + print(f"Loaded {len(playlist_data)} Tidal playlists with track data") return jsonify(playlist_data) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -32057,7 +32057,7 @@ def get_tidal_playlist_tracks(playlist_id): if not tidal_client or not tidal_client.is_authenticated(): return jsonify({"error": "Tidal not authenticated."}), 401 try: - print(f"🎵 Getting full Tidal playlist with tracks for: {playlist_id}") + print(f"Getting full Tidal playlist with tracks for: {playlist_id}") # Fetch this single playlist directly — no need to re-fetch all playlists full_playlist = tidal_client.get_playlist(playlist_id) @@ -32067,7 +32067,7 @@ def get_tidal_playlist_tracks(playlist_id): if not full_playlist.tracks: return jsonify({"error": "This playlist appears to have no tracks or they cannot be accessed"}), 403 - print(f"🎵 Loaded {len(full_playlist.tracks)} tracks from Tidal playlist: {full_playlist.name}") + print(f"Loaded {len(full_playlist.tracks)} tracks from Tidal playlist: {full_playlist.name}") # Convert playlist to dict (matches sync.py structure) playlist_dict = { @@ -32092,7 +32092,7 @@ def get_tidal_playlist_tracks(playlist_id): return jsonify(playlist_dict) except Exception as e: - print(f"❌ Error getting Tidal playlist tracks: {e}") + print(f"Error getting Tidal playlist tracks: {e}") return jsonify({"error": str(e)}), 500 @@ -32152,18 +32152,18 @@ def start_tidal_discovery(playlist_id): tidal_discovery_states[playlist_id] = state # Add activity for discovery start - add_activity_item("🔍", "Tidal Discovery Started", f"'{target_playlist.name}' - {len(target_playlist.tracks)} tracks", "Now") + add_activity_item("", "Tidal Discovery Started", f"'{target_playlist.name}' - {len(target_playlist.tracks)} tracks", "Now") # Start discovery worker (capture profile ID while we have Flask context) state['_profile_id'] = get_current_profile_id() future = tidal_discovery_executor.submit(_run_tidal_discovery_worker, playlist_id) state['discovery_future'] = future - print(f"🔍 Started Spotify discovery for Tidal playlist: {target_playlist.name}") + print(f"Started Spotify discovery for Tidal playlist: {target_playlist.name}") return jsonify({"success": True, "message": "Discovery started"}) except Exception as e: - print(f"❌ Error starting Tidal discovery: {e}") + print(f"Error starting Tidal discovery: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/discovery/status/', methods=['GET']) @@ -32189,7 +32189,7 @@ def get_tidal_discovery_status(playlist_id): return jsonify(response) except Exception as e: - print(f"❌ Error getting Tidal discovery status: {e}") + print(f"Error getting Tidal discovery status: {e}") return jsonify({"error": str(e)}), 500 @@ -32219,7 +32219,7 @@ def update_tidal_discovery_match(): old_status = result.get('status') # Update with user-selected track - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = spotify_track['name'] result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists']) @@ -32247,10 +32247,10 @@ def update_tidal_discovery_match(): result['manual_match'] = True # Flag for tracking # Update match count if status changed from not found/error - if old_status != 'found' and old_status != '✅ Found': + if old_status != 'found' and old_status != 'Found': state['spotify_matches'] = state.get('spotify_matches', 0) + 1 - print(f"✅ Manual match updated: tidal - {identifier} - track {track_index}") + print(f"Manual match updated: tidal - {identifier} - track {track_index}") print(f" → {result['spotify_artist']} - {result['spotify_track']}") # Save manual fix to discovery cache so it appears in discovery pool @@ -32283,14 +32283,14 @@ def update_tidal_discovery_match(): cache_key[0], cache_key[1], 'spotify', 1.0, matched_data, original_name, original_artist ) - print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}") + print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}") except Exception as cache_err: - print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}") + print(f"Error saving manual fix to discovery cache: {cache_err}") return jsonify({'success': True, 'result': result}) except Exception as e: - print(f"❌ Error updating Tidal discovery match: {e}") + print(f"Error updating Tidal discovery match: {e}") return jsonify({'error': str(e)}), 500 @@ -32320,11 +32320,11 @@ def get_tidal_playlist_states(): } states.append(state_info) - print(f"🎵 Returning {len(states)} stored Tidal playlist states for hydration") + print(f"Returning {len(states)} stored Tidal playlist states for hydration") return jsonify({"states": states}) except Exception as e: - print(f"❌ Error getting Tidal playlist states: {e}") + print(f"Error getting Tidal playlist states: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/state/', methods=['GET']) @@ -32357,7 +32357,7 @@ def get_tidal_playlist_state(playlist_id): return jsonify(response) except Exception as e: - print(f"❌ Error getting Tidal playlist state: {e}") + print(f"Error getting Tidal playlist state: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/reset/', methods=['POST']) @@ -32386,11 +32386,11 @@ def reset_tidal_playlist(playlist_id): state['discovery_future'] = None state['last_accessed'] = time.time() - print(f"🔄 Reset Tidal playlist to fresh: {playlist_id}") + print(f"Reset Tidal playlist to fresh: {playlist_id}") return jsonify({"success": True, "message": "Playlist reset to fresh phase"}) except Exception as e: - print(f"❌ Error resetting Tidal playlist: {e}") + print(f"Error resetting Tidal playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/delete/', methods=['POST']) @@ -32409,11 +32409,11 @@ def delete_tidal_playlist(playlist_id): # Remove from state dictionary del tidal_discovery_states[playlist_id] - print(f"🗑️ Deleted Tidal playlist state: {playlist_id}") + print(f"Deleted Tidal playlist state: {playlist_id}") return jsonify({"success": True, "message": "Playlist deleted"}) except Exception as e: - print(f"❌ Error deleting Tidal playlist: {e}") + print(f"Error deleting Tidal playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/update_phase/', methods=['POST']) @@ -32438,11 +32438,11 @@ def update_tidal_playlist_phase(playlist_id): state['phase'] = new_phase state['last_accessed'] = time.time() - print(f"🔄 Updated Tidal playlist {playlist_id} phase: {old_phase} → {new_phase}") + print(f"Updated Tidal playlist {playlist_id} phase: {old_phase} → {new_phase}") return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase}) except Exception as e: - print(f"❌ Error updating Tidal playlist phase: {e}") + print(f"Error updating Tidal playlist phase: {e}") return jsonify({"error": str(e)}), 500 @@ -32463,7 +32463,7 @@ def _pause_enrichment_workers(label='discovery'): if worker and not worker.paused: worker.pause() was_running[name] = True - print(f"⏸️ Paused {name} enrichment worker during {label}") + print(f"Paused {name} enrichment worker during {label}") except Exception: pass return was_running @@ -32481,7 +32481,7 @@ def _resume_enrichment_workers(was_running, label='discovery'): try: if was_running.get(name) and worker: worker.resume() - print(f"▶️ Resumed {name} enrichment worker after {label}") + print(f"Resumed {name} enrichment worker after {label}") except Exception: pass @@ -32500,10 +32500,10 @@ def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discove break if not mirrored_pl: - print(f"⚠️ [Discovery Sync] No mirrored playlist found for {source_type}:{source_playlist_id} (profile {profile_id})") + print(f"[Discovery Sync] No mirrored playlist found for {source_type}:{source_playlist_id} (profile {profile_id})") return - print(f"📝 [Discovery Sync] Found mirrored playlist '{mirrored_pl.get('name')}' (DB id={mirrored_pl['id']}) for {source_type}:{source_playlist_id}") + print(f"[Discovery Sync] Found mirrored playlist '{mirrored_pl.get('name')}' (DB id={mirrored_pl['id']}) for {source_type}:{source_playlist_id}") mirrored_tracks = db.get_mirrored_playlist_tracks(mirrored_pl['id']) if not mirrored_tracks: return @@ -32558,11 +32558,11 @@ def _sync_discovery_results_to_mirrored(source_type, source_playlist_id, discove updated += 1 if updated > 0: - print(f"📝 Synced {updated} discovery results back to mirrored playlist '{mirrored_pl.get('name', '')}'") + print(f"Synced {updated} discovery results back to mirrored playlist '{mirrored_pl.get('name', '')}'") except Exception as e: import traceback - print(f"⚠️ Failed to sync discovery results to mirrored playlist: {e}") + print(f"Failed to sync discovery results to mirrored playlist: {e}") traceback.print_exc() @@ -32580,7 +32580,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): try: itunes_client_instance = _get_metadata_fallback_client() except Exception: - print(f"❌ Neither Spotify nor {_get_metadata_fallback_source()} available for discovery") + print(f"Neither Spotify nor {_get_metadata_fallback_source()} available for discovery") _update_automation_progress(automation_id, status='error', progress=100, phase='Error', log_line=f'Neither Spotify nor {_get_metadata_fallback_source()} available', log_type='error') @@ -32612,7 +32612,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): if not tracks: continue - print(f"🔍 Starting discovery for playlist '{pl_name}' ({len(tracks)} tracks, using {discovery_source.upper()})") + print(f"Starting discovery for playlist '{pl_name}' ({len(tracks)} tracks, using {discovery_source.upper()})") _update_automation_progress(automation_id, phase=f'Discovering: "{pl_name}"', log_line=f'Playlist "{pl_name}" — {len(tracks)} tracks ({discovery_source.upper()})', log_type='info') @@ -32661,7 +32661,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): # Check for cancellation if automation_id and automation_id in _playlist_discovery_cancelled: _playlist_discovery_cancelled.discard(automation_id) - print(f"🛑 Playlist discovery cancelled (automation {automation_id})") + print(f"Playlist discovery cancelled (automation {automation_id})") _update_automation_progress(automation_id, status='finished', progress=100, phase='Discovery cancelled', log_line=f'Cancelled: {total_discovered} discovered, {total_failed} failed', @@ -32687,7 +32687,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): } db.update_mirrored_track_extra_data(track_id, extra_data) total_discovered += 1 - print(f"⚡ CACHE [{i+1}/{len(undiscovered_tracks)}]: {track_name} → {cached_match.get('name', '?')}") + print(f"CACHE [{i+1}/{len(undiscovered_tracks)}]: {track_name} → {cached_match.get('name', '?')}") _update_automation_progress(automation_id, progress=((total_skipped + total_discovered + total_failed) / max(1, grand_total)) * 100, current_item=track_name, @@ -32822,7 +32822,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): except Exception: pass - print(f"✅ [{i+1}/{len(undiscovered_tracks)}] {track_name} → {matched_data['name']} ({best_confidence:.2f})") + print(f"[{i+1}/{len(undiscovered_tracks)}] {track_name} → {matched_data['name']} ({best_confidence:.2f})") _update_automation_progress(automation_id, progress=((total_skipped + total_discovered + total_failed) / max(1, grand_total)) * 100, processed=total_discovered + total_failed, @@ -32836,7 +32836,7 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): } db.update_mirrored_track_extra_data(track_id, extra_data) total_failed += 1 - print(f"❌ [{i+1}/{len(undiscovered_tracks)}] No match: {track_name} by {artist_name}") + print(f"[{i+1}/{len(undiscovered_tracks)}] No match: {track_name} by {artist_name}") _update_automation_progress(automation_id, progress=((total_skipped + total_discovered + total_failed) / max(1, grand_total)) * 100, processed=total_discovered + total_failed, @@ -32861,14 +32861,14 @@ def _run_playlist_discovery_worker(playlists, automation_id=None): except Exception: pass - print(f"✅ Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped") + print(f"Playlist discovery complete: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped") _update_automation_progress(automation_id, status='finished', progress=100, phase='Discovery complete', log_line=f'Done: {total_discovered} discovered, {total_failed} failed, {total_skipped} skipped', log_type='success') except Exception as e: - print(f"❌ Error in playlist discovery worker: {e}") + print(f"Error in playlist discovery worker: {e}") import traceback traceback.print_exc() _update_automation_progress(automation_id, status='error', progress=100, @@ -32929,7 +32929,7 @@ def _validate_discovery_cache_artist(source_artist, cached_match): best_sim = sim if best_sim < min_artist_similarity: - print(f"🚫 Cache artist mismatch: source='{source_artist}' vs cached='{cached_artists[0]}' (sim={best_sim:.2f}), re-searching") + print(f"Cache artist mismatch: source='{source_artist}' vs cached='{cached_artists[0]}' (sim={best_sim:.2f}), re-searching") return False return True @@ -33016,7 +33016,7 @@ def _discovery_score_candidates(source_title, source_artist, source_duration_ms, best_index = idx except Exception as e: - print(f"⚠️ Error scoring candidate {idx}: {e}") + print(f"Error scoring candidate {idx}: {e}") continue return best_match, best_confidence, best_index @@ -33039,7 +33039,7 @@ def _run_tidal_discovery_worker(playlist_id): if not use_spotify: itunes_client_instance = _get_metadata_fallback_client() - print(f"🎵 Starting Tidal discovery for: {playlist.name} (using {discovery_source.upper()})") + print(f"Starting Tidal discovery for: {playlist.name} (using {discovery_source.upper()})") # Store discovery source in state for frontend state['discovery_source'] = discovery_source @@ -33051,7 +33051,7 @@ def _run_tidal_discovery_worker(playlist_id): break try: - print(f"🔍 [{i+1}/{len(playlist.tracks)}] Searching {discovery_source.upper()}: {tidal_track.name} by {', '.join(tidal_track.artists)}") + print(f"[{i+1}/{len(playlist.tracks)}] Searching {discovery_source.upper()}: {tidal_track.name} by {', '.join(tidal_track.artists)}") # Check discovery cache first cache_key = _get_discovery_cache_key(tidal_track.name, tidal_track.artists[0] if tidal_track.artists else '') @@ -33059,7 +33059,7 @@ def _run_tidal_discovery_worker(playlist_id): cache_db = get_database() cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source) if cached_match and _validate_discovery_cache_artist(tidal_track.artists[0] if tidal_track.artists else '', cached_match): - print(f"⚡ CACHE HIT [{i+1}/{len(playlist.tracks)}]: {tidal_track.name} by {', '.join(tidal_track.artists)}") + print(f"CACHE HIT [{i+1}/{len(playlist.tracks)}]: {tidal_track.name} by {', '.join(tidal_track.artists)}") result = { 'tidal_track': { 'id': tidal_track.id, @@ -33079,7 +33079,7 @@ def _run_tidal_discovery_worker(playlist_id): state['discovery_progress'] = int(((i + 1) / len(playlist.tracks)) * 100) continue except Exception as cache_err: - print(f"⚠️ Cache lookup error: {cache_err}") + print(f"Cache lookup error: {cache_err}") # Use the search function with appropriate provider track_result = _search_spotify_for_tidal_track( @@ -33169,9 +33169,9 @@ def _run_tidal_discovery_worker(playlist_id): result['match_data'], tidal_track.name, tidal_track.artists[0] if tidal_track.artists else '' ) - print(f"💾 CACHE SAVED: {tidal_track.name} (confidence: {match_confidence:.3f})") + print(f"CACHE SAVED: {tidal_track.name} (confidence: {match_confidence:.3f})") except Exception as cache_err: - print(f"⚠️ Cache save error: {cache_err}") + print(f"Cache save error: {cache_err}") state['discovery_results'].append(result) state['discovery_progress'] = int(((i + 1) / len(playlist.tracks)) * 100) @@ -33180,7 +33180,7 @@ def _run_tidal_discovery_worker(playlist_id): time.sleep(0.1) except Exception as e: - print(f"❌ Error processing track {i+1}: {e}") + print(f"Error processing track {i+1}: {e}") # Add error result result = { 'tidal_track': { @@ -33203,15 +33203,15 @@ def _run_tidal_discovery_worker(playlist_id): # Add activity for discovery completion source_label = discovery_source.upper() - add_activity_item("✅", f"Tidal Discovery Complete ({source_label})", f"'{playlist.name}' - {successful_discoveries}/{len(playlist.tracks)} tracks found", "Now") + add_activity_item("", f"Tidal Discovery Complete ({source_label})", f"'{playlist.name}' - {successful_discoveries}/{len(playlist.tracks)} tracks found", "Now") - print(f"✅ Tidal discovery complete ({source_label}): {successful_discoveries}/{len(playlist.tracks)} tracks found") + print(f"Tidal discovery complete ({source_label}): {successful_discoveries}/{len(playlist.tracks)} tracks found") # Sync discovery results back to mirrored playlist _sync_discovery_results_to_mirrored('tidal', playlist_id, state.get('discovery_results', []), discovery_source, profile_id=state.get('_profile_id', 1)) except Exception as e: - print(f"❌ Error in Tidal discovery worker: {e}") + print(f"Error in Tidal discovery worker: {e}") state['phase'] = 'error' state['status'] = f'error: {str(e)}' finally: @@ -33249,7 +33249,7 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client source_duration = getattr(tidal_track, 'duration_ms', 0) or 0 source_name = "Spotify" if use_spotify else _get_metadata_fallback_source().capitalize() - print(f"🔍 Tidal track: '{artist_name}' - '{track_name}' (searching {source_name})") + print(f"Tidal track: '{artist_name}' - '{track_name}' (searching {source_name})") # Use matching engine to generate search queries (with fallback) try: @@ -33259,9 +33259,9 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client 'album': None })() search_queries = matching_engine.generate_download_queries(temp_track) - print(f"🔍 Generated {len(search_queries)} search queries for Tidal track") + print(f"Generated {len(search_queries)} search queries for Tidal track") except Exception as e: - print(f"⚠️ Matching engine failed for Tidal, falling back to basic queries: {e}") + print(f"Matching engine failed for Tidal, falling back to basic queries: {e}") if use_spotify: search_queries = [ f'track:"{track_name}" artist:"{artist_name}"', @@ -33282,7 +33282,7 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client for query_idx, search_query in enumerate(search_queries): try: - print(f"🔍 Tidal query {query_idx + 1}/{len(search_queries)}: {search_query} ({source_name})") + print(f"Tidal query {query_idx + 1}/{len(search_queries)}: {search_query} ({source_name})") if use_spotify and not _spotify_rate_limited(): results = spotify_client.search_tracks(search_query, limit=10) @@ -33306,19 +33306,19 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client best_match_raw = _cache.get_entity('spotify', 'track', match.id) else: best_match_raw = None - print(f"✅ New best Tidal match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"New best Tidal match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") if best_confidence >= 0.9: - print(f"🎯 High confidence Tidal match found ({best_confidence:.3f}), stopping search") + print(f"High confidence Tidal match found ({best_confidence:.3f}), stopping search") break except Exception as e: - print(f"❌ Error in Tidal {source_name} search for query '{search_query}': {e}") + print(f"Error in Tidal {source_name} search for query '{search_query}': {e}") continue # Strategy 4: Extended search with higher limit (last resort) if not best_match: - print(f"🔄 Tidal Strategy 4: Extended search with limit=50") + print(f"Tidal Strategy 4: Extended search with limit=50") query = f"{artist_name} {track_name}" if use_spotify: extended_results = spotify_client.search_tracks(query, limit=50) @@ -33331,17 +33331,17 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client if match and confidence >= min_confidence: best_match = match best_confidence = confidence - print(f"✅ Strategy 4 Tidal match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 4 Tidal match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") if best_match: if use_spotify: - print(f"✅ Final Tidal Spotify match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})") + print(f"Final Tidal Spotify match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})") return (best_match, best_match_raw, best_confidence) else: result_artists = best_match.artists if hasattr(best_match, 'artists') else [] result_artist = result_artists[0] if result_artists else 'Unknown' result_name = best_match.name if hasattr(best_match, 'name') else 'Unknown' - print(f"✅ Final Tidal {source_name} match: {result_artist} - {result_name} (confidence: {best_confidence:.3f})") + print(f"Final Tidal {source_name} match: {result_artist} - {result_name} (confidence: {best_confidence:.3f})") album_name = best_match.album if hasattr(best_match, 'album') else 'Unknown Album' image_url = best_match.image_url if hasattr(best_match, 'image_url') else '' @@ -33378,11 +33378,11 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client if detailed: track_number = detailed.get('track_number') disc_number = detailed.get('disc_number') - print(f"🔢 [Discovery Enrich] {result_name}: track_number={track_number}, disc={disc_number}") + print(f"[Discovery Enrich] {result_name}: track_number={track_number}, disc={disc_number}") else: - print(f"⚠️ [Discovery Enrich] get_track_details returned None for ID {track_id} ({result_name})") + print(f"[Discovery Enrich] get_track_details returned None for ID {track_id} ({result_name})") except Exception as _enrich_err: - print(f"⚠️ [Discovery Enrich] Failed for {result_name} (ID {track_id}): {_enrich_err}") + print(f"[Discovery Enrich] Failed for {result_name} (ID {track_id}): {_enrich_err}") result_data = { 'id': track_id, @@ -33399,11 +33399,11 @@ def _search_spotify_for_tidal_track(tidal_track, use_spotify=True, itunes_client result_data['disc_number'] = disc_number return result_data else: - print(f"❌ No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})") + print(f"No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})") return None except Exception as e: - print(f"❌ Error searching Spotify for Tidal track: {e}") + print(f"Error searching Spotify for Tidal track: {e}") return None @@ -33440,7 +33440,7 @@ def convert_tidal_results_to_spotify_tracks(discovery_results): } spotify_tracks.append(track) - print(f"🔄 Converted {len(spotify_tracks)} Tidal matches to Spotify tracks for sync") + print(f"Converted {len(spotify_tracks)} Tidal matches to Spotify tracks for sync") return spotify_tracks @@ -33472,7 +33472,7 @@ def start_tidal_sync(playlist_id): playlist_name = state['playlist'].name # Tidal playlist object has .name attribute # Add activity for sync start - add_activity_item("🔄", "Tidal Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") + add_activity_item("", "Tidal Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") # Update Tidal state state['phase'] = 'syncing' @@ -33493,11 +33493,11 @@ def start_tidal_sync(playlist_id): future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) active_sync_workers[sync_playlist_id] = future - print(f"🔄 Started Tidal sync for: {playlist_name} ({len(spotify_tracks)} tracks)") + print(f"Started Tidal sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) except Exception as e: - print(f"❌ Error starting Tidal sync: {e}") + print(f"Error starting Tidal sync: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/sync/status/', methods=['GET']) @@ -33533,17 +33533,17 @@ def get_tidal_sync_status(playlist_id): # Add activity for sync completion playlist = state.get('playlist') playlist_name = playlist.name if playlist and hasattr(playlist, 'name') else 'Unknown Playlist' - add_activity_item("🔄", "Sync Complete", f"Tidal playlist '{playlist_name}' synced successfully", "Now") + add_activity_item("", "Sync Complete", f"Tidal playlist '{playlist_name}' synced successfully", "Now") elif sync_state.get('status') == 'error': state['phase'] = 'discovered' # Revert on error playlist = state.get('playlist') playlist_name = playlist.name if playlist and hasattr(playlist, 'name') else 'Unknown Playlist' - add_activity_item("❌", "Sync Failed", f"Tidal playlist '{playlist_name}' sync failed", "Now") + add_activity_item("", "Sync Failed", f"Tidal playlist '{playlist_name}' sync failed", "Now") return jsonify(response) except Exception as e: - print(f"❌ Error getting Tidal sync status: {e}") + print(f"Error getting Tidal sync status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/tidal/sync/cancel/', methods=['POST']) @@ -33574,7 +33574,7 @@ def cancel_tidal_sync(playlist_id): return jsonify({"success": True, "message": "Tidal sync cancelled"}) except Exception as e: - print(f"❌ Error cancelling Tidal sync: {e}") + print(f"Error cancelling Tidal sync: {e}") return jsonify({"error": str(e)}), 500 @@ -33675,7 +33675,7 @@ def get_deezer_arl_playlists(): 'sync_status': 'Never Synced', }) - print(f"🎵 Loaded {len(playlist_data)} Deezer user playlists via ARL") + print(f"Loaded {len(playlist_data)} Deezer user playlists via ARL") return jsonify(playlist_data) except Exception as e: return jsonify({'error': str(e)}), 500 @@ -33695,7 +33695,7 @@ def get_deezer_arl_playlist_tracks(playlist_id): if not playlist: return jsonify({'error': 'Playlist not found or unable to access.'}), 404 - print(f"🎵 Loaded {len(playlist.get('tracks', []))} tracks from Deezer playlist: {playlist.get('name')}") + print(f"Loaded {len(playlist.get('tracks', []))} tracks from Deezer playlist: {playlist.get('name')}") return jsonify(playlist) except Exception as e: return jsonify({'error': str(e)}), 500 @@ -33721,7 +33721,7 @@ def get_deezer_playlist(playlist_id): return jsonify(playlist) except Exception as e: - print(f"❌ Error fetching Deezer playlist: {e}") + print(f"Error fetching Deezer playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/discovery/start/', methods=['POST']) @@ -33787,18 +33787,18 @@ def start_deezer_discovery(playlist_id): # Add activity for discovery start playlist_name = state['playlist']['name'] track_count = len(state['playlist']['tracks']) - add_activity_item("🔍", "Deezer Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") + add_activity_item("", "Deezer Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") # Start discovery worker (capture profile ID while we have Flask context) deezer_discovery_states[playlist_id]['_profile_id'] = get_current_profile_id() future = deezer_discovery_executor.submit(_run_deezer_discovery_worker, playlist_id) state['discovery_future'] = future - print(f"🔍 Started Spotify discovery for Deezer playlist: {playlist_name}") + print(f"Started Spotify discovery for Deezer playlist: {playlist_name}") return jsonify({"success": True, "message": "Discovery started"}) except Exception as e: - print(f"❌ Error starting Deezer discovery: {e}") + print(f"Error starting Deezer discovery: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/discovery/status/', methods=['GET']) @@ -33824,7 +33824,7 @@ def get_deezer_discovery_status(playlist_id): return jsonify(response) except Exception as e: - print(f"❌ Error getting Deezer discovery status: {e}") + print(f"Error getting Deezer discovery status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/discovery/update_match', methods=['POST']) @@ -33853,7 +33853,7 @@ def update_deezer_discovery_match(): old_status = result.get('status') # Update with user-selected track - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = spotify_track['name'] result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists']) @@ -33881,10 +33881,10 @@ def update_deezer_discovery_match(): result['manual_match'] = True # Update match count if status changed from not found/error - if old_status != 'found' and old_status != '✅ Found': + if old_status != 'found' and old_status != 'Found': state['spotify_matches'] = state.get('spotify_matches', 0) + 1 - print(f"✅ Manual match updated: deezer - {identifier} - track {track_index}") + print(f"Manual match updated: deezer - {identifier} - track {track_index}") print(f" → {result['spotify_artist']} - {result['spotify_track']}") # Save manual fix to discovery cache so it appears in discovery pool @@ -33915,14 +33915,14 @@ def update_deezer_discovery_match(): cache_key[0], cache_key[1], 'spotify', 1.0, matched_data, original_name, original_artist ) - print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}") + print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}") except Exception as cache_err: - print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}") + print(f"Error saving manual fix to discovery cache: {cache_err}") return jsonify({'success': True, 'result': result}) except Exception as e: - print(f"❌ Error updating Deezer discovery match: {e}") + print(f"Error updating Deezer discovery match: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/deezer/playlists/states', methods=['GET']) @@ -33949,11 +33949,11 @@ def get_deezer_playlist_states(): } states.append(state_info) - print(f"🎵 Returning {len(states)} stored Deezer playlist states for hydration") + print(f"Returning {len(states)} stored Deezer playlist states for hydration") return jsonify({"states": states}) except Exception as e: - print(f"❌ Error getting Deezer playlist states: {e}") + print(f"Error getting Deezer playlist states: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/state/', methods=['GET']) @@ -33986,7 +33986,7 @@ def get_deezer_playlist_state(playlist_id): return jsonify(response) except Exception as e: - print(f"❌ Error getting Deezer playlist state: {e}") + print(f"Error getting Deezer playlist state: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/reset/', methods=['POST']) @@ -34015,11 +34015,11 @@ def reset_deezer_playlist(playlist_id): state['discovery_future'] = None state['last_accessed'] = time.time() - print(f"🔄 Reset Deezer playlist to fresh: {playlist_id}") + print(f"Reset Deezer playlist to fresh: {playlist_id}") return jsonify({"success": True, "message": "Playlist reset to fresh phase"}) except Exception as e: - print(f"❌ Error resetting Deezer playlist: {e}") + print(f"Error resetting Deezer playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/delete/', methods=['POST']) @@ -34038,11 +34038,11 @@ def delete_deezer_playlist(playlist_id): # Remove from state dictionary del deezer_discovery_states[playlist_id] - print(f"🗑️ Deleted Deezer playlist state: {playlist_id}") + print(f"Deleted Deezer playlist state: {playlist_id}") return jsonify({"success": True, "message": "Playlist deleted"}) except Exception as e: - print(f"❌ Error deleting Deezer playlist: {e}") + print(f"Error deleting Deezer playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/update_phase/', methods=['POST']) @@ -34075,11 +34075,11 @@ def update_deezer_playlist_phase(playlist_id): if 'converted_spotify_playlist_id' in data: state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id'] - print(f"🔄 Updated Deezer playlist {playlist_id} phase: {old_phase} → {new_phase}") + print(f"Updated Deezer playlist {playlist_id} phase: {old_phase} → {new_phase}") return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase}) except Exception as e: - print(f"❌ Error updating Deezer playlist phase: {e}") + print(f"Error updating Deezer playlist phase: {e}") return jsonify({"error": str(e)}), 500 @@ -34100,7 +34100,7 @@ def _run_deezer_discovery_worker(playlist_id): if not use_spotify: itunes_client_instance = _get_metadata_fallback_client() - print(f"🎵 Starting Deezer discovery for: {playlist['name']} (using {discovery_source.upper()})") + print(f"Starting Deezer discovery for: {playlist['name']} (using {discovery_source.upper()})") # Store discovery source in state for frontend state['discovery_source'] = discovery_source @@ -34119,7 +34119,7 @@ def _run_deezer_discovery_worker(playlist_id): track_album = deezer_track.get('album', '') track_duration_ms = deezer_track.get('duration_ms', 0) - print(f"🔍 [{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}") + print(f"[{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}") # Check discovery cache first cache_key = _get_discovery_cache_key(track_name, track_artists[0] if track_artists else '') @@ -34127,7 +34127,7 @@ def _run_deezer_discovery_worker(playlist_id): cache_db = get_database() cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source) if cached_match and _validate_discovery_cache_artist(track_artists[0] if track_artists else '', cached_match): - print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}") + print(f"CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}") # Extract display-friendly artist string from cached match cached_artists = cached_match.get('artists', []) if cached_artists: @@ -34150,7 +34150,7 @@ def _run_deezer_discovery_worker(playlist_id): }, 'spotify_data': cached_match, 'match_data': cached_match, - 'status': '✅ Found', + 'status': 'Found', 'status_class': 'found', 'spotify_track': cached_match.get('name', ''), 'spotify_artist': cached_artist_str, @@ -34165,7 +34165,7 @@ def _run_deezer_discovery_worker(playlist_id): state['discovery_progress'] = int(((i + 1) / len(tracks)) * 100) continue except Exception as cache_err: - print(f"⚠️ Cache lookup error: {cache_err}") + print(f"Cache lookup error: {cache_err}") # Create a SimpleNamespace duck-type object for _search_spotify_for_tidal_track track_ns = types.SimpleNamespace( @@ -34194,7 +34194,7 @@ def _run_deezer_discovery_worker(playlist_id): }, 'spotify_data': None, 'match_data': None, - 'status': '❌ Not Found', + 'status': 'Not Found', 'status_class': 'not-found', 'spotify_track': '', 'spotify_artist': '', @@ -34237,7 +34237,7 @@ def _run_deezer_discovery_worker(playlist_id): match_data['disc_number'] = raw_track_data['disc_number'] result['spotify_data'] = match_data result['match_data'] = match_data - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = track_obj.name result['spotify_artist'] = ', '.join(track_obj.artists) if isinstance(track_obj.artists, list) else str(track_obj.artists) @@ -34259,7 +34259,7 @@ def _run_deezer_discovery_worker(playlist_id): match_data['image_url'] = _fb_images[0].get('url', '') result['spotify_data'] = match_data result['match_data'] = match_data - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = match_data.get('name', '') itunes_artists = match_data.get('artists', []) @@ -34279,9 +34279,9 @@ def _run_deezer_discovery_worker(playlist_id): result['match_data'], track_name, track_artists[0] if track_artists else '' ) - print(f"💾 CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})") + print(f"CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})") except Exception as cache_err: - print(f"⚠️ Cache save error: {cache_err}") + print(f"Cache save error: {cache_err}") result['index'] = i state['discovery_results'].append(result) @@ -34291,7 +34291,7 @@ def _run_deezer_discovery_worker(playlist_id): time.sleep(0.1) except Exception as e: - print(f"❌ Error processing track {i+1}: {e}") + print(f"Error processing track {i+1}: {e}") # Add error result result = { 'deezer_track': { @@ -34300,7 +34300,7 @@ def _run_deezer_discovery_worker(playlist_id): }, 'spotify_data': None, 'match_data': None, - 'status': '❌ Error', + 'status': 'Error', 'status_class': 'error', 'spotify_track': '', 'spotify_artist': '', @@ -34319,15 +34319,15 @@ def _run_deezer_discovery_worker(playlist_id): # Add activity for discovery completion source_label = discovery_source.upper() - add_activity_item("✅", f"Deezer Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now") + add_activity_item("", f"Deezer Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now") - print(f"✅ Deezer discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found") + print(f"Deezer discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found") # Sync discovery results back to mirrored playlist _sync_discovery_results_to_mirrored('deezer', playlist_id, state.get('discovery_results', []), discovery_source, profile_id=state.get('_profile_id', 1)) except Exception as e: - print(f"❌ Error in Deezer discovery worker: {e}") + print(f"Error in Deezer discovery worker: {e}") if playlist_id in deezer_discovery_states: deezer_discovery_states[playlist_id]['phase'] = 'error' deezer_discovery_states[playlist_id]['status'] = f'error: {str(e)}' @@ -34366,7 +34366,7 @@ def convert_deezer_results_to_spotify_tracks(discovery_results): } spotify_tracks.append(track) - print(f"🔄 Converted {len(spotify_tracks)} Deezer matches to Spotify tracks for sync") + print(f"Converted {len(spotify_tracks)} Deezer matches to Spotify tracks for sync") return spotify_tracks @@ -34398,7 +34398,7 @@ def start_deezer_sync(playlist_id): playlist_name = state['playlist']['name'] # Add activity for sync start - add_activity_item("🔄", "Deezer Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") + add_activity_item("", "Deezer Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") # Update Deezer state state['phase'] = 'syncing' @@ -34419,11 +34419,11 @@ def start_deezer_sync(playlist_id): future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) active_sync_workers[sync_playlist_id] = future - print(f"🔄 Started Deezer sync for: {playlist_name} ({len(spotify_tracks)} tracks)") + print(f"Started Deezer sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) except Exception as e: - print(f"❌ Error starting Deezer sync: {e}") + print(f"Error starting Deezer sync: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/sync/status/', methods=['GET']) @@ -34457,16 +34457,16 @@ def get_deezer_sync_status(playlist_id): state['phase'] = 'sync_complete' state['sync_progress'] = sync_state.get('progress', {}) playlist_name = state['playlist']['name'] - add_activity_item("🔄", "Sync Complete", f"Deezer playlist '{playlist_name}' synced successfully", "Now") + add_activity_item("", "Sync Complete", f"Deezer playlist '{playlist_name}' synced successfully", "Now") elif sync_state.get('status') == 'error': state['phase'] = 'discovered' # Revert on error playlist_name = state['playlist']['name'] - add_activity_item("❌", "Sync Failed", f"Deezer playlist '{playlist_name}' sync failed", "Now") + add_activity_item("", "Sync Failed", f"Deezer playlist '{playlist_name}' sync failed", "Now") return jsonify(response) except Exception as e: - print(f"❌ Error getting Deezer sync status: {e}") + print(f"Error getting Deezer sync status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/deezer/sync/cancel/', methods=['POST']) @@ -34497,7 +34497,7 @@ def cancel_deezer_sync(playlist_id): return jsonify({"success": True, "message": "Deezer sync cancelled"}) except Exception as e: - print(f"❌ Error cancelling Deezer sync: {e}") + print(f"Error cancelling Deezer sync: {e}") return jsonify({"error": str(e)}), 500 @@ -34525,7 +34525,7 @@ def parse_spotify_public_endpoint(): if not parsed: return jsonify({"error": "Invalid Spotify URL. Please use a playlist or album link from open.spotify.com"}), 400 - print(f"🎵 Scraping public Spotify {parsed['type']}: {parsed['id']}") + print(f"Scraping public Spotify {parsed['type']}: {parsed['id']}") result = scrape_spotify_embed(parsed['type'], parsed['id']) @@ -34584,11 +34584,11 @@ def parse_spotify_public_endpoint(): spotify_public_discovery_states[url_hash]['playlist'] = response_data spotify_public_discovery_states[url_hash]['last_accessed'] = time.time() - print(f"✅ Spotify {parsed['type']} scraped: {result['name']} ({len(spotify_tracks)} tracks)") + print(f"Spotify {parsed['type']} scraped: {result['name']} ({len(spotify_tracks)} tracks)") return jsonify(response_data) except Exception as e: - print(f"❌ Error parsing Spotify URL: {e}") + print(f"Error parsing Spotify URL: {e}") import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @@ -34617,17 +34617,17 @@ def start_spotify_public_discovery(url_hash): # Add activity for discovery start playlist_name = state['playlist']['name'] track_count = len(state['playlist']['tracks']) - add_activity_item("🔍", "Spotify Link Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") + add_activity_item("", "Spotify Link Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") # Start discovery worker future = spotify_public_discovery_executor.submit(_run_spotify_public_discovery_worker, url_hash) state['discovery_future'] = future - print(f"🔍 Started Spotify discovery for Spotify Public playlist: {playlist_name}") + print(f"Started Spotify discovery for Spotify Public playlist: {playlist_name}") return jsonify({"success": True, "message": "Discovery started"}) except Exception as e: - print(f"❌ Error starting Spotify Public discovery: {e}") + print(f"Error starting Spotify Public discovery: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/discovery/status/', methods=['GET']) @@ -34653,7 +34653,7 @@ def get_spotify_public_discovery_status(url_hash): return jsonify(response) except Exception as e: - print(f"❌ Error getting Spotify Public discovery status: {e}") + print(f"Error getting Spotify Public discovery status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/discovery/update_match', methods=['POST']) @@ -34682,7 +34682,7 @@ def update_spotify_public_discovery_match(): old_status = result.get('status') # Update with user-selected track - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = spotify_track['name'] result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists']) @@ -34710,10 +34710,10 @@ def update_spotify_public_discovery_match(): result['manual_match'] = True # Update match count if status changed from not found/error - if old_status != 'found' and old_status != '✅ Found': + if old_status != 'found' and old_status != 'Found': state['spotify_matches'] = state.get('spotify_matches', 0) + 1 - print(f"✅ Manual match updated: spotify_public - {identifier} - track {track_index}") + print(f"Manual match updated: spotify_public - {identifier} - track {track_index}") print(f" → {result['spotify_artist']} - {result['spotify_track']}") # Save manual fix to discovery cache so it appears in discovery pool @@ -34744,14 +34744,14 @@ def update_spotify_public_discovery_match(): cache_key[0], cache_key[1], 'spotify', 1.0, matched_data, original_name, original_artist ) - print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}") + print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}") except Exception as cache_err: - print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}") + print(f"Error saving manual fix to discovery cache: {cache_err}") return jsonify({'success': True, 'result': result}) except Exception as e: - print(f"❌ Error updating Spotify Public discovery match: {e}") + print(f"Error updating Spotify Public discovery match: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/spotify-public/playlists/states', methods=['GET']) @@ -34778,11 +34778,11 @@ def get_spotify_public_playlist_states(): } states.append(state_info) - print(f"🎵 Returning {len(states)} stored Spotify Public playlist states for hydration") + print(f"Returning {len(states)} stored Spotify Public playlist states for hydration") return jsonify({"states": states}) except Exception as e: - print(f"❌ Error getting Spotify Public playlist states: {e}") + print(f"Error getting Spotify Public playlist states: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/state/', methods=['GET']) @@ -34814,7 +34814,7 @@ def get_spotify_public_playlist_state(url_hash): return jsonify(response) except Exception as e: - print(f"❌ Error getting Spotify Public playlist state: {e}") + print(f"Error getting Spotify Public playlist state: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/reset/', methods=['POST']) @@ -34843,11 +34843,11 @@ def reset_spotify_public_playlist(url_hash): state['discovery_future'] = None state['last_accessed'] = time.time() - print(f"🔄 Reset Spotify Public playlist to fresh: {url_hash}") + print(f"Reset Spotify Public playlist to fresh: {url_hash}") return jsonify({"success": True, "message": "Playlist reset to fresh phase"}) except Exception as e: - print(f"❌ Error resetting Spotify Public playlist: {e}") + print(f"Error resetting Spotify Public playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/delete/', methods=['POST']) @@ -34866,11 +34866,11 @@ def delete_spotify_public_playlist(url_hash): # Remove from state dictionary del spotify_public_discovery_states[url_hash] - print(f"🗑️ Deleted Spotify Public playlist state: {url_hash}") + print(f"Deleted Spotify Public playlist state: {url_hash}") return jsonify({"success": True, "message": "Playlist deleted"}) except Exception as e: - print(f"❌ Error deleting Spotify Public playlist: {e}") + print(f"Error deleting Spotify Public playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/update_phase/', methods=['POST']) @@ -34903,11 +34903,11 @@ def update_spotify_public_playlist_phase(url_hash): if 'converted_spotify_playlist_id' in data: state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id'] - print(f"🔄 Updated Spotify Public playlist {url_hash} phase: {old_phase} → {new_phase}") + print(f"Updated Spotify Public playlist {url_hash} phase: {old_phase} → {new_phase}") return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase}) except Exception as e: - print(f"❌ Error updating Spotify Public playlist phase: {e}") + print(f"Error updating Spotify Public playlist phase: {e}") return jsonify({"error": str(e)}), 500 @@ -34928,7 +34928,7 @@ def _run_spotify_public_discovery_worker(url_hash): if not use_spotify: itunes_client_instance = _get_metadata_fallback_client() - print(f"🎵 Starting Spotify Public discovery for: {playlist['name']} (using {discovery_source.upper()})") + print(f"Starting Spotify Public discovery for: {playlist['name']} (using {discovery_source.upper()})") # Store discovery source in state for frontend state['discovery_source'] = discovery_source @@ -34958,7 +34958,7 @@ def _run_spotify_public_discovery_worker(url_hash): track_album_name = track_album or '' track_duration_ms = sp_track.get('duration_ms', 0) - print(f"🔍 [{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}") + print(f"[{i+1}/{len(tracks)}] Searching {discovery_source.upper()}: {track_name} by {', '.join(track_artists)}") # Check discovery cache first cache_key = _get_discovery_cache_key(track_name, track_artists[0] if track_artists else '') @@ -34966,7 +34966,7 @@ def _run_spotify_public_discovery_worker(url_hash): cache_db = get_database() cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source) if cached_match and _validate_discovery_cache_artist(track_artists[0] if track_artists else '', cached_match): - print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}") + print(f"CACHE HIT [{i+1}/{len(tracks)}]: {track_name} by {', '.join(track_artists)}") # Extract display-friendly artist string from cached match cached_artists = cached_match.get('artists', []) if cached_artists: @@ -34989,7 +34989,7 @@ def _run_spotify_public_discovery_worker(url_hash): }, 'spotify_data': cached_match, 'match_data': cached_match, - 'status': '✅ Found', + 'status': 'Found', 'status_class': 'found', 'spotify_track': cached_match.get('name', ''), 'spotify_artist': cached_artist_str, @@ -35004,7 +35004,7 @@ def _run_spotify_public_discovery_worker(url_hash): state['discovery_progress'] = int(((i + 1) / len(tracks)) * 100) continue except Exception as cache_err: - print(f"⚠️ Cache lookup error: {cache_err}") + print(f"Cache lookup error: {cache_err}") # Create a SimpleNamespace duck-type object for _search_spotify_for_tidal_track track_ns = types.SimpleNamespace( @@ -35033,7 +35033,7 @@ def _run_spotify_public_discovery_worker(url_hash): }, 'spotify_data': None, 'match_data': None, - 'status': '❌ Not Found', + 'status': 'Not Found', 'status_class': 'not-found', 'spotify_track': '', 'spotify_artist': '', @@ -35076,7 +35076,7 @@ def _run_spotify_public_discovery_worker(url_hash): match_data['disc_number'] = raw_track_data['disc_number'] result['spotify_data'] = match_data result['match_data'] = match_data - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = track_obj.name result['spotify_artist'] = ', '.join(track_obj.artists) if isinstance(track_obj.artists, list) else str(track_obj.artists) @@ -35098,7 +35098,7 @@ def _run_spotify_public_discovery_worker(url_hash): match_data['image_url'] = _fb_images[0].get('url', '') result['spotify_data'] = match_data result['match_data'] = match_data - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = match_data.get('name', '') itunes_artists = match_data.get('artists', []) @@ -35118,9 +35118,9 @@ def _run_spotify_public_discovery_worker(url_hash): result['match_data'], track_name, track_artists[0] if track_artists else '' ) - print(f"💾 CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})") + print(f"CACHE SAVED: {track_name} (confidence: {match_confidence:.3f})") except Exception as cache_err: - print(f"⚠️ Cache save error: {cache_err}") + print(f"Cache save error: {cache_err}") result['index'] = i state['discovery_results'].append(result) @@ -35130,7 +35130,7 @@ def _run_spotify_public_discovery_worker(url_hash): time.sleep(0.1) except Exception as e: - print(f"❌ Error processing track {i+1}: {e}") + print(f"Error processing track {i+1}: {e}") # Add error result result = { 'spotify_public_track': { @@ -35139,7 +35139,7 @@ def _run_spotify_public_discovery_worker(url_hash): }, 'spotify_data': None, 'match_data': None, - 'status': '❌ Error', + 'status': 'Error', 'status_class': 'error', 'spotify_track': '', 'spotify_artist': '', @@ -35158,12 +35158,12 @@ def _run_spotify_public_discovery_worker(url_hash): # Add activity for discovery completion source_label = discovery_source.upper() - add_activity_item("✅", f"Spotify Link Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now") + add_activity_item("", f"Spotify Link Discovery Complete ({source_label})", f"'{playlist['name']}' - {successful_discoveries}/{len(tracks)} tracks found", "Now") - print(f"✅ Spotify Public discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found") + print(f"Spotify Public discovery complete ({source_label}): {successful_discoveries}/{len(tracks)} tracks found") except Exception as e: - print(f"❌ Error in Spotify Public discovery worker: {e}") + print(f"Error in Spotify Public discovery worker: {e}") if url_hash in spotify_public_discovery_states: spotify_public_discovery_states[url_hash]['phase'] = 'error' spotify_public_discovery_states[url_hash]['status'] = f'error: {str(e)}' @@ -35203,7 +35203,7 @@ def convert_spotify_public_results_to_spotify_tracks(discovery_results): } spotify_tracks.append(track) - print(f"🔄 Converted {len(spotify_tracks)} Spotify Public matches to Spotify tracks for sync") + print(f"Converted {len(spotify_tracks)} Spotify Public matches to Spotify tracks for sync") return spotify_tracks @@ -35235,7 +35235,7 @@ def start_spotify_public_sync(url_hash): playlist_name = state['playlist']['name'] # Add activity for sync start - add_activity_item("🔄", "Spotify Link Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") + add_activity_item("", "Spotify Link Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") # Update Spotify Public state state['phase'] = 'syncing' @@ -35256,11 +35256,11 @@ def start_spotify_public_sync(url_hash): future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) active_sync_workers[sync_playlist_id] = future - print(f"🔄 Started Spotify Public sync for: {playlist_name} ({len(spotify_tracks)} tracks)") + print(f"Started Spotify Public sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) except Exception as e: - print(f"❌ Error starting Spotify Public sync: {e}") + print(f"Error starting Spotify Public sync: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/sync/status/', methods=['GET']) @@ -35294,16 +35294,16 @@ def get_spotify_public_sync_status(url_hash): state['phase'] = 'sync_complete' state['sync_progress'] = sync_state.get('progress', {}) playlist_name = state['playlist']['name'] - add_activity_item("🔄", "Sync Complete", f"Spotify Link playlist '{playlist_name}' synced successfully", "Now") + add_activity_item("", "Sync Complete", f"Spotify Link playlist '{playlist_name}' synced successfully", "Now") elif sync_state.get('status') == 'error': state['phase'] = 'discovered' # Revert on error playlist_name = state['playlist']['name'] - add_activity_item("❌", "Sync Failed", f"Spotify Link playlist '{playlist_name}' sync failed", "Now") + add_activity_item("", "Sync Failed", f"Spotify Link playlist '{playlist_name}' sync failed", "Now") return jsonify(response) except Exception as e: - print(f"❌ Error getting Spotify Public sync status: {e}") + print(f"Error getting Spotify Public sync status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/spotify-public/sync/cancel/', methods=['POST']) @@ -35334,7 +35334,7 @@ def cancel_spotify_public_sync(url_hash): return jsonify({"success": True, "message": "Spotify Public sync cancelled"}) except Exception as e: - print(f"❌ Error cancelling Spotify Public sync: {e}") + print(f"Error cancelling Spotify Public sync: {e}") return jsonify({"error": str(e)}), 500 @@ -35368,7 +35368,7 @@ def parse_youtube_playlist_endpoint(): if not ('youtube.com/playlist' in url or 'music.youtube.com/playlist' in url): return jsonify({"error": "Invalid YouTube playlist URL"}), 400 - print(f"🎬 Parsing YouTube playlist: {url}") + print(f"Parsing YouTube playlist: {url}") # Parse the playlist using our function playlist_data = parse_youtube_playlist(url) @@ -35433,11 +35433,11 @@ def parse_youtube_playlist_endpoint(): playlist_data['url_hash'] = url_hash - print(f"✅ YouTube playlist parsed successfully: {playlist_data['name']} ({len(playlist_data['tracks'])} tracks)") + print(f"YouTube playlist parsed successfully: {playlist_data['name']} ({len(playlist_data['tracks'])} tracks)") return jsonify(playlist_data) except Exception as e: - print(f"❌ Error parsing YouTube playlist: {e}") + print(f"Error parsing YouTube playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/discovery/start/', methods=['POST']) @@ -35467,17 +35467,17 @@ def start_youtube_discovery(url_hash): # Add activity for discovery start playlist_name = state['playlist']['name'] track_count = len(state['playlist']['tracks']) - add_activity_item("🔍", "YouTube Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") + add_activity_item("", "YouTube Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") # Start discovery worker future = youtube_discovery_executor.submit(_run_youtube_discovery_worker, url_hash) state['discovery_future'] = future - print(f"🔍 Started Spotify discovery for YouTube playlist: {state['playlist']['name']}") + print(f"Started Spotify discovery for YouTube playlist: {state['playlist']['name']}") return jsonify({"success": True, "message": "Discovery started"}) except Exception as e: - print(f"❌ Error starting YouTube discovery: {e}") + print(f"Error starting YouTube discovery: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/discovery/status/', methods=['GET']) @@ -35503,7 +35503,7 @@ def get_youtube_discovery_status(url_hash): return jsonify(response) except Exception as e: - print(f"❌ Error getting YouTube discovery status: {e}") + print(f"Error getting YouTube discovery status: {e}") return jsonify({"error": str(e)}), 500 @@ -35533,7 +35533,7 @@ def update_youtube_discovery_match(): old_status = result.get('status') # Update with user-selected track - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = spotify_track['name'] result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists']) @@ -35561,10 +35561,10 @@ def update_youtube_discovery_match(): result['manual_match'] = True # Flag for tracking # Update match count if status changed from not found/error - if old_status != 'found' and old_status != '✅ Found': + if old_status != 'found' and old_status != 'Found': state['spotify_matches'] = state.get('spotify_matches', 0) + 1 - print(f"✅ Manual match updated: youtube - {identifier} - track {track_index}") + print(f"Manual match updated: youtube - {identifier} - track {track_index}") print(f" → {result['spotify_artist']} - {result['spotify_track']}") # Save manual fix to discovery cache so it appears in discovery pool @@ -35599,9 +35599,9 @@ def update_youtube_discovery_match(): cache_key[0], cache_key[1], 'spotify', 1.0, matched_data, original_name, original_artist ) - print(f"💾 Manual fix saved to discovery cache: {original_name} by {original_artist}") + print(f"Manual fix saved to discovery cache: {original_name} by {original_artist}") except Exception as cache_err: - print(f"⚠️ Error saving manual fix to discovery cache: {cache_err}") + print(f"Error saving manual fix to discovery cache: {cache_err}") # Persist manual fix to DB for mirrored playlists if identifier.startswith('mirrored_'): @@ -35620,14 +35620,14 @@ def update_youtube_discovery_match(): } db.update_mirrored_track_extra_data(db_track_id, extra_data) result['matched_data'] = matched_data - print(f"💾 Persisted manual fix to DB for track {db_track_id}") + print(f"Persisted manual fix to DB for track {db_track_id}") except Exception as wb_err: - print(f"⚠️ Error persisting manual fix to DB: {wb_err}") + print(f"Error persisting manual fix to DB: {wb_err}") return jsonify({'success': True, 'result': result}) except Exception as e: - print(f"❌ Error updating YouTube discovery match: {e}") + print(f"Error updating YouTube discovery match: {e}") return jsonify({'error': str(e)}), 500 @@ -35647,7 +35647,7 @@ def _run_youtube_discovery_worker(url_hash): # Get fallback client itunes_client = _get_metadata_fallback_client() - print(f"🔍 Starting {discovery_source} discovery for {len(tracks)} YouTube tracks...") + print(f"Starting {discovery_source} discovery for {len(tracks)} YouTube tracks...") # Store the discovery source in state state['discovery_source'] = discovery_source @@ -35657,7 +35657,7 @@ def _run_youtube_discovery_worker(url_hash): try: # Check for cancellation (phase changed by reset/delete/close) if state.get('phase') != 'discovering': - print(f"🛑 Discovery cancelled for {url_hash} (phase changed to '{state.get('phase')}')") + print(f"Discovery cancelled for {url_hash} (phase changed to '{state.get('phase')}')") return # Update progress @@ -35671,7 +35671,7 @@ def _run_youtube_discovery_worker(url_hash): cleaned_title = track['name'] cleaned_artist = track['artists'][0] if track['artists'] else 'Unknown Artist' - print(f"🔍 Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'") + print(f"Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'") # Check discovery cache first cache_key = _get_discovery_cache_key(cleaned_title, cleaned_artist) @@ -35679,12 +35679,12 @@ def _run_youtube_discovery_worker(url_hash): cache_db = get_database() cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source) if cached_match and _validate_discovery_cache_artist(cleaned_artist, cached_match): - print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}") + print(f"CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}") result = { 'index': i, 'yt_track': cleaned_title, 'yt_artist': cleaned_artist, - 'status': '✅ Found', + 'status': 'Found', 'status_class': 'found', 'spotify_track': cached_match.get('name', ''), 'spotify_artist': _extract_artist_name(cached_match.get('artists', [''])[0]) if cached_match.get('artists') else '', @@ -35698,7 +35698,7 @@ def _run_youtube_discovery_worker(url_hash): state['discovery_results'].append(result) continue except Exception as cache_err: - print(f"⚠️ Cache lookup error: {cache_err}") + print(f"Cache lookup error: {cache_err}") # Try multiple search strategies using matching engine matched_track = None @@ -35715,14 +35715,14 @@ def _run_youtube_discovery_worker(url_hash): 'album': None })() search_queries = matching_engine.generate_download_queries(temp_track) - print(f"🔍 Generated {len(search_queries)} search queries for YouTube track") + print(f"Generated {len(search_queries)} search queries for YouTube track") except Exception as e: - print(f"⚠️ Matching engine failed for YouTube, falling back to basic query: {e}") + print(f"Matching engine failed for YouTube, falling back to basic query: {e}") search_queries = [f"{cleaned_artist} {cleaned_title}", cleaned_title] for query_idx, search_query in enumerate(search_queries): try: - print(f"🔍 YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}") + print(f"YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}") search_results = None @@ -35747,22 +35747,22 @@ def _run_youtube_discovery_worker(url_hash): best_raw_track = _cache.get_entity('spotify', 'track', match.id) else: best_raw_track = None - print(f"✅ New best YouTube match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"New best YouTube match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") if best_confidence >= 0.9: - print(f"🎯 High confidence YouTube match found ({best_confidence:.3f}), stopping search") + print(f"High confidence YouTube match found ({best_confidence:.3f}), stopping search") break except Exception as e: - print(f"❌ Error in YouTube search for query '{search_query}': {e}") + print(f"Error in YouTube search for query '{search_query}': {e}") continue if matched_track: - print(f"✅ Strategy 1 YouTube match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})") + print(f"Strategy 1 YouTube match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})") # Strategy 2: Swapped search (if first failed) - score results properly if not matched_track: - print("🔄 YouTube Strategy 2: Trying swapped search (artist/title reversed)") + print("YouTube Strategy 2: Trying swapped search (artist/title reversed)") if use_spotify: query = f"artist:{cleaned_title} track:{cleaned_artist}" fallback_results = spotify_client.search_tracks(query, limit=5) @@ -35776,13 +35776,13 @@ def _run_youtube_discovery_worker(url_hash): if match and confidence >= min_confidence: matched_track = match best_confidence = confidence - print(f"✅ Strategy 2 YouTube match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 2 YouTube match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") # Strategy 3: Raw data search (if still failed) - score results properly if not matched_track: raw_title = track.get('raw_title', cleaned_title) raw_artist = track.get('raw_artist', cleaned_artist) - print(f"🔄 YouTube Strategy 3: Trying raw data search: '{raw_artist} {raw_title}'") + print(f"YouTube Strategy 3: Trying raw data search: '{raw_artist} {raw_title}'") query = f"{raw_artist} {raw_title}" if use_spotify: fallback_results = spotify_client.search_tracks(query, limit=5) @@ -35795,11 +35795,11 @@ def _run_youtube_discovery_worker(url_hash): if match and confidence >= min_confidence: matched_track = match best_confidence = confidence - print(f"✅ Strategy 3 YouTube match (raw): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 3 YouTube match (raw): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") # Strategy 4: Extended search with higher limit (last resort) if not matched_track: - print(f"🔄 YouTube Strategy 4: Extended search with limit=50") + print(f"YouTube Strategy 4: Extended search with limit=50") query = f"{cleaned_artist} {cleaned_title}" if use_spotify: extended_results = spotify_client.search_tracks(query, limit=50) @@ -35812,14 +35812,14 @@ def _run_youtube_discovery_worker(url_hash): if match and confidence >= min_confidence: matched_track = match best_confidence = confidence - print(f"✅ Strategy 4 YouTube match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 4 YouTube match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") # Create result entry result = { 'index': i, 'yt_track': cleaned_title, 'yt_artist': cleaned_artist, - 'status': '✅ Found' if matched_track else '❌ Not Found', + 'status': 'Found' if matched_track else 'Not Found', 'status_class': 'found' if matched_track else 'not-found', 'spotify_track': matched_track.name if matched_track else '', 'spotify_artist': _extract_artist_name(matched_track.artists[0]) if matched_track else '', @@ -35866,21 +35866,21 @@ def _run_youtube_discovery_worker(url_hash): cache_key[0], cache_key[1], discovery_source, best_confidence, result['matched_data'], cleaned_title, cleaned_artist ) - print(f"💾 CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})") + print(f"CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})") except Exception as cache_err: - print(f"⚠️ Cache save error: {cache_err}") + print(f"Cache save error: {cache_err}") state['discovery_results'].append(result) - print(f" {'✅' if matched_track else '❌'} Track {i+1}/{len(tracks)}: {result['status']}") + print(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}") except Exception as e: - print(f"❌ Error processing track {i}: {e}") + print(f"Error processing track {i}: {e}") result = { 'index': i, 'yt_track': track['name'], 'yt_artist': track['artists'][0] if track['artists'] else 'Unknown', - 'status': '❌ Error', + 'status': 'Error', 'status_class': 'error', 'spotify_track': '', 'spotify_artist': '', @@ -35927,18 +35927,18 @@ def _run_youtube_discovery_worker(url_hash): 'provider': discovery_source, } db.update_mirrored_track_extra_data(db_track_id, extra_data) - print(f"💾 Wrote discovery results to DB for {url_hash}") + print(f"Wrote discovery results to DB for {url_hash}") except Exception as wb_err: - print(f"⚠️ Error writing discovery results to DB: {wb_err}") + print(f"Error writing discovery results to DB: {wb_err}") playlist_name = playlist['name'] source_label = discovery_source.upper() - add_activity_item("✅", f"YouTube Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now") + add_activity_item("", f"YouTube Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now") - print(f"✅ YouTube discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched") + print(f"YouTube discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched") except Exception as e: - print(f"❌ Error in YouTube discovery worker: {e}") + print(f"Error in YouTube discovery worker: {e}") state['status'] = 'error' state['phase'] = 'fresh' finally: @@ -35961,7 +35961,7 @@ def _run_listenbrainz_discovery_worker(state_key): # Get fallback client itunes_client = _get_metadata_fallback_client() - print(f"🔍 Starting {discovery_source} discovery for {len(tracks)} ListenBrainz tracks...") + print(f"Starting {discovery_source} discovery for {len(tracks)} ListenBrainz tracks...") # Store the discovery source in state state['discovery_source'] = discovery_source @@ -35971,7 +35971,7 @@ def _run_listenbrainz_discovery_worker(state_key): try: # Check for cancellation if state.get('phase') != 'discovering': - print(f"🛑 ListenBrainz discovery cancelled (phase changed to '{state.get('phase')}')") + print(f"ListenBrainz discovery cancelled (phase changed to '{state.get('phase')}')") return # Update progress @@ -35983,7 +35983,7 @@ def _run_listenbrainz_discovery_worker(state_key): album_name = track.get('album_name', '') duration_ms = track.get('duration_ms', 0) - print(f"🔍 Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'") + print(f"Searching {discovery_source} for: '{cleaned_artist}' - '{cleaned_title}'") # Check discovery cache first cache_key = _get_discovery_cache_key(cleaned_title, cleaned_artist) @@ -35991,12 +35991,12 @@ def _run_listenbrainz_discovery_worker(state_key): cache_db = get_database() cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source) if cached_match and _validate_discovery_cache_artist(cleaned_artist, cached_match): - print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}") + print(f"CACHE HIT [{i+1}/{len(tracks)}]: {cleaned_artist} - {cleaned_title}") result = { 'index': i, 'lb_track': cleaned_title, 'lb_artist': cleaned_artist, - 'status': '✅ Found', + 'status': 'Found', 'status_class': 'found', 'spotify_track': cached_match.get('name', ''), 'spotify_artist': _extract_artist_name(cached_match.get('artists', [''])[0]) if cached_match.get('artists') else '', @@ -36010,7 +36010,7 @@ def _run_listenbrainz_discovery_worker(state_key): state['discovery_results'].append(result) continue except Exception as cache_err: - print(f"⚠️ Cache lookup error: {cache_err}") + print(f"Cache lookup error: {cache_err}") # Try multiple search strategies using matching engine matched_track = None @@ -36027,14 +36027,14 @@ def _run_listenbrainz_discovery_worker(state_key): 'album': album_name if album_name else None })() search_queries = matching_engine.generate_download_queries(temp_track) - print(f"🔍 Generated {len(search_queries)} search queries for ListenBrainz track") + print(f"Generated {len(search_queries)} search queries for ListenBrainz track") except Exception as e: - print(f"⚠️ Matching engine failed for ListenBrainz, falling back to basic query: {e}") + print(f"Matching engine failed for ListenBrainz, falling back to basic query: {e}") search_queries = [f"{cleaned_artist} {cleaned_title}", cleaned_title] for query_idx, search_query in enumerate(search_queries): try: - print(f"🔍 ListenBrainz query {query_idx + 1}/{len(search_queries)}: {search_query}") + print(f"ListenBrainz query {query_idx + 1}/{len(search_queries)}: {search_query}") search_results = None @@ -36059,22 +36059,22 @@ def _run_listenbrainz_discovery_worker(state_key): best_raw_track = _cache.get_entity('spotify', 'track', match.id) else: best_raw_track = None - print(f"✅ New best ListenBrainz match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"New best ListenBrainz match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") if best_confidence >= 0.9: - print(f"🎯 High confidence ListenBrainz match found ({best_confidence:.3f}), stopping search") + print(f"High confidence ListenBrainz match found ({best_confidence:.3f}), stopping search") break except Exception as e: - print(f"❌ Error in ListenBrainz search for query '{search_query}': {e}") + print(f"Error in ListenBrainz search for query '{search_query}': {e}") continue if matched_track: - print(f"✅ Strategy 1 ListenBrainz match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})") + print(f"Strategy 1 ListenBrainz match: {matched_track.artists[0]} - {matched_track.name} (confidence: {best_confidence:.3f})") # Strategy 2: Swapped search (if first failed) - score results properly if not matched_track: - print("🔄 ListenBrainz Strategy 2: Trying swapped search (artist/title reversed)") + print("ListenBrainz Strategy 2: Trying swapped search (artist/title reversed)") if use_spotify: query = f"artist:{cleaned_title} track:{cleaned_artist}" fallback_results = spotify_client.search_tracks(query, limit=5) @@ -36088,11 +36088,11 @@ def _run_listenbrainz_discovery_worker(state_key): if match and confidence >= min_confidence: matched_track = match best_confidence = confidence - print(f"✅ Strategy 2 ListenBrainz match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 2 ListenBrainz match (swapped): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") # Strategy 3: Album-based search (if still failed and we have album name) - score results properly if not matched_track and album_name: - print(f"🔄 ListenBrainz Strategy 3: Trying album-based search: '{cleaned_artist} {album_name} {cleaned_title}'") + print(f"ListenBrainz Strategy 3: Trying album-based search: '{cleaned_artist} {album_name} {cleaned_title}'") if use_spotify: query = f"artist:{cleaned_artist} album:{album_name} track:{cleaned_title}" fallback_results = spotify_client.search_tracks(query, limit=5) @@ -36106,11 +36106,11 @@ def _run_listenbrainz_discovery_worker(state_key): if match and confidence >= min_confidence: matched_track = match best_confidence = confidence - print(f"✅ Strategy 3 ListenBrainz match (album): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 3 ListenBrainz match (album): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") # Strategy 4: Extended search with higher limit (last resort) if not matched_track: - print(f"🔄 ListenBrainz Strategy 4: Extended search with limit=50") + print(f"ListenBrainz Strategy 4: Extended search with limit=50") query = f"{cleaned_artist} {cleaned_title}" if use_spotify: extended_results = spotify_client.search_tracks(query, limit=50) @@ -36123,14 +36123,14 @@ def _run_listenbrainz_discovery_worker(state_key): if match and confidence >= min_confidence: matched_track = match best_confidence = confidence - print(f"✅ Strategy 4 ListenBrainz match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 4 ListenBrainz match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") # Create result entry result = { 'index': i, 'lb_track': cleaned_title, 'lb_artist': cleaned_artist, - 'status': '✅ Found' if matched_track else '❌ Not Found', + 'status': 'Found' if matched_track else 'Not Found', 'status_class': 'found' if matched_track else 'not-found', 'spotify_track': matched_track.name if matched_track else '', 'spotify_artist': _extract_artist_name(matched_track.artists[0]) if matched_track else '', @@ -36177,21 +36177,21 @@ def _run_listenbrainz_discovery_worker(state_key): cache_key[0], cache_key[1], discovery_source, best_confidence, result['matched_data'], cleaned_title, cleaned_artist ) - print(f"💾 CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})") + print(f"CACHE SAVED: {cleaned_artist} - {cleaned_title} (confidence: {best_confidence:.3f})") except Exception as cache_err: - print(f"⚠️ Cache save error: {cache_err}") + print(f"Cache save error: {cache_err}") state['discovery_results'].append(result) - print(f" {'✅' if matched_track else '❌'} Track {i+1}/{len(tracks)}: {result['status']}") + print(f" {'' if matched_track else ''} Track {i+1}/{len(tracks)}: {result['status']}") except Exception as e: - print(f"❌ Error processing track {i}: {e}") + print(f"Error processing track {i}: {e}") result = { 'index': i, 'lb_track': track['track_name'], 'lb_artist': track['artist_name'], - 'status': '❌ Error', + 'status': 'Error', 'status_class': 'error', 'spotify_track': '', 'spotify_artist': '', @@ -36207,12 +36207,12 @@ def _run_listenbrainz_discovery_worker(state_key): playlist_name = playlist['name'] source_label = discovery_source.upper() - add_activity_item("✅", f"ListenBrainz Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now") + add_activity_item("", f"ListenBrainz Discovery Complete ({source_label})", f"'{playlist_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now") - print(f"✅ ListenBrainz discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched") + print(f"ListenBrainz discovery complete ({discovery_source}): {state['spotify_matches']}/{len(tracks)} tracks matched") except Exception as e: - print(f"❌ Error in ListenBrainz discovery worker: {e}") + print(f"Error in ListenBrainz discovery worker: {e}") state['status'] = 'error' state['phase'] = 'fresh' finally: @@ -36266,7 +36266,7 @@ def start_youtube_sync(url_hash): playlist_name = state['playlist']['name'] # Add activity for sync start - add_activity_item("🔄", "YouTube Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") + add_activity_item("", "YouTube Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") # Update YouTube state state['phase'] = 'syncing' @@ -36287,11 +36287,11 @@ def start_youtube_sync(url_hash): future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) active_sync_workers[sync_playlist_id] = future - print(f"🔄 Started YouTube sync for: {playlist_name} ({len(spotify_tracks)} tracks)") + print(f"Started YouTube sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) except Exception as e: - print(f"❌ Error starting YouTube sync: {e}") + print(f"Error starting YouTube sync: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/sync/status/', methods=['GET']) @@ -36326,16 +36326,16 @@ def get_youtube_sync_status(url_hash): state['sync_progress'] = sync_state.get('progress', {}) # Add activity for sync completion playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("🔄", "Sync Complete", f"YouTube playlist '{playlist_name}' synced successfully", "Now") + add_activity_item("", "Sync Complete", f"YouTube playlist '{playlist_name}' synced successfully", "Now") elif sync_state.get('status') == 'error': state['phase'] = 'discovered' # Revert on error playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("❌", "Sync Failed", f"YouTube playlist '{playlist_name}' sync failed", "Now") + add_activity_item("", "Sync Failed", f"YouTube playlist '{playlist_name}' sync failed", "Now") return jsonify(response) except Exception as e: - print(f"❌ Error getting YouTube sync status: {e}") + print(f"Error getting YouTube sync status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/sync/cancel/', methods=['POST']) @@ -36366,7 +36366,7 @@ def cancel_youtube_sync(url_hash): return jsonify({"success": True, "message": "YouTube sync cancelled"}) except Exception as e: - print(f"❌ Error cancelling YouTube sync: {e}") + print(f"Error cancelling YouTube sync: {e}") return jsonify({"error": str(e)}), 500 # New YouTube Playlist Management Endpoints (for persistent state) @@ -36402,11 +36402,11 @@ def get_all_youtube_playlists(): } playlists.append(playlist_info) - print(f"📋 Returning {len(playlists)} stored YouTube playlists for hydration") + print(f"Returning {len(playlists)} stored YouTube playlists for hydration") return jsonify({"playlists": playlists}) except Exception as e: - print(f"❌ Error getting YouTube playlists: {e}") + print(f"Error getting YouTube playlists: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/state/', methods=['GET']) @@ -36440,7 +36440,7 @@ def get_youtube_playlist_state(url_hash): return jsonify(response) except Exception as e: - print(f"❌ Error getting YouTube playlist state: {e}") + print(f"Error getting YouTube playlist state: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/reset/', methods=['POST']) @@ -36468,11 +36468,11 @@ def reset_youtube_playlist(url_hash): state['discovery_future'] = None state['last_accessed'] = time.time() - print(f"🔄 Reset YouTube playlist to fresh phase: {state['playlist']['name']}") + print(f"Reset YouTube playlist to fresh phase: {state['playlist']['name']}") return jsonify({"success": True, "message": "Playlist reset to fresh state"}) except Exception as e: - print(f"❌ Error resetting YouTube playlist: {e}") + print(f"Error resetting YouTube playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/delete/', methods=['DELETE']) @@ -36492,11 +36492,11 @@ def delete_youtube_playlist(url_hash): playlist_name = state['playlist']['name'] del youtube_playlist_states[url_hash] - print(f"🗑️ Deleted YouTube playlist from backend: {playlist_name}") + print(f"Deleted YouTube playlist from backend: {playlist_name}") return jsonify({"success": True, "message": f"Playlist '{playlist_name}' deleted"}) except Exception as e: - print(f"❌ Error deleting YouTube playlist: {e}") + print(f"Error deleting YouTube playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/youtube/update_phase/', methods=['POST']) @@ -36521,11 +36521,11 @@ def update_youtube_playlist_phase(url_hash): state['phase'] = new_phase state['last_accessed'] = time.time() - print(f"🔄 Updated YouTube playlist {url_hash} phase: {old_phase} → {new_phase}") + print(f"Updated YouTube playlist {url_hash} phase: {old_phase} → {new_phase}") return jsonify({"success": True, "message": f"Phase updated to {new_phase}", "old_phase": old_phase, "new_phase": new_phase}) except Exception as e: - print(f"❌ Error updating YouTube playlist phase: {e}") + print(f"Error updating YouTube playlist phase: {e}") return jsonify({"error": str(e)}), 500 def convert_youtube_results_to_spotify_tracks(discovery_results): @@ -36561,7 +36561,7 @@ def convert_youtube_results_to_spotify_tracks(discovery_results): } spotify_tracks.append(track) - print(f"🔄 Converted {len(spotify_tracks)} YouTube matches to Spotify tracks for sync") + print(f"Converted {len(spotify_tracks)} YouTube matches to Spotify tracks for sync") return spotify_tracks @@ -36572,8 +36572,8 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, global sync_states, sync_service task_start_time = time.time() - print(f"🚀 [TIMING] _run_sync_task STARTED for playlist '{playlist_name}' at {time.strftime('%H:%M:%S')}") - print(f"📊 Received {len(tracks_json)} tracks from frontend") + print(f"[TIMING] _run_sync_task STARTED for playlist '{playlist_name}' at {time.strftime('%H:%M:%S')}") + print(f"Received {len(tracks_json)} tracks from frontend") # Record sync history start (skip for re-syncs triggered from history) _is_resync = playlist_id.startswith('resync_') @@ -36600,7 +36600,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, try: # Recreate a Playlist object from the JSON data sent by the frontend # This avoids needing to re-fetch it from Spotify - print(f"🔄 Converting JSON tracks to SpotifyTrack objects...") + print(f"Converting JSON tracks to SpotifyTrack objects...") # Store original track data with full album objects (for wishlist with cover art) # Normalize formats for wishlist: album must be dict {'name': ...}, artists must be [{'name': ...}] @@ -36668,7 +36668,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, if i < 3: # Log first 3 tracks for debugging print(f" Track {i+1}: '{track.name}' by {track.artists}") - print(f"✅ Created {len(tracks)} SpotifyTrack objects") + print(f"Created {len(tracks)} SpotifyTrack objects") playlist = SpotifyPlaylist( id=playlist_id, @@ -36680,7 +36680,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, tracks=tracks, total_tracks=len(tracks) ) - print(f"✅ Created SpotifyPlaylist object: '{playlist.name}' with {playlist.total_tracks} tracks") + print(f"Created SpotifyPlaylist object: '{playlist.name}' with {playlist.total_tracks} tracks") first_callback_time = [None] # Use list to allow modification in nested function @@ -36691,15 +36691,15 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, first_callback_duration = (first_callback_time[0] - task_start_time) * 1000 print(f"⏱️ [TIMING] FIRST progress callback at {time.strftime('%H:%M:%S')} (took {first_callback_duration:.1f}ms from start)") - print(f"⚡ PROGRESS CALLBACK: {progress.current_step} - {progress.current_track}") - print(f" 📊 Progress: {progress.progress}% ({progress.matched_tracks}/{progress.total_tracks} matched, {progress.failed_tracks} failed)") + print(f"PROGRESS CALLBACK: {progress.current_step} - {progress.current_track}") + print(f" Progress: {progress.progress}% ({progress.matched_tracks}/{progress.total_tracks} matched, {progress.failed_tracks} failed)") with sync_lock: sync_states[playlist_id] = { "status": "syncing", "progress": progress.__dict__ # Convert dataclass to dict } - print(f" ✅ Updated sync_states for {playlist_id}") + print(f" Updated sync_states for {playlist_id}") # Update automation progress card if automation_id: @@ -36719,7 +36719,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, log_line=f'{track} — {step}' if track else step, log_type=log_type) except Exception as setup_error: - print(f"❌ SETUP ERROR in _run_sync_task: {setup_error}") + print(f"SETUP ERROR in _run_sync_task: {setup_error}") import traceback traceback.print_exc() with sync_lock: @@ -36733,7 +36733,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, return try: - print(f"🔧 Setting up sync service...") + print(f"Setting up sync service...") print(f" sync_service available: {sync_service is not None}") if sync_service is None: @@ -36762,24 +36762,24 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, db = MusicDatabase() print(f" Database initialized: {db is not None}") except Exception as db_error: - print(f" ❌ Database initialization failed: {db_error}") + print(f" Database initialization failed: {db_error}") - print(f"🔄 Attaching progress callback...") + print(f"Attaching progress callback...") # Attach the progress callback sync_service.set_progress_callback(progress_callback, playlist.name) - print(f"✅ Progress callback attached for playlist: {playlist.name}") + print(f"Progress callback attached for playlist: {playlist.name}") # CRITICAL FIX: Add database-only fallback for web context # If media client is not connected, patch the sync service to use database-only matching if media_client is None or not media_client.is_connected(): - print(f"⚠️ Media client not connected - patching sync service for database-only matching") + print(f"Media client not connected - patching sync service for database-only matching") # Store original method original_find_track = sync_service._find_track_in_media_server # Create database-only replacement method async def database_only_find_track(spotify_track): - print(f"🗃️ Database-only search for: '{spotify_track.name}' by {spotify_track.artists}") + print(f"Database-only search for: '{spotify_track.name}' by {spotify_track.artists}") try: from database.music_database import MusicDatabase from config.settings import config_manager @@ -36801,9 +36801,9 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, self.ratingKey = db_t.id self.title = db_t.title self.id = db_t.id - print(f"⚡ Sync cache hit: '{original_title}' → server track {cached['server_track_id']}") + print(f"Sync cache hit: '{original_title}' → server track {cached['server_track_id']}") return DatabaseTrackCached(db_track_check), cached['confidence'] - print(f"🔄 Sync cache stale for '{original_title}' — track gone") + print(f"Sync cache stale for '{original_title}' — track gone") except Exception: pass # --- End cache fast-path --- @@ -36825,7 +36825,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, ) if db_track and confidence >= 0.80: - print(f"✅ Database match: '{db_track.title}' (confidence: {confidence:.2f})") + print(f"Database match: '{db_track.title}' (confidence: {confidence:.2f})") # Save to sync match cache if spotify_id: @@ -36848,21 +36848,21 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, return DatabaseTrackMock(db_track), confidence - print(f"❌ No database match found for: '{original_title}'") + print(f"No database match found for: '{original_title}'") return None, 0.0 except Exception as e: - print(f"❌ Database search error: {e}") + print(f"Database search error: {e}") return None, 0.0 # Patch the method sync_service._find_track_in_media_server = database_only_find_track - print(f"✅ Patched sync service to use database-only matching") + print(f"Patched sync service to use database-only matching") sync_start_time = time.time() setup_duration = (sync_start_time - task_start_time) * 1000 print(f"⏱️ [TIMING] Setup completed at {time.strftime('%H:%M:%S')} (took {setup_duration:.1f}ms)") - print(f"🚀 Starting actual sync process with run_async()...") + print(f"Starting actual sync process with run_async()...") # Attach original tracks map to sync_service for wishlist with album images sync_service._original_tracks_map = original_tracks_map @@ -36883,7 +36883,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, sync_duration = (time.time() - sync_start_time) * 1000 total_duration = (time.time() - task_start_time) * 1000 print(f"⏱️ [TIMING] Sync completed at {time.strftime('%H:%M:%S')} (sync: {sync_duration:.1f}ms, total: {total_duration:.1f}ms)") - print(f"✅ Sync process completed! Result type: {type(result)}") + print(f"Sync process completed! Result type: {type(result)}") print(f" Result details: matched={getattr(result, 'matched_tracks', 'N/A')}, total={getattr(result, 'total_tracks', 'N/A')}") # Update final state on completion @@ -36909,7 +36909,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, "progress": result_dict, "result": result_dict } - print(f"🏁 Sync finished for {playlist_id} - state updated") + print(f"Sync finished for {playlist_id} - state updated") # Set playlist poster image if available (Plex, Jellyfin, Emby) if playlist_image_url and getattr(result, 'synced_tracks', 0) > 0: @@ -36921,7 +36921,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, jellyfin_client.set_playlist_image(playlist_name, playlist_image_url) # Navidrome doesn't support custom playlist images except Exception as img_err: - print(f"⚠️ Could not set playlist image: {img_err}") + print(f"Could not set playlist image: {img_err}") # Record sync history completion with per-track data try: @@ -36948,11 +36948,11 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, try: track_results_json = json.dumps(match_details, default=str) saved = db.update_sync_history_track_results(target_batch_id, track_results_json) - print(f"📝 [Sync History] Saved {len(match_details)} track results for batch {target_batch_id} (saved={saved})") + print(f"[Sync History] Saved {len(match_details)} track results for batch {target_batch_id} (saved={saved})") except Exception as json_err: - print(f"⚠️ [Sync History] Failed to serialize track results: {json_err}") + print(f"[Sync History] Failed to serialize track results: {json_err}") else: - print(f"⚠️ [Sync History] No match_details on SyncResult for batch {target_batch_id}") + print(f"[Sync History] No match_details on SyncResult for batch {target_batch_id}") except Exception as e: logger.warning(f"Failed to record sync history completion: {e}") @@ -36989,7 +36989,7 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, tracks_hash=_tracks_hash) except Exception as e: - print(f"❌ SYNC FAILED for {playlist_id}: {e}") + print(f"SYNC FAILED for {playlist_id}: {e}") import traceback traceback.print_exc() with sync_lock: @@ -37001,14 +37001,14 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, _update_automation_progress(automation_id, status='error', progress=100, phase='Error', log_line=f'Sync failed: {str(e)}', log_type='error') finally: - print(f"🧹 Cleaning up progress callback for {playlist.name}") + print(f"Cleaning up progress callback for {playlist.name}") # Clean up the callback if sync_service: sync_service.clear_progress_callback(playlist.name) # Clean up original tracks map if hasattr(sync_service, '_original_tracks_map'): del sync_service._original_tracks_map - print(f"✅ Cleanup completed for {playlist_id}") + print(f"Cleanup completed for {playlist_id}") @app.route('/api/sync/start', methods=['POST']) @@ -37027,9 +37027,9 @@ def start_playlist_sync(): return jsonify({"success": False, "error": "Missing playlist_id, name, or tracks."}), 400 # Add activity for sync start - add_activity_item("🔄", "Spotify Sync Started", f"'{playlist_name}' - {len(tracks_json)} tracks", "Now") + add_activity_item("", "Spotify Sync Started", f"'{playlist_name}' - {len(tracks_json)} tracks", "Now") - logger.info(f"🔄 Starting playlist sync for '{playlist_name}' with {len(tracks_json)} tracks") + logger.info(f"Starting playlist sync for '{playlist_name}' with {len(tracks_json)} tracks") logger.debug(f"Request parsed at {time.strftime('%H:%M:%S')} (took {(time.time()-request_start_time)*1000:.1f}ms)") with sync_lock: @@ -37102,25 +37102,25 @@ def cancel_playlist_sync(): def test_database_access(): """Test endpoint to verify database connectivity for sync operations""" try: - print(f"🧪 Testing database access for sync operations...") + print(f"Testing database access for sync operations...") # Test database initialization from database.music_database import MusicDatabase db = MusicDatabase() - print(f" ✅ Database initialized: {db is not None}") + print(f" Database initialized: {db is not None}") # Test basic database query stats = db.get_database_info_for_server() - print(f" ✅ Database stats retrieved: {stats}") + print(f" Database stats retrieved: {stats}") # Test track existence check (like sync service does) db_track, confidence = db.check_track_exists("test track", "test artist", confidence_threshold=0.7) - print(f" ✅ Track existence check works: found={db_track is not None}, confidence={confidence}") + print(f" Track existence check works: found={db_track is not None}, confidence={confidence}") # Test config manager from config.settings import config_manager active_server = config_manager.get_active_media_server() - print(f" ✅ Active media server: {active_server}") + print(f" Active media server: {active_server}") # Test media clients print(f" Media clients status:") @@ -37144,7 +37144,7 @@ def test_database_access(): }) except Exception as e: - print(f" ❌ Database test failed: {e}") + print(f" Database test failed: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37173,7 +37173,7 @@ def save_discover_download_snapshot(): db.save_bubble_snapshot('discover_downloads', downloads, profile_id=get_current_profile_id()) download_count = len(downloads) - print(f"📸 Saved discover download snapshot: {download_count} downloads") + print(f"Saved discover download snapshot: {download_count} downloads") return jsonify({ 'success': True, @@ -37182,7 +37182,7 @@ def save_discover_download_snapshot(): }) except Exception as e: - print(f"❌ Error saving discover download snapshot: {e}") + print(f"Error saving discover download snapshot: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37218,7 +37218,7 @@ def hydrate_discover_downloads(): snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00')) cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: - print(f"🧹 Cleaning up old discover download snapshot from {snapshot_time}") + print(f"Cleaning up old discover download snapshot from {snapshot_time}") db.delete_bubble_snapshot('discover_downloads', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37226,7 +37226,7 @@ def hydrate_discover_downloads(): 'message': 'Old snapshot cleaned up' }) except ValueError as e: - print(f"⚠️ Error checking discover snapshot age: {e}") + print(f"Error checking discover snapshot age: {e}") # Get current active download processes for live status current_processes = {} @@ -37242,11 +37242,11 @@ def hydrate_discover_downloads(): 'phase': batch_data.get('phase') } except Exception as e: - print(f"⚠️ Error fetching active processes for discover download hydration: {e}") + print(f"Error fetching active processes for discover download hydration: {e}") # If no active processes exist, the app likely restarted - clean up snapshots if not current_processes: - print(f"🧹 No active processes found - app likely restarted, cleaning up discover download snapshot") + print(f"No active processes found - app likely restarted, cleaning up discover download snapshot") db.delete_bubble_snapshot('discover_downloads', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37261,11 +37261,11 @@ def hydrate_discover_downloads(): if playlist_id in current_processes: process_info = current_processes[playlist_id] live_status = 'in_progress' - print(f"🔄 Found active process for discover download {playlist_id}: {process_info['phase']}") + print(f"Found active process for discover download {playlist_id}: {process_info['phase']}") else: # No active process - likely completed live_status = 'completed' - print(f"✅ No active process for discover download {playlist_id} - marking as completed") + print(f"No active process for discover download {playlist_id} - marking as completed") # Create updated download entry hydrated_downloads[playlist_id] = { @@ -37281,7 +37281,7 @@ def hydrate_discover_downloads(): active_count = sum(1 for d in hydrated_downloads.values() if d['status'] == 'in_progress') completed_count = sum(1 for d in hydrated_downloads.values() if d['status'] == 'completed') - print(f"✅ Hydrated {download_count} discover downloads: {active_count} active, {completed_count} completed") + print(f"Hydrated {download_count} discover downloads: {active_count} active, {completed_count} completed") return jsonify({ 'success': True, @@ -37294,7 +37294,7 @@ def hydrate_discover_downloads(): }) except Exception as e: - print(f"❌ Error hydrating discover downloads: {e}") + print(f"Error hydrating discover downloads: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37322,7 +37322,7 @@ def save_artist_bubble_snapshot(): db.save_bubble_snapshot('artist_bubbles', bubbles, profile_id=get_current_profile_id()) bubble_count = len(bubbles) - print(f"📸 Saved artist bubble snapshot: {bubble_count} artists") + print(f"Saved artist bubble snapshot: {bubble_count} artists") return jsonify({ 'success': True, @@ -37331,7 +37331,7 @@ def save_artist_bubble_snapshot(): }) except Exception as e: - print(f"❌ Error saving artist bubble snapshot: {e}") + print(f"Error saving artist bubble snapshot: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37367,7 +37367,7 @@ def hydrate_artist_bubbles(): snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00')) cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: - print(f"🧹 Cleaning up old snapshot from {snapshot_time}") + print(f"Cleaning up old snapshot from {snapshot_time}") db.delete_bubble_snapshot('artist_bubbles', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37375,7 +37375,7 @@ def hydrate_artist_bubbles(): 'message': 'Old snapshot cleaned up' }) except ValueError as e: - print(f"⚠️ Error checking snapshot age: {e}") + print(f"Error checking snapshot age: {e}") # Get current active download processes for live status current_processes = {} @@ -37391,11 +37391,11 @@ def hydrate_artist_bubbles(): 'phase': batch_data.get('phase') } except Exception as e: - print(f"⚠️ Error fetching active processes for hydration: {e}") + print(f"Error fetching active processes for hydration: {e}") # If no active processes exist, the app likely restarted - clean up snapshots if not current_processes: - print(f"🧹 No active processes found - app likely restarted, cleaning up snapshot") + print(f"No active processes found - app likely restarted, cleaning up snapshot") db.delete_bubble_snapshot('artist_bubbles', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37419,11 +37419,11 @@ def hydrate_artist_bubbles(): if virtual_playlist_id in current_processes: process_info = current_processes[virtual_playlist_id] live_status = 'in_progress' - print(f"🔄 Found active process for {download['album']['name']}: {process_info['phase']}") + print(f"Found active process for {download['album']['name']}: {process_info['phase']}") else: # No active process - likely completed live_status = 'view_results' - print(f"✅ No active process for {download['album']['name']} - marking as completed") + print(f"No active process for {download['album']['name']} - marking as completed") # Create updated download entry updated_download = { @@ -37452,7 +37452,7 @@ def hydrate_artist_bubbles(): for download in bubble['downloads'] if download['status'] == 'view_results') - print(f"🔄 Hydrated {bubble_count} artist bubbles: {active_count} active, {completed_count} completed") + print(f"Hydrated {bubble_count} artist bubbles: {active_count} active, {completed_count} completed") return jsonify({ 'success': True, @@ -37466,7 +37466,7 @@ def hydrate_artist_bubbles(): }) except Exception as e: - print(f"❌ Error hydrating artist bubbles: {e}") + print(f"Error hydrating artist bubbles: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37494,7 +37494,7 @@ def save_search_bubble_snapshot(): db.save_bubble_snapshot('search_bubbles', bubbles, profile_id=get_current_profile_id()) bubble_count = len(bubbles) - print(f"📸 Saved search bubble snapshot: {bubble_count} albums/tracks") + print(f"Saved search bubble snapshot: {bubble_count} albums/tracks") return jsonify({ 'success': True, @@ -37503,7 +37503,7 @@ def save_search_bubble_snapshot(): }) except Exception as e: - print(f"❌ Error saving search bubble snapshot: {e}") + print(f"Error saving search bubble snapshot: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37539,7 +37539,7 @@ def hydrate_search_bubbles(): snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00')) cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: - print(f"🧹 Cleaning up old search snapshot from {snapshot_time}") + print(f"Cleaning up old search snapshot from {snapshot_time}") db.delete_bubble_snapshot('search_bubbles', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37547,7 +37547,7 @@ def hydrate_search_bubbles(): 'message': 'Old snapshot cleaned up' }) except ValueError as e: - print(f"⚠️ Error checking snapshot age: {e}") + print(f"Error checking snapshot age: {e}") # Get current active download processes for live status current_processes = {} @@ -37563,11 +37563,11 @@ def hydrate_search_bubbles(): 'phase': batch_data.get('phase') } except Exception as e: - print(f"⚠️ Error fetching active processes for hydration: {e}") + print(f"Error fetching active processes for hydration: {e}") # If no active processes exist, the app likely restarted - clean up snapshots if not current_processes: - print(f"🧹 No active processes found - app likely restarted, cleaning up search snapshot") + print(f"No active processes found - app likely restarted, cleaning up search snapshot") db.delete_bubble_snapshot('search_bubbles', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37590,11 +37590,11 @@ def hydrate_search_bubbles(): if virtual_playlist_id in current_processes: process_info = current_processes[virtual_playlist_id] live_status = 'in_progress' - print(f"🔄 Found active process for {download['item']['name']}: {process_info['phase']}") + print(f"Found active process for {download['item']['name']}: {process_info['phase']}") else: # No active process - likely completed live_status = 'view_results' - print(f"✅ No active process for {download['item']['name']} - marking as completed") + print(f"No active process for {download['item']['name']} - marking as completed") # Create updated download entry updated_download = { @@ -37619,7 +37619,7 @@ def hydrate_search_bubbles(): for download in bubble['downloads'] if download['status'] == 'view_results') - print(f"🔄 Hydrated {bubble_count} search bubbles (artists): {active_count} active, {completed_count} completed") + print(f"Hydrated {bubble_count} search bubbles (artists): {active_count} active, {completed_count} completed") return jsonify({ 'success': True, @@ -37633,7 +37633,7 @@ def hydrate_search_bubbles(): }) except Exception as e: - print(f"❌ Error hydrating search bubbles: {e}") + print(f"Error hydrating search bubbles: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37657,7 +37657,7 @@ def save_beatport_bubble_snapshot(): db.save_bubble_snapshot('beatport_bubbles', bubbles, profile_id=get_current_profile_id()) bubble_count = len(bubbles) - print(f"📸 Saved Beatport bubble snapshot: {bubble_count} charts") + print(f"Saved Beatport bubble snapshot: {bubble_count} charts") return jsonify({ 'success': True, @@ -37666,7 +37666,7 @@ def save_beatport_bubble_snapshot(): }) except Exception as e: - print(f"❌ Error saving Beatport bubble snapshot: {e}") + print(f"Error saving Beatport bubble snapshot: {e}") import traceback traceback.print_exc() return jsonify({ @@ -37699,7 +37699,7 @@ def hydrate_beatport_bubbles(): snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00')) cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: - print(f"🧹 Cleaning up old Beatport snapshot from {snapshot_time}") + print(f"Cleaning up old Beatport snapshot from {snapshot_time}") db.delete_bubble_snapshot('beatport_bubbles', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37707,7 +37707,7 @@ def hydrate_beatport_bubbles(): 'message': 'Old snapshot cleaned up' }) except ValueError as e: - print(f"⚠️ Error checking Beatport snapshot age: {e}") + print(f"Error checking Beatport snapshot age: {e}") # Get current active download processes for live status current_processes = {} @@ -37723,11 +37723,11 @@ def hydrate_beatport_bubbles(): 'phase': batch_data.get('phase') } except Exception as e: - print(f"⚠️ Error fetching active processes for Beatport hydration: {e}") + print(f"Error fetching active processes for Beatport hydration: {e}") # If no active processes exist, app likely restarted — clean up if not current_processes: - print(f"🧹 No active processes found - cleaning up Beatport snapshot") + print(f"No active processes found - cleaning up Beatport snapshot") db.delete_bubble_snapshot('beatport_bubbles', profile_id=get_current_profile_id()) return jsonify({ 'success': True, @@ -37766,7 +37766,7 @@ def hydrate_beatport_bubbles(): completed_count = sum(1 for b in hydrated_bubbles.values() for d in b['downloads'] if d['status'] == 'view_results') - print(f"🔄 Hydrated {bubble_count} Beatport bubbles: {active_count} active, {completed_count} completed") + print(f"Hydrated {bubble_count} Beatport bubbles: {active_count} active, {completed_count} completed") return jsonify({ 'success': True, @@ -37779,7 +37779,7 @@ def hydrate_beatport_bubbles(): }) except Exception as e: - print(f"❌ Error hydrating Beatport bubbles: {e}") + print(f"Error hydrating Beatport bubbles: {e}") import traceback traceback.print_exc() return jsonify({ @@ -38432,18 +38432,18 @@ def add_to_watchlist(): dc_data = dc.get_artist(artist_id) if dc_data: image_url = dc_data.get('image_url') - print(f"🖼️ Discogs artist image: {image_url[:60] if image_url else 'None'}") + print(f"Discogs artist image: {image_url[:60] if image_url else 'None'}") elif source == 'deezer' or fallback_source == 'deezer': # Deezer: fetch artist image directly from API dz_resp = requests.get(f'https://api.deezer.com/artist/{artist_id}', timeout=5) if dz_resp.ok: dz_data = dz_resp.json() image_url = dz_data.get('picture_xl') or dz_data.get('picture_big') or dz_data.get('picture_medium') - print(f"🖼️ Deezer artist image: {image_url[:60] if image_url else 'None'}") + print(f"Deezer artist image: {image_url[:60] if image_url else 'None'}") else: # iTunes: look up album entity for artwork itunes_url = f"https://itunes.apple.com/lookup?id={artist_id}&entity=album&limit=5" - print(f"🔍 Fetching iTunes artist image: {itunes_url}") + print(f"Fetching iTunes artist image: {itunes_url}") resp = requests.get(itunes_url, timeout=5) image_url = None @@ -38459,11 +38459,11 @@ def add_to_watchlist(): if image_url: database.update_watchlist_artist_image(artist_id, image_url) - print(f"✅ Cached {fallback_source} artist image for {artist_name}") + print(f"Cached {fallback_source} artist image for {artist_name}") else: - print(f"⚠️ No artwork found for {fallback_source} artist {artist_name}") + print(f"No artwork found for {fallback_source} artist {artist_name}") except Exception as fb_error: - print(f"⚠️ Error fetching {fallback_source} artwork: {fb_error}") + print(f"Error fetching {fallback_source} artwork: {fb_error}") elif spotify_client and spotify_client.is_authenticated(): # For Spotify artists, fetch from Spotify API artist_data = spotify_client.get_artist(artist_id) @@ -38478,16 +38478,16 @@ def add_to_watchlist(): # Update in database if image_url: database.update_watchlist_artist_image(artist_id, image_url) - print(f"✅ Cached artist image for {artist_name}") + print(f"Cached artist image for {artist_name}") else: - print(f"⚠️ No image URL found for {artist_name}") + print(f"No image URL found for {artist_name}") else: - print(f"⚠️ No images in Spotify data for {artist_name}") + print(f"No images in Spotify data for {artist_name}") else: - print(f"⚠️ Spotify client not available for fetching artist image") + print(f"Spotify client not available for fetching artist image") except Exception as img_error: # Don't fail the add operation if image fetch fails - print(f"⚠️ Could not fetch artist image for {artist_name}: {img_error}") + print(f"Could not fetch artist image for {artist_name}: {img_error}") # Push updated count to this profile's WebSocket room immediately try: @@ -38609,7 +38609,7 @@ def add_batch_to_watchlist(): if image_url: database.update_watchlist_artist_image(artist_id, image_url) except Exception as img_error: - print(f"⚠️ Could not fetch artist image for {artist_name}: {img_error}") + print(f"Could not fetch artist image for {artist_name}: {img_error}") return jsonify({ "success": True, @@ -38810,7 +38810,7 @@ def start_watchlist_scan(): with watchlist_timer_lock: watchlist_auto_scanning = True watchlist_auto_scanning_timestamp = time.time() - print(f"🔒 [Manual Watchlist Scan] Flag set at timestamp {watchlist_auto_scanning_timestamp}") + print(f"[Manual Watchlist Scan] Flag set at timestamp {watchlist_auto_scanning_timestamp}") # Get list of artists to scan (for the current profile) database = get_database() @@ -38848,10 +38848,10 @@ def start_watchlist_scan(): pass for _bf_provider in providers_to_backfill: try: - print(f"🔍 Checking for missing {_bf_provider} IDs in watchlist...") + print(f"Checking for missing {_bf_provider} IDs in watchlist...") scanner._backfill_missing_ids(watchlist_artists, _bf_provider) except Exception as backfill_error: - print(f"⚠️ Error during {_bf_provider} ID backfilling: {backfill_error}") + print(f"Error during {_bf_provider} ID backfilling: {backfill_error}") # Continue with next provider # IMAGE BACKFILL — fix watchlist artists with missing images @@ -38869,7 +38869,7 @@ def start_watchlist_scan(): imageless = cursor.fetchall() if imageless: - print(f"🖼️ Backfilling images for {len(imageless)} watchlist artists...") + print(f"Backfilling images for {len(imageless)} watchlist artists...") filled = 0 for row in imageless: name = row['artist_name'] @@ -38928,9 +38928,9 @@ def start_watchlist_scan(): filled += 1 if filled: - print(f"🖼️ Backfilled {filled}/{len(imageless)} watchlist artist images") + print(f"Backfilled {filled}/{len(imageless)} watchlist artist images") except Exception as img_err: - print(f"⚠️ Image backfill error: {img_err}") + print(f"Image backfill error: {img_err}") # Initialize detailed progress tracking watchlist_scan_state.update({ @@ -38976,7 +38976,7 @@ def start_watchlist_scan(): artist_delay = base_artist_delay album_delay = base_album_delay - print(f"📊 Scan parameters: {artist_count} artists, lookback={lookback_period}, " + print(f"Scan parameters: {artist_count} artists, lookback={lookback_period}, " f"delays: {artist_delay:.1f}s/artist, {album_delay:.1f}s/album") # Circuit breaker: pause scan on consecutive rate-limit failures @@ -38987,7 +38987,7 @@ def start_watchlist_scan(): for i, artist in enumerate(watchlist_artists): # Check for cancel request if watchlist_scan_state.get('cancel_requested'): - print(f"🛑 [Manual Watchlist Scan] Cancel requested after {i}/{len(watchlist_artists)} artists") + print(f"[Manual Watchlist Scan] Cancel requested after {i}/{len(watchlist_artists)} artists") watchlist_scan_state['status'] = 'cancelled' watchlist_scan_state['current_phase'] = 'cancelled' watchlist_scan_state['summary'] = { @@ -39124,7 +39124,7 @@ def start_watchlist_scan(): 'error_message': None })()) - print(f"✅ Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist") + print(f"Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist") # Fetch similar artists for discovery feature # This is critical for the discover page to work @@ -39143,7 +39143,7 @@ def start_watchlist_scan(): scanner.update_similar_artists(artist, profile_id=scan_profile_id) print(f" Similar artists updated for {artist.artist_name}") except Exception as similar_error: - print(f" ⚠️ Failed to update similar artists for {artist.artist_name}: {similar_error}") + print(f" Failed to update similar artists for {artist.artist_name}: {similar_error}") # Delay between artists if i < len(watchlist_artists) - 1: @@ -39162,7 +39162,7 @@ def start_watchlist_scan(): if "429" in error_str or "rate limit" in error_str: consecutive_failures += 1 if consecutive_failures >= CIRCUIT_BREAKER_THRESHOLD: - print(f"🛑 Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s") + print(f"Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s") watchlist_scan_state['current_phase'] = 'circuit_breaker_pause' time.sleep(circuit_breaker_pause) circuit_breaker_pause = min(circuit_breaker_pause * 2, 600) @@ -39204,23 +39204,23 @@ def start_watchlist_scan(): print(f"Watchlist scan completed: {len(successful_scans)}/{len(scan_results)} artists scanned successfully") print(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist") else: - print(f"🛑 Watchlist scan cancelled — skipping post-scan steps") + print(f"Watchlist scan cancelled — skipping post-scan steps") # Post-scan steps — skip if cancelled if not was_cancelled: # Populate discovery pool from similar artists - print("🎵 Starting discovery pool population...") + print("Starting discovery pool population...") watchlist_scan_state['current_phase'] = 'populating_discovery_pool' try: scanner.populate_discovery_pool(profile_id=scan_profile_id) - print("✅ Discovery pool population complete") + print("Discovery pool population complete") except Exception as discovery_error: - print(f"⚠️ Error populating discovery pool: {discovery_error}") + print(f"Error populating discovery pool: {discovery_error}") import traceback traceback.print_exc() # Update ListenBrainz playlists cache - print("🧠 Starting ListenBrainz playlists update...") + print("Starting ListenBrainz playlists update...") watchlist_scan_state['current_phase'] = 'updating_listenbrainz' try: from core.listenbrainz_manager import ListenBrainzManager @@ -39233,22 +39233,22 @@ def start_watchlist_scan(): lb_manager = ListenBrainzManager(db_path, profile_id=lb_prof['id'], token=lb_prof['token'], base_url=lb_prof['base_url']) lb_result = lb_manager.update_all_playlists() if lb_result.get('success'): - print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {lb_result.get('summary', {})}") + print(f"ListenBrainz update complete for profile {lb_prof['id']}: {lb_result.get('summary', {})}") else: # Fallback: use global config token lb_manager = ListenBrainzManager(db_path) lb_result = lb_manager.update_all_playlists() if lb_result.get('success'): - print(f"✅ ListenBrainz update complete (global): {lb_result.get('summary', {})}") + print(f"ListenBrainz update complete (global): {lb_result.get('summary', {})}") elif lb_result.get('error'): - print(f"⚠️ ListenBrainz update skipped: {lb_result.get('error')}") + print(f"ListenBrainz update skipped: {lb_result.get('error')}") except Exception as lb_error: - print(f"⚠️ Error updating ListenBrainz: {lb_error}") + print(f"Error updating ListenBrainz: {lb_error}") import traceback traceback.print_exc() # Update current seasonal playlist (weekly refresh) - print("🎃 Starting seasonal content update...") + print("Starting seasonal content update...") watchlist_scan_state['current_phase'] = 'updating_seasonal' try: from core.seasonal_discovery import get_seasonal_discovery_service @@ -39258,27 +39258,27 @@ def start_watchlist_scan(): current_season = seasonal_service.get_current_season() if current_season: if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7): - print(f"🎃 Updating {current_season} seasonal content...") + print(f"Updating {current_season} seasonal content...") seasonal_service.populate_seasonal_content(current_season) seasonal_service.curate_seasonal_playlist(current_season) - print(f"✅ {current_season.capitalize()} seasonal content updated") + print(f"{current_season.capitalize()} seasonal content updated") else: - print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping") + print(f"{current_season.capitalize()} seasonal content recently updated, skipping") else: print("ℹ️ No active season at this time") except Exception as seasonal_error: - print(f"⚠️ Error updating seasonal content: {seasonal_error}") + print(f"Error updating seasonal content: {seasonal_error}") import traceback traceback.print_exc() # Sync Spotify library cache - print("📚 Syncing Spotify library cache...") + print("Syncing Spotify library cache...") watchlist_scan_state['current_phase'] = 'syncing_spotify_library' try: scanner.sync_spotify_library_cache(profile_id=scan_profile_id) - print("✅ Spotify library cache sync complete") + print("Spotify library cache sync complete") except Exception as lib_error: - print(f"⚠️ Error syncing Spotify library: {lib_error}") + print(f"Error syncing Spotify library: {lib_error}") except Exception as e: print(f"Error during watchlist scan: {e}") @@ -39299,7 +39299,7 @@ def start_watchlist_scan(): with watchlist_timer_lock: watchlist_auto_scanning = False watchlist_auto_scanning_timestamp = 0 - print("🔓 [Manual Watchlist Scan] Flag reset - scan complete") + print("[Manual Watchlist Scan] Flag reset - scan complete") # Initialize scan state global watchlist_scan_state @@ -39362,7 +39362,7 @@ def cancel_watchlist_scan(): return jsonify({"success": False, "error": "No scan is currently running"}), 400 watchlist_scan_state['cancel_requested'] = True - print("🛑 [Watchlist Scan] Cancel requested by user") + print("[Watchlist Scan] Cancel requested by user") return jsonify({"success": True, "message": "Cancel request sent"}) except Exception as e: @@ -39609,7 +39609,7 @@ def watchlist_artist_config(artist_id): conn.close() - print(f"✅ Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}, live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, compilations={include_compilations}, instrumentals={include_instrumentals}") + print(f"Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}, live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, compilations={include_compilations}, instrumentals={include_instrumentals}") return jsonify({ "success": True, @@ -39692,7 +39692,7 @@ def watchlist_artist_link_provider(artist_id): conn.close() action = 'Cleared' if is_clear else 'Linked' - print(f"✅ {action} watchlist artist '{artist_name}' {provider} ID: {new_provider_id or 'NULL'}") + print(f"{action} watchlist artist '{artist_name}' {provider} ID: {new_provider_id or 'NULL'}") return jsonify({ "success": True, @@ -39756,7 +39756,7 @@ def watchlist_global_config(): config_manager.set('watchlist.global_include_instrumentals', include_instrumentals) config_manager.set('watchlist.exclude_terms', exclude_terms) - print(f"✅ Updated global watchlist config: override={global_override_enabled}, " + print(f"Updated global watchlist config: override={global_override_enabled}, " f"albums={include_albums}, eps={include_eps}, singles={include_singles}, " f"live={include_live}, remixes={include_remixes}, acoustic={include_acoustic}, " f"compilations={include_compilations}, instrumentals={include_instrumentals}, " @@ -39798,7 +39798,7 @@ def _apply_watchlist_global_overrides(watchlist_artists): g_acoustic = config_manager.get('watchlist.global_include_acoustic', False) g_compilations = config_manager.get('watchlist.global_include_compilations', False) g_instrumentals = config_manager.get('watchlist.global_include_instrumentals', False) - print(f"🌐 [Watchlist] Global override is ACTIVE — applying to {len(watchlist_artists)} artists " + print(f"[Watchlist] Global override is ACTIVE — applying to {len(watchlist_artists)} artists " f"(albums={g_albums}, eps={g_eps}, singles={g_singles}, live={g_live}, " f"remixes={g_remixes}, acoustic={g_acoustic}, compilations={g_compilations}, " f"instrumentals={g_instrumentals})") @@ -39821,7 +39821,7 @@ def _update_similar_artists_worker(): from database.music_database import get_database import time - print("🎵 [Similar Artists] Starting similar artists update...") + print("[Similar Artists] Starting similar artists update...") database = get_database() all_profiles = database.get_all_profiles() @@ -39838,11 +39838,11 @@ def _update_similar_artists_worker(): if not artist_profiles: similar_artists_update_state['status'] = 'completed' - print("📭 [Similar Artists] No watchlist artists to process") + print("[Similar Artists] No watchlist artists to process") return similar_artists_update_state['total_artists'] = len(artist_profiles) - print(f"📊 [Similar Artists] Processing {len(artist_profiles)} unique watchlist artists across {len(all_profiles)} profiles") + print(f"[Similar Artists] Processing {len(artist_profiles)} unique watchlist artists across {len(all_profiles)} profiles") scanner = get_watchlist_scanner(spotify_client) @@ -39862,17 +39862,17 @@ def _update_similar_artists_worker(): time.sleep(2.0) # 2 seconds between artists except Exception as artist_error: - print(f"❌ [Similar Artists] Error processing {artist.artist_name}: {artist_error}") + print(f"[Similar Artists] Error processing {artist.artist_name}: {artist_error}") continue # Update complete similar_artists_update_state['status'] = 'completed' similar_artists_update_state['current_artist'] = None - print(f"✅ [Similar Artists] Update complete! Processed {len(artist_profiles)} artists") + print(f"[Similar Artists] Update complete! Processed {len(artist_profiles)} artists") except Exception as e: - print(f"❌ [Similar Artists] Critical error: {e}") + print(f"[Similar Artists] Critical error: {e}") import traceback traceback.print_exc() @@ -39899,7 +39899,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): global watchlist_auto_scanning, watchlist_auto_scanning_timestamp, watchlist_scan_state scope_label = f"profile {profile_id}" if profile_id else "all profiles" - print(f"🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan ({scope_label})...") + print(f"[Auto-Watchlist] Timer triggered - starting automatic watchlist scan ({scope_label})...") _ew_state = {} @@ -39907,20 +39907,20 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): # CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock # This prevents deadlock and handles stuck flags (2-hour timeout) if is_watchlist_actually_scanning(): - print("⚠️ [Auto-Watchlist] Already scanning (verified with stuck detection), skipping.") + print("[Auto-Watchlist] Already scanning (verified with stuck detection), skipping.") return with watchlist_timer_lock: # Re-check inside lock to handle race conditions if watchlist_auto_scanning: - print("⚠️ [Auto-Watchlist] Already scanning (race condition check), skipping.") + print("[Auto-Watchlist] Already scanning (race condition check), skipping.") return # Set flag and timestamp import time watchlist_auto_scanning = True watchlist_auto_scanning_timestamp = time.time() - print(f"🔒 [Auto-Watchlist] Flag set at timestamp {watchlist_auto_scanning_timestamp}") + print(f"[Auto-Watchlist] Flag set at timestamp {watchlist_auto_scanning_timestamp}") # Use app context for database operations with app.app_context(): @@ -39939,7 +39939,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): watchlist_count = sum(database.get_watchlist_count(profile_id=p['id']) for p in scan_profiles) profile_label = f"profile {profile_id}" if profile_id else f"{len(scan_profiles)} profiles" - print(f"🔍 [Auto-Watchlist] Watchlist count check: {watchlist_count} artists found ({profile_label})") + print(f"[Auto-Watchlist] Watchlist count check: {watchlist_count} artists found ({profile_label})") if watchlist_count == 0: print("ℹ️ [Auto-Watchlist] No artists in watchlist for auto-scanning.") @@ -39955,7 +39955,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): watchlist_auto_scanning_timestamp = 0 return - print(f"👁️ [Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...") + print(f"[Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...") _update_automation_progress(automation_id, progress=5, phase='Loading watchlist', log_line=f'{watchlist_count} artists ({profile_label})', log_type='info') @@ -40019,7 +40019,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): artist_delay = base_artist_delay album_delay = base_album_delay - print(f"📊 [Auto-Watchlist] Scan parameters: {artist_count} artists, lookback={lookback_period}, " + print(f"[Auto-Watchlist] Scan parameters: {artist_count} artists, lookback={lookback_period}, " f"delays: {artist_delay:.1f}s/artist, {album_delay:.1f}s/album") # Circuit breaker: pause scan on consecutive rate-limit failures @@ -40031,7 +40031,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): for i, artist in enumerate(watchlist_artists): # Check for cancel request if watchlist_scan_state.get('cancel_requested'): - print(f"🛑 [Auto-Watchlist] Cancel requested after {i}/{len(watchlist_artists)} artists") + print(f"[Auto-Watchlist] Cancel requested after {i}/{len(watchlist_artists)} artists") watchlist_scan_state['status'] = 'cancelled' watchlist_scan_state['current_phase'] = 'cancelled' watchlist_scan_state['summary'] = { @@ -40176,7 +40176,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): 'error_message': None })()) - print(f"✅ Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist") + print(f"Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist") if artist_new_tracks > 0: _update_automation_progress(automation_id, log_line=f'{artist.artist_name} — {artist_new_tracks} new, {artist_added_tracks} added', log_type='success') @@ -40211,7 +40211,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): scanner.update_similar_artists(artist, profile_id=artist_profile_id) print(f" Similar artists updated for {artist.artist_name}") except Exception as similar_error: - print(f" ⚠️ Failed to update similar artists for {artist.artist_name}: {similar_error}") + print(f" Failed to update similar artists for {artist.artist_name}: {similar_error}") # Delay between artists if i < len(watchlist_artists) - 1: @@ -40232,7 +40232,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): if "429" in error_str or "rate limit" in error_str: consecutive_failures += 1 if consecutive_failures >= CIRCUIT_BREAKER_THRESHOLD: - print(f"🛑 [Auto-Watchlist] Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s") + print(f"[Auto-Watchlist] Circuit breaker: {consecutive_failures} consecutive rate-limit failures, pausing {circuit_breaker_pause}s") watchlist_scan_state['current_phase'] = 'circuit_breaker_pause' time.sleep(circuit_breaker_pause) circuit_breaker_pause = min(circuit_breaker_pause * 2, 600) @@ -40276,12 +40276,12 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): else: total_new_tracks = watchlist_scan_state.get('summary', {}).get('new_tracks_found', 0) total_added_to_wishlist = watchlist_scan_state.get('summary', {}).get('tracks_added_to_wishlist', 0) - print(f"🛑 Automatic watchlist scan cancelled — skipping post-scan steps") + print(f"Automatic watchlist scan cancelled — skipping post-scan steps") # Post-scan steps — skip if cancelled if not was_cancelled: # Populate discovery pool from similar artists (per-profile) - print("🎵 Starting discovery pool population...") + print("Starting discovery pool population...") watchlist_scan_state['current_phase'] = 'populating_discovery_pool' _update_automation_progress(automation_id, progress=96, phase='Populating discovery pool', log_line='Building discovery pool from similar artists...', log_type='info') @@ -40303,16 +40303,16 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): for p in all_profiles: scanner.populate_discovery_pool(profile_id=p['id'], progress_callback=_discovery_progress) - print("✅ Discovery pool population complete") + print("Discovery pool population complete") except Exception as discovery_error: - print(f"⚠️ Error populating discovery pool: {discovery_error}") + print(f"Error populating discovery pool: {discovery_error}") import traceback traceback.print_exc() _update_automation_progress(automation_id, log_line=f'Discovery pool error: {discovery_error}', log_type='error') # Update ListenBrainz playlists cache - print("🧠 Starting ListenBrainz playlists update...") + print("Starting ListenBrainz playlists update...") watchlist_scan_state['current_phase'] = 'updating_listenbrainz' _update_automation_progress(automation_id, progress=97, phase='Updating ListenBrainz', log_line='Fetching ListenBrainz playlists...', log_type='info') @@ -40327,7 +40327,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): lb_result = lb_manager.update_all_playlists() if lb_result.get('success'): summary = lb_result.get('summary', {}) - print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {summary}") + print(f"ListenBrainz update complete for profile {lb_prof['id']}: {summary}") _update_automation_progress(automation_id, log_line=f'ListenBrainz (profile {lb_prof["id"]}): playlists updated', log_type='success') else: @@ -40335,22 +40335,22 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): lb_result = lb_manager.update_all_playlists() if lb_result.get('success'): summary = lb_result.get('summary', {}) - print(f"✅ ListenBrainz update complete (global): {summary}") + print(f"ListenBrainz update complete (global): {summary}") _update_automation_progress(automation_id, log_line=f'ListenBrainz: playlists updated', log_type='success') else: - print(f"⚠️ ListenBrainz update had issues: {lb_result.get('error', 'Unknown error')}") + print(f"ListenBrainz update had issues: {lb_result.get('error', 'Unknown error')}") _update_automation_progress(automation_id, log_line=f'ListenBrainz: {lb_result.get("error", "Unknown error")}', log_type='error') except Exception as lb_error: - print(f"⚠️ Error updating ListenBrainz: {lb_error}") + print(f"Error updating ListenBrainz: {lb_error}") import traceback traceback.print_exc() _update_automation_progress(automation_id, log_line=f'ListenBrainz error: {lb_error}', log_type='error') # Update current seasonal playlist (weekly refresh) - print("🎃 Starting seasonal content update...") + print("Starting seasonal content update...") watchlist_scan_state['current_phase'] = 'updating_seasonal' _update_automation_progress(automation_id, progress=98, phase='Updating seasonal content', log_line='Checking seasonal playlists...', log_type='info') @@ -40362,16 +40362,16 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): current_season = seasonal_service.get_current_season() if current_season: if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7): - print(f"🎃 Updating {current_season} seasonal content...") + print(f"Updating {current_season} seasonal content...") _update_automation_progress(automation_id, log_line=f'Updating {current_season} seasonal content...', log_type='info') seasonal_service.populate_seasonal_content(current_season) seasonal_service.curate_seasonal_playlist(current_season) - print(f"✅ {current_season.capitalize()} seasonal content updated") + print(f"{current_season.capitalize()} seasonal content updated") _update_automation_progress(automation_id, log_line=f'{current_season.capitalize()} seasonal content updated', log_type='success') else: - print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping") + print(f"{current_season.capitalize()} seasonal content recently updated, skipping") _update_automation_progress(automation_id, log_line=f'{current_season.capitalize()} seasonal content up to date', log_type='info') else: @@ -40379,28 +40379,28 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): _update_automation_progress(automation_id, log_line='No active season', log_type='info') except Exception as seasonal_error: - print(f"⚠️ Error updating seasonal content: {seasonal_error}") + print(f"Error updating seasonal content: {seasonal_error}") import traceback traceback.print_exc() _update_automation_progress(automation_id, log_line=f'Seasonal error: {seasonal_error}', log_type='error') # Sync Spotify library cache - print("📚 Syncing Spotify library cache...") + print("Syncing Spotify library cache...") try: for p in all_profiles: scanner.sync_spotify_library_cache(profile_id=p['id']) - print("✅ Spotify library cache sync complete") + print("Spotify library cache sync complete") _update_automation_progress(automation_id, log_line='Spotify library cache synced', log_type='info') except Exception as lib_error: - print(f"⚠️ Error syncing Spotify library: {lib_error}") + print(f"Error syncing Spotify library: {lib_error}") _update_automation_progress(automation_id, log_line=f'Library cache error: {lib_error}', log_type='error') # Add activity for watchlist scan completion if total_added_to_wishlist > 0: - add_activity_item("👁️", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now") + add_activity_item("", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now") try: if automation_engine: @@ -40413,7 +40413,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): pass except Exception as e: - print(f"❌ Error in automatic watchlist scan: {e}") + print(f"Error in automatic watchlist scan: {e}") import traceback traceback.print_exc() _update_automation_progress(automation_id, log_line=f'Error: {str(e)}', log_type='error') @@ -40437,7 +40437,7 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None): watchlist_auto_scanning = False watchlist_auto_scanning_timestamp = 0 - print("✅ Automatic watchlist scanning complete") + print("Automatic watchlist scanning complete") # --- Metadata Updater System --- @@ -40485,7 +40485,7 @@ def get_discover_hero(): # Determine active source active_source = _get_active_discovery_source() - print(f"🎵 Discover hero using source: {active_source}") + print(f"Discover hero using source: {active_source}") # Import fallback client for non-Spotify lookups itunes_client = _get_metadata_fallback_client() @@ -40913,7 +40913,7 @@ def refresh_spotify_library(): database.set_metadata('spotify_library_last_sync', '') database.set_metadata('spotify_library_last_full_sync', '') scanner.sync_spotify_library_cache(profile_id=get_current_profile_id()) - print("✅ Manual Spotify library refresh complete") + print("Manual Spotify library refresh complete") except Exception as e: print(f"Error in manual Spotify library refresh: {e}") @@ -41671,10 +41671,10 @@ def refresh_seasonal_content(): try: current_season = seasonal_service.get_current_season() if current_season: - print(f"🔄 Force-refreshing seasonal content for: {current_season}") + print(f"Force-refreshing seasonal content for: {current_season}") seasonal_service.populate_seasonal_content(current_season) seasonal_service.curate_seasonal_playlist(current_season) - print(f"✅ Seasonal content refreshed for: {current_season}") + print(f"Seasonal content refreshed for: {current_season}") else: print("ℹ️ No active season to refresh") except Exception as e: @@ -42019,7 +42019,7 @@ def refresh_your_artists(): if request.args.get('clear', '').lower() == 'true': database = get_database() cleared = database.clear_liked_artists(profile_id) - print(f"🎵 [Your Artists] Cleared {cleared} entries before refresh") + print(f"[Your Artists] Cleared {cleared} entries before refresh") _trigger_your_artists_refresh(profile_id) return jsonify({"success": True, "message": "Refresh started"}) except Exception as e: @@ -42061,7 +42061,7 @@ def _fetch_and_match_liked_artists(profile_id: int): # 1. Fetch from Spotify (followed artists) try: if spotify_client and spotify_client.is_spotify_authenticated(): - print("🎵 [Your Artists] Fetching followed artists from Spotify...") + print("[Your Artists] Fetching followed artists from Spotify...") artists = spotify_client.get_followed_artists() for a in artists: database.upsert_liked_artist( @@ -42071,7 +42071,7 @@ def _fetch_and_match_liked_artists(profile_id: int): profile_id=profile_id ) fetched += len(artists) - print(f"🎵 [Your Artists] Fetched {len(artists)} from Spotify") + print(f"[Your Artists] Fetched {len(artists)} from Spotify") except Exception as e: logger.error(f"[Your Artists] Spotify fetch error: {e}") @@ -42080,7 +42080,7 @@ def _fetch_and_match_liked_artists(profile_id: int): if tidal_client and hasattr(tidal_client, 'get_favorite_artists'): tidal_auth = tidal_client._ensure_valid_token() if hasattr(tidal_client, '_ensure_valid_token') else False if tidal_auth: - print("🎵 [Your Artists] Fetching favorite artists from Tidal...") + print("[Your Artists] Fetching favorite artists from Tidal...") artists = tidal_client.get_favorite_artists(limit=200) for a in artists: database.upsert_liked_artist( @@ -42088,7 +42088,7 @@ def _fetch_and_match_liked_artists(profile_id: int): image_url=a.get('image_url'), profile_id=profile_id ) fetched += len(artists) - print(f"🎵 [Your Artists] Fetched {len(artists)} from Tidal") + print(f"[Your Artists] Fetched {len(artists)} from Tidal") except Exception as e: logger.error(f"[Your Artists] Tidal fetch error: {e}") @@ -42097,14 +42097,14 @@ def _fetch_and_match_liked_artists(profile_id: int): lastfm_key = config_manager.get('lastfm.api_key', '') lastfm_secret = config_manager.get('lastfm.api_secret', '') lastfm_session = config_manager.get('lastfm.session_key', '') - print(f"🎵 [Your Artists] Last.fm credentials: key={'yes' if lastfm_key else 'NO'}, secret={'yes' if lastfm_secret else 'NO'}, session={'yes' if lastfm_session else 'NO'}") + print(f"[Your Artists] Last.fm credentials: key={'yes' if lastfm_key else 'NO'}, secret={'yes' if lastfm_secret else 'NO'}, session={'yes' if lastfm_session else 'NO'}") if lastfm_key and lastfm_secret and lastfm_session: from core.lastfm_client import LastFMClient lfm = LastFMClient(api_key=lastfm_key, api_secret=lastfm_secret, session_key=lastfm_session) username = lfm.get_authenticated_username() - print(f"🎵 [Your Artists] Last.fm username resolved: {username or 'NONE'}") + print(f"[Your Artists] Last.fm username resolved: {username or 'NONE'}") if username: - print(f"🎵 [Your Artists] Fetching top artists from Last.fm ({username})...") + print(f"[Your Artists] Fetching top artists from Last.fm ({username})...") artists = lfm.get_user_top_artists(username, period='overall', limit=200) for a in artists: database.upsert_liked_artist( @@ -42112,7 +42112,7 @@ def _fetch_and_match_liked_artists(profile_id: int): image_url=a.get('image_url'), profile_id=profile_id ) fetched += len(artists) - print(f"🎵 [Your Artists] Fetched {len(artists)} from Last.fm") + print(f"[Your Artists] Fetched {len(artists)} from Last.fm") except Exception as e: logger.error(f"[Your Artists] Last.fm fetch error: {e}") @@ -42120,7 +42120,7 @@ def _fetch_and_match_liked_artists(profile_id: int): try: deezer_cl = _get_deezer_client() if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated(): - print("🎵 [Your Artists] Fetching favorite artists from Deezer...") + print("[Your Artists] Fetching favorite artists from Deezer...") artists = deezer_cl.get_user_favorite_artists(limit=200) for a in artists: database.upsert_liked_artist( @@ -42129,11 +42129,11 @@ def _fetch_and_match_liked_artists(profile_id: int): image_url=a.get('image_url'), profile_id=profile_id ) fetched += len(artists) - print(f"🎵 [Your Artists] Fetched {len(artists)} from Deezer") + print(f"[Your Artists] Fetched {len(artists)} from Deezer") except Exception as e: logger.error(f"[Your Artists] Deezer fetch error: {e}") - print(f"🎵 [Your Artists] Total fetched: {fetched}") + print(f"[Your Artists] Total fetched: {fetched}") # 5. Match pending artists to active source _match_liked_artists_to_all_sources(database, profile_id) @@ -42336,7 +42336,7 @@ def _match_liked_artists_to_all_sources(database, profile_id: int): matched += 1 database.sync_liked_artists_watchlist_flags(profile_id) - print(f"🎵 [Your Artists] Matched {matched}/{len(pending)} artists to {len(search_clients)} sources ({api_calls} API calls)") + print(f"[Your Artists] Matched {matched}/{len(pending)} artists to {len(search_clients)} sources ({api_calls} API calls)") # Image backfill: fetch images for matched artists that have IDs but no image _backfill_liked_artist_images(database, profile_id, search_clients) @@ -42360,7 +42360,7 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic if not rows: return - print(f"🖼️ [Your Artists] Backfilling images for {len(rows)} artists...") + print(f"[Your Artists] Backfilling images for {len(rows)} artists...") filled = 0 for row in rows: @@ -42396,7 +42396,7 @@ def _backfill_liked_artist_images(database, profile_id: int, search_clients: dic conn.commit() if filled: - print(f"🖼️ [Your Artists] Backfilled {filled}/{len(rows)} artist images") + print(f"[Your Artists] Backfilled {filled}/{len(rows)} artist images") except Exception as e: logger.debug(f"[Your Artists] Image backfill error: {e}") @@ -43759,7 +43759,7 @@ def _get_lb_discover_playlists(playlist_type): "count": 0, "username": None }) - print(f"📦 Cache empty for profile {lb_manager.profile_id}, populating ListenBrainz playlists...") + print(f"Cache empty for profile {lb_manager.profile_id}, populating ListenBrainz playlists...") lb_manager.update_all_playlists() playlists = lb_manager.get_cached_playlists(playlist_type) @@ -43827,7 +43827,7 @@ def get_listenbrainz_playlist_tracks(playlist_mbid): if not tracks: # Cache miss or stale entry with no tracks — try fetching from LB API if lb_manager.client.is_authenticated(): - print(f"🔄 Cache miss for playlist {playlist_mbid}, fetching from ListenBrainz...") + print(f"Cache miss for playlist {playlist_mbid}, fetching from ListenBrainz...") # Remove stale playlist row (if any) so _update_playlist doesn't # skip due to matching track_count with 0 actual tracks existing_type = lb_manager.get_playlist_type(playlist_mbid) or 'created_for' @@ -43914,11 +43914,11 @@ def get_all_listenbrainz_playlists(): } playlists.append(playlist_info) - print(f"📋 Returning {len(playlists)} stored ListenBrainz playlists for profile {profile_id}") + print(f"Returning {len(playlists)} stored ListenBrainz playlists for profile {profile_id}") return jsonify({"playlists": playlists}) except Exception as e: - print(f"❌ Error getting ListenBrainz playlists: {e}") + print(f"Error getting ListenBrainz playlists: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/state/', methods=['GET']) @@ -43953,7 +43953,7 @@ def get_listenbrainz_playlist_state(playlist_mbid): return jsonify(response) except Exception as e: - print(f"❌ Error getting ListenBrainz playlist state: {e}") + print(f"Error getting ListenBrainz playlist state: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/reset/', methods=['POST']) @@ -43982,11 +43982,11 @@ def reset_listenbrainz_playlist(playlist_mbid): state['discovery_future'] = None state['last_accessed'] = time.time() - print(f"🔄 Reset ListenBrainz playlist to fresh: {state['playlist']['title']}") + print(f"Reset ListenBrainz playlist to fresh: {state['playlist']['title']}") return jsonify({"success": True, "phase": "fresh"}) except Exception as e: - print(f"❌ Error resetting ListenBrainz playlist: {e}") + print(f"Error resetting ListenBrainz playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/remove/', methods=['POST']) @@ -44006,11 +44006,11 @@ def remove_listenbrainz_playlist(playlist_mbid): # Remove from state del listenbrainz_playlist_states[state_key] - print(f"🗑️ Removed ListenBrainz playlist from state: {playlist_mbid}") + print(f"Removed ListenBrainz playlist from state: {playlist_mbid}") return jsonify({"success": True}) except Exception as e: - print(f"❌ Error removing ListenBrainz playlist: {e}") + print(f"Error removing ListenBrainz playlist: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/discovery/start/', methods=['POST']) @@ -44039,7 +44039,7 @@ def start_listenbrainz_discovery(playlist_mbid): 'created_at': time.time(), 'last_accessed': time.time() } - print(f"✅ Created new ListenBrainz playlist state: {playlist_data.get('name', 'Unknown')}") + print(f"Created new ListenBrainz playlist state: {playlist_data.get('name', 'Unknown')}") else: # State already exists, update it state = listenbrainz_playlist_states[state_key] @@ -44059,17 +44059,17 @@ def start_listenbrainz_discovery(playlist_mbid): # Add activity for discovery start playlist_name = playlist_data.get('name', 'Unknown Playlist') track_count = len(playlist_data.get('tracks', [])) - add_activity_item("🔍", "ListenBrainz Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") + add_activity_item("", "ListenBrainz Discovery Started", f"'{playlist_name}' - {track_count} tracks", "Now") # Start discovery worker (pass state_key for profile-scoped state access) future = listenbrainz_discovery_executor.submit(_run_listenbrainz_discovery_worker, state_key) state['discovery_future'] = future - print(f"🔍 Started Spotify discovery for ListenBrainz playlist: {playlist_name}") + print(f"Started Spotify discovery for ListenBrainz playlist: {playlist_name}") return jsonify({"success": True, "message": "Discovery started"}) except Exception as e: - print(f"❌ Error starting ListenBrainz discovery: {e}") + print(f"Error starting ListenBrainz discovery: {e}") import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @@ -44098,7 +44098,7 @@ def get_listenbrainz_discovery_status(playlist_mbid): return jsonify(response) except Exception as e: - print(f"❌ Error getting ListenBrainz discovery status: {e}") + print(f"Error getting ListenBrainz discovery status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/update-phase/', methods=['POST']) @@ -44122,14 +44122,14 @@ def update_listenbrainz_phase(playlist_mbid): # Update download process ID if provided (for download persistence) if 'download_process_id' in data: state['download_process_id'] = data['download_process_id'] - logger.info(f"🎵 Updated ListenBrainz download_process_id: {data['download_process_id']}") + logger.info(f"Updated ListenBrainz download_process_id: {data['download_process_id']}") # Update converted Spotify playlist ID if provided (for download persistence) if 'converted_spotify_playlist_id' in data: state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id'] - logger.info(f"🎵 Updated ListenBrainz converted_spotify_playlist_id: {data['converted_spotify_playlist_id']}") + logger.info(f"Updated ListenBrainz converted_spotify_playlist_id: {data['converted_spotify_playlist_id']}") - logger.info(f"🎵 Updated ListenBrainz playlist {playlist_mbid} phase to: {new_phase}") + logger.info(f"Updated ListenBrainz playlist {playlist_mbid} phase to: {new_phase}") return jsonify({ "success": True, @@ -44137,7 +44137,7 @@ def update_listenbrainz_phase(playlist_mbid): }) except Exception as e: - print(f"❌ Error updating ListenBrainz playlist phase: {e}") + print(f"Error updating ListenBrainz playlist phase: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/discovery/update_match', methods=['POST']) @@ -44170,7 +44170,7 @@ def update_listenbrainz_discovery_match(): state['spotify_matches'] -= 1 # Update result - result['status'] = '✅ Found' if spotify_track else '❌ Not Found' + result['status'] = 'Found' if spotify_track else 'Not Found' result['status_class'] = 'found' if spotify_track else 'not-found' result['spotify_track'] = spotify_track.get('name', '') if spotify_track else '' # Join all artists (matching YouTube/Tidal/Beatport format) @@ -44195,13 +44195,13 @@ def update_listenbrainz_discovery_match(): result['manual_match'] = True - print(f"✅ Updated ListenBrainz match for track {track_index}: {result['status']}") + print(f"Updated ListenBrainz match for track {track_index}: {result['status']}") return jsonify({'success': True}) else: return jsonify({'error': 'Invalid track index'}), 400 except Exception as e: - print(f"❌ Error updating ListenBrainz discovery match: {e}") + print(f"Error updating ListenBrainz discovery match: {e}") import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @@ -44239,7 +44239,7 @@ def convert_listenbrainz_results_to_spotify_tracks(discovery_results): } spotify_tracks.append(track) - print(f"🔄 Converted {len(spotify_tracks)} ListenBrainz matches to Spotify tracks for sync") + print(f"Converted {len(spotify_tracks)} ListenBrainz matches to Spotify tracks for sync") return spotify_tracks @app.route('/api/wing-it/sync', methods=['POST']) @@ -44284,7 +44284,7 @@ def wing_it_sync(): sync_playlist_id = f"wing_it_sync_{int(time.time())}" - add_activity_item("⚡", "Wing It Sync Started", f"'{playlist_name}' — {len(sync_tracks)} tracks", "Now") + add_activity_item("", "Wing It Sync Started", f"'{playlist_name}' — {len(sync_tracks)} tracks", "Now") with sync_lock: sync_states[sync_playlist_id] = {"status": "starting", "progress": {}} @@ -44296,7 +44296,7 @@ def wing_it_sync(): future = sync_executor.submit(_run_sync_task, sync_playlist_id, playlist_name, sync_tracks, None, get_current_profile_id()) active_sync_workers[sync_playlist_id] = future - logger.info(f"⚡ [Wing It] Started sync for: {playlist_name} ({len(sync_tracks)} tracks)") + logger.info(f"[Wing It] Started sync for: {playlist_name} ({len(sync_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) except Exception as e: @@ -44329,7 +44329,7 @@ def start_listenbrainz_sync(playlist_mbid): playlist_name = state['playlist']['name'] # Add activity for sync start - add_activity_item("🔄", "ListenBrainz Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") + add_activity_item("", "ListenBrainz Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now") # Update ListenBrainz state state['phase'] = 'syncing' @@ -44350,11 +44350,11 @@ def start_listenbrainz_sync(playlist_mbid): future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) active_sync_workers[sync_playlist_id] = future - print(f"🔄 Started ListenBrainz sync for: {playlist_name} ({len(spotify_tracks)} tracks)") + print(f"Started ListenBrainz sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) except Exception as e: - print(f"❌ Error starting ListenBrainz sync: {e}") + print(f"Error starting ListenBrainz sync: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/sync/status/', methods=['GET']) @@ -44390,16 +44390,16 @@ def get_listenbrainz_sync_status(playlist_mbid): state['sync_progress'] = sync_state.get('progress', {}) # Add activity for sync completion playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("🔄", "Sync Complete", f"ListenBrainz playlist '{playlist_name}' synced successfully", "Now") + add_activity_item("", "Sync Complete", f"ListenBrainz playlist '{playlist_name}' synced successfully", "Now") elif sync_state.get('status') == 'error': state['phase'] = 'discovered' # Revert on error playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("❌", "Sync Failed", f"ListenBrainz playlist '{playlist_name}' sync failed", "Now") + add_activity_item("", "Sync Failed", f"ListenBrainz playlist '{playlist_name}' sync failed", "Now") return jsonify(response) except Exception as e: - print(f"❌ Error getting ListenBrainz sync status: {e}") + print(f"Error getting ListenBrainz sync status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/listenbrainz/sync/cancel/', methods=['POST']) @@ -44431,7 +44431,7 @@ def cancel_listenbrainz_sync(playlist_mbid): return jsonify({"success": True, "message": "ListenBrainz sync cancelled"}) except Exception as e: - print(f"❌ Error cancelling ListenBrainz sync: {e}") + print(f"Error cancelling ListenBrainz sync: {e}") return jsonify({"error": str(e)}), 500 @@ -44456,7 +44456,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid): # Convert to our standard format - prepare tracks first without cover art tracks = [] - print(f"🎵 Processing {len(jspf_tracks)} tracks from playlist") + print(f"Processing {len(jspf_tracks)} tracks from playlist") # First pass: extract all track data without cover art track_data_list = [] @@ -44474,8 +44474,8 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid): mb_data = extension.get('https://musicbrainz.org/doc/jspf#track', {}) if idx == 0: - print(f"📋 Sample track extension data: {extension}") - print(f"📋 Sample mb_data keys: {mb_data.keys() if mb_data else 'None'}") + print(f"Sample track extension data: {extension}") + print(f"Sample mb_data keys: {mb_data.keys() if mb_data else 'None'}") # Extract release MBID for cover art release_mbid = None @@ -44535,7 +44535,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid): return None - print(f"🎨 Fetching cover art for {len(track_data_list)} tracks in parallel...") + print(f"Fetching cover art for {len(track_data_list)} tracks in parallel...") start_time = time.time() # Fetch up to 10 covers at a time @@ -44554,7 +44554,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid): elapsed = time.time() - start_time covers_found = sum(1 for t in track_data_list if t.get('album_cover_url')) - print(f"✅ Fetched {covers_found}/{len(track_data_list)} covers in {elapsed:.2f}s") + print(f"Fetched {covers_found}/{len(track_data_list)} covers in {elapsed:.2f}s") tracks = track_data_list @@ -44591,17 +44591,17 @@ def start_metadata_update(): if active_server == "jellyfin": media_client = jellyfin_client if not media_client: - add_activity_item("❌", "Metadata Update", "Jellyfin client not available", "Now") + add_activity_item("", "Metadata Update", "Jellyfin client not available", "Now") return jsonify({"success": False, "error": "Jellyfin client not available"}), 400 elif active_server == "navidrome": media_client = navidrome_client if not media_client: - add_activity_item("❌", "Metadata Update", "Navidrome client not available", "Now") + add_activity_item("", "Metadata Update", "Navidrome client not available", "Now") return jsonify({"success": False, "error": "Navidrome client not available"}), 400 else: # plex media_client = plex_client if not media_client: - add_activity_item("❌", "Metadata Update", "Plex client not available", "Now") + add_activity_item("", "Metadata Update", "Plex client not available", "Now") return jsonify({"success": False, "error": "Plex client not available"}), 400 # DEBUG: Check Plex connection details @@ -44621,7 +44621,7 @@ def start_metadata_update(): # Check Spotify client - EXACTLY like dashboard.py if not spotify_client: - add_activity_item("❌", "Metadata Update", "Spotify client not available", "Now") + add_activity_item("", "Metadata Update", "Spotify client not available", "Now") return jsonify({"success": False, "error": "Spotify client not available"}), 400 # Reset state @@ -44654,11 +44654,11 @@ def start_metadata_update(): print(f"Error in metadata update worker: {e}") metadata_update_state['status'] = 'error' metadata_update_state['error'] = str(e) - add_activity_item("❌", "Metadata Error", str(e), "Now") + add_activity_item("", "Metadata Error", str(e), "Now") metadata_update_worker = metadata_update_executor.submit(run_metadata_update) - add_activity_item("🎵", "Metadata Update", "Loading artists from library...", "Now") + add_activity_item("", "Metadata Update", "Loading artists from library...", "Now") return jsonify({"success": True}) @@ -44677,7 +44677,7 @@ def stop_metadata_update(): if metadata_update_state['status'] == 'running': metadata_update_state['status'] = 'stopping' metadata_update_state['current_artist'] = 'Stopping...' - add_activity_item("⏹️", "Metadata Update", "Stopping metadata update process", "Now") + add_activity_item("", "Metadata Update", "Stopping metadata update process", "Now") return jsonify({"success": True}) @@ -44722,7 +44722,7 @@ def get_active_media_server(): def get_beatport_genres(): """Get current Beatport genres with images dynamically scraped from homepage""" try: - logger.info("🔍 API request for Beatport genres") + logger.info("API request for Beatport genres") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44732,13 +44732,13 @@ def get_beatport_genres(): # Discover genres dynamically if include_images: - logger.info("🖼️ Including genre images in response (slower)") + logger.info("Including genre images in response (slower)") genres = scraper.discover_genres_with_images(include_images=True) else: - logger.info("📝 Returning genres without images (faster)") + logger.info("Returning genres without images (faster)") genres = scraper.discover_genres_from_homepage() - logger.info(f"✅ Successfully discovered {len(genres)} Beatport genres") + logger.info(f"Successfully discovered {len(genres)} Beatport genres") return jsonify({ "success": True, @@ -44748,7 +44748,7 @@ def get_beatport_genres(): }) except Exception as e: - logger.error(f"❌ Error fetching Beatport genres: {e}") + logger.error(f"Error fetching Beatport genres: {e}") return jsonify({ "success": False, "error": str(e), @@ -44760,7 +44760,7 @@ def get_beatport_genres(): def get_beatport_genre_tracks(genre_slug, genre_id): """Get tracks for a specific Beatport genre""" try: - logger.info(f"🎵 API request for {genre_slug} genre tracks (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre tracks (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44779,7 +44779,7 @@ def get_beatport_genre_tracks(genre_slug, genre_id): # Scrape tracks for this genre tracks = scraper.scrape_genre_charts(genre, limit=limit, enrich=enrich) - logger.info(f"✅ Successfully scraped {len(tracks)} tracks for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} tracks for {genre_slug}") return jsonify({ "success": True, @@ -44789,7 +44789,7 @@ def get_beatport_genre_tracks(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching tracks for {genre_slug}: {e}") + logger.error(f"Error fetching tracks for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -44816,8 +44816,8 @@ def extract_beatport_chart_tracks(): enrich = data.get('enrich', True) - logger.info(f"🔍 API request to extract tracks from chart: {chart_name}") - logger.info(f"🔗 Chart URL: {chart_url}") + logger.info(f"API request to extract tracks from chart: {chart_name}") + logger.info(f"Chart URL: {chart_url}") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44840,7 +44840,7 @@ def extract_beatport_chart_tracks(): if len(table_tracks) > len(tracks): tracks = table_tracks - logger.info(f"✅ Successfully extracted {len(tracks)} tracks from chart: {chart_name}") + logger.info(f"Successfully extracted {len(tracks)} tracks from chart: {chart_name}") return jsonify({ "success": True, @@ -44851,7 +44851,7 @@ def extract_beatport_chart_tracks(): }) except Exception as e: - logger.error(f"❌ Error extracting tracks from chart: {e}") + logger.error(f"Error extracting tracks from chart: {e}") return jsonify({ "success": False, "error": str(e), @@ -44863,7 +44863,7 @@ def extract_beatport_chart_tracks(): def get_beatport_genre_top_10(genre_slug, genre_id): """Get top 10 tracks for a specific Beatport genre""" try: - logger.info(f"🔥 API request for {genre_slug} genre top 10 tracks (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre top 10 tracks (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44878,7 +44878,7 @@ def get_beatport_genre_top_10(genre_slug, genre_id): # Scrape top 10 tracks for this genre tracks = scraper.scrape_genre_top_10(genre) - logger.info(f"✅ Successfully scraped {len(tracks)} top 10 tracks for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} top 10 tracks for {genre_slug}") return jsonify({ "success": True, @@ -44888,7 +44888,7 @@ def get_beatport_genre_top_10(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching top 10 tracks for {genre_slug}: {e}") + logger.error(f"Error fetching top 10 tracks for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -44900,7 +44900,7 @@ def get_beatport_genre_top_10(genre_slug, genre_id): def get_beatport_genre_releases_top_10(genre_slug, genre_id): """Get top 10 releases for a specific Beatport genre""" try: - logger.info(f"📊 API request for {genre_slug} genre top 10 releases (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre top 10 releases (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44915,7 +44915,7 @@ def get_beatport_genre_releases_top_10(genre_slug, genre_id): # Scrape top 10 releases for this genre releases = scraper.scrape_genre_releases(genre, limit=10) - logger.info(f"✅ Successfully scraped {len(releases)} top 10 releases for {genre_slug}") + logger.info(f"Successfully scraped {len(releases)} top 10 releases for {genre_slug}") return jsonify({ "success": True, @@ -44925,7 +44925,7 @@ def get_beatport_genre_releases_top_10(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching top 10 releases for {genre_slug}: {e}") + logger.error(f"Error fetching top 10 releases for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -44937,7 +44937,7 @@ def get_beatport_genre_releases_top_10(genre_slug, genre_id): def get_beatport_genre_releases_top_100(genre_slug, genre_id): """Get top 100 releases for a specific Beatport genre""" try: - logger.info(f"📊 API request for {genre_slug} genre top 100 releases (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre top 100 releases (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44955,7 +44955,7 @@ def get_beatport_genre_releases_top_100(genre_slug, genre_id): # Scrape top releases for this genre releases = scraper.scrape_genre_releases(genre, limit=limit) - logger.info(f"✅ Successfully scraped {len(releases)} top 100 releases for {genre_slug}") + logger.info(f"Successfully scraped {len(releases)} top 100 releases for {genre_slug}") return jsonify({ "success": True, @@ -44965,7 +44965,7 @@ def get_beatport_genre_releases_top_100(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching top 100 releases for {genre_slug}: {e}") + logger.error(f"Error fetching top 100 releases for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -44977,7 +44977,7 @@ def get_beatport_genre_releases_top_100(genre_slug, genre_id): def get_beatport_genre_staff_picks(genre_slug, genre_id): """Get staff picks for a specific Beatport genre""" try: - logger.info(f"⭐ API request for {genre_slug} genre staff picks (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre staff picks (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -44995,7 +44995,7 @@ def get_beatport_genre_staff_picks(genre_slug, genre_id): # Scrape staff picks for this genre tracks = scraper.scrape_genre_staff_picks(genre, limit=limit) - logger.info(f"✅ Successfully scraped {len(tracks)} staff picks for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} staff picks for {genre_slug}") return jsonify({ "success": True, @@ -45005,7 +45005,7 @@ def get_beatport_genre_staff_picks(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching staff picks for {genre_slug}: {e}") + logger.error(f"Error fetching staff picks for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45017,7 +45017,7 @@ def get_beatport_genre_staff_picks(genre_slug, genre_id): def get_beatport_genre_hype_top_10(genre_slug, genre_id): """Get hype top 10 tracks for a specific Beatport genre""" try: - logger.info(f"🚀 API request for {genre_slug} genre hype top 10 (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre hype top 10 (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45032,7 +45032,7 @@ def get_beatport_genre_hype_top_10(genre_slug, genre_id): # Scrape hype top 10 for this genre tracks = scraper.scrape_genre_hype_top_10(genre) - logger.info(f"✅ Successfully scraped {len(tracks)} hype top 10 tracks for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} hype top 10 tracks for {genre_slug}") return jsonify({ "success": True, @@ -45042,7 +45042,7 @@ def get_beatport_genre_hype_top_10(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching hype top 10 for {genre_slug}: {e}") + logger.error(f"Error fetching hype top 10 for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45054,7 +45054,7 @@ def get_beatport_genre_hype_top_10(genre_slug, genre_id): def get_beatport_genre_hype_top_100(genre_slug, genre_id): """Get hype top 100 tracks for a specific Beatport genre""" try: - logger.info(f"💥 API request for {genre_slug} genre hype top 100 (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre hype top 100 (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45069,7 +45069,7 @@ def get_beatport_genre_hype_top_100(genre_slug, genre_id): # Scrape hype top 100 for this genre tracks = scraper.scrape_genre_hype_charts(genre, limit=100) - logger.info(f"✅ Successfully scraped {len(tracks)} hype top 100 tracks for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} hype top 100 tracks for {genre_slug}") return jsonify({ "success": True, @@ -45079,7 +45079,7 @@ def get_beatport_genre_hype_top_100(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching hype top 100 for {genre_slug}: {e}") + logger.error(f"Error fetching hype top 100 for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45091,7 +45091,7 @@ def get_beatport_genre_hype_top_100(genre_slug, genre_id): def get_beatport_genre_hype_picks(genre_slug, genre_id): """Get hype picks for a specific Beatport genre""" try: - logger.info(f"⚡ API request for {genre_slug} genre hype picks (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre hype picks (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45109,7 +45109,7 @@ def get_beatport_genre_hype_picks(genre_slug, genre_id): # Scrape hype picks for this genre tracks = scraper.scrape_genre_hype_picks(genre, limit=limit) - logger.info(f"✅ Successfully scraped {len(tracks)} hype picks for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} hype picks for {genre_slug}") return jsonify({ "success": True, @@ -45119,7 +45119,7 @@ def get_beatport_genre_hype_picks(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching hype picks for {genre_slug}: {e}") + logger.error(f"Error fetching hype picks for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45131,7 +45131,7 @@ def get_beatport_genre_hype_picks(genre_slug, genre_id): def get_beatport_genre_latest_releases(genre_slug, genre_id): """Get latest releases for a specific Beatport genre""" try: - logger.info(f"🕒 API request for {genre_slug} genre latest releases (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre latest releases (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45149,7 +45149,7 @@ def get_beatport_genre_latest_releases(genre_slug, genre_id): # Scrape latest releases for this genre tracks = scraper.scrape_genre_latest_releases(genre, limit=limit) - logger.info(f"✅ Successfully scraped {len(tracks)} latest releases for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} latest releases for {genre_slug}") return jsonify({ "success": True, @@ -45159,7 +45159,7 @@ def get_beatport_genre_latest_releases(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching latest releases for {genre_slug}: {e}") + logger.error(f"Error fetching latest releases for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45171,7 +45171,7 @@ def get_beatport_genre_latest_releases(genre_slug, genre_id): def get_beatport_genre_new_charts(genre_slug, genre_id): """Get new charts for a specific Beatport genre""" try: - logger.info(f"📈 API request for {genre_slug} genre new charts (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre new charts (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45189,7 +45189,7 @@ def get_beatport_genre_new_charts(genre_slug, genre_id): # Scrape new charts for this genre tracks = scraper.scrape_genre_new_charts(genre, limit=limit) - logger.info(f"✅ Successfully scraped {len(tracks)} new charts for {genre_slug}") + logger.info(f"Successfully scraped {len(tracks)} new charts for {genre_slug}") return jsonify({ "success": True, @@ -45199,7 +45199,7 @@ def get_beatport_genre_new_charts(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching new charts for {genre_slug}: {e}") + logger.error(f"Error fetching new charts for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45211,14 +45211,14 @@ def get_beatport_genre_new_charts(genre_slug, genre_id): def get_beatport_genre_hero(genre_slug, genre_id): """Get hero slider data for a specific Beatport genre with 1-hour caching""" try: - logger.info(f"🎠 API request for {genre_slug} genre hero slider (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre hero slider (ID: {genre_id})") # Check cache first (1-hour TTL like other genre data) cache_key = f"hero_{genre_slug}_{genre_id}" cached_data = get_cached_beatport_data('genre', cache_key, genre_slug) if cached_data: - logger.info(f"💾 Returning cached hero data for {genre_slug}") + logger.info(f"Returning cached hero data for {genre_slug}") return jsonify({ "success": True, "releases": cached_data, @@ -45239,7 +45239,7 @@ def get_beatport_genre_hero(genre_slug, genre_id): # Cache the data (1-hour TTL) set_cached_beatport_data('genre', cache_key, hero_releases, genre_slug) - logger.info(f"✅ Successfully scraped and cached {len(hero_releases)} hero releases for {genre_slug}") + logger.info(f"Successfully scraped and cached {len(hero_releases)} hero releases for {genre_slug}") return jsonify({ "success": True, @@ -45251,7 +45251,7 @@ def get_beatport_genre_hero(genre_slug, genre_id): "scrape_timestamp": time.time() }) else: - logger.info(f"⚠️ No hero releases found for {genre_slug}") + logger.info(f"No hero releases found for {genre_slug}") return jsonify({ "success": False, "releases": [], @@ -45262,7 +45262,7 @@ def get_beatport_genre_hero(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching hero data for {genre_slug}: {e}") + logger.error(f"Error fetching hero data for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45276,12 +45276,12 @@ def get_beatport_genre_hero(genre_slug, genre_id): def get_beatport_genre_top10_lists(genre_slug, genre_id): """Get Top 10 lists (Beatport + Hype) for a specific genre with 1-hour caching""" try: - logger.info(f"🎵 API request for {genre_slug} Top 10 lists (ID: {genre_id})") + logger.info(f"API request for {genre_slug} Top 10 lists (ID: {genre_id})") # Check cache first (1-hour TTL) cached_data = get_cached_beatport_data('genre', 'top_10_lists', genre_slug) if cached_data: - logger.info(f"✅ Returning cached Top 10 lists for {genre_slug}") + logger.info(f"Returning cached Top 10 lists for {genre_slug}") cached_data['success'] = True cached_data['cached'] = True return jsonify(cached_data) @@ -45323,13 +45323,13 @@ def get_beatport_genre_top10_lists(genre_slug, genre_id): # Cache the data (1-hour TTL) set_cached_beatport_data('genre', 'top_10_lists', response_data, genre_slug) - logger.info(f"✅ Successfully fetched {response_data['beatport_count']} Beatport + {response_data['hype_count']} Hype Top 10 tracks for {genre_slug}") + logger.info(f"Successfully fetched {response_data['beatport_count']} Beatport + {response_data['hype_count']} Hype Top 10 tracks for {genre_slug}") response_data['success'] = True return jsonify(response_data) except Exception as e: - logger.error(f"❌ Error fetching Top 10 lists for {genre_slug}: {e}") + logger.error(f"Error fetching Top 10 lists for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45347,12 +45347,12 @@ def get_beatport_genre_top10_lists(genre_slug, genre_id): def get_beatport_genre_top10_releases(genre_slug, genre_id): """Get Top 10 releases for a specific genre using .partial-artwork elements with 1-hour caching""" try: - logger.info(f"💿 API request for {genre_slug} Top 10 releases (ID: {genre_id})") + logger.info(f"API request for {genre_slug} Top 10 releases (ID: {genre_id})") # Check cache first (1-hour TTL) cached_data = get_cached_beatport_data('genre', 'top_10_releases', genre_slug) if cached_data: - logger.info(f"✅ Returning cached Top 10 releases for {genre_slug}") + logger.info(f"Returning cached Top 10 releases for {genre_slug}") cached_data['success'] = True cached_data['cached'] = True return jsonify(cached_data) @@ -45387,13 +45387,13 @@ def get_beatport_genre_top10_releases(genre_slug, genre_id): # Cache the data (1-hour TTL) set_cached_beatport_data('genre', 'top_10_releases', response_data, genre_slug) - logger.info(f"✅ Successfully fetched {response_data['releases_count']} Top 10 releases for {genre_slug}") + logger.info(f"Successfully fetched {response_data['releases_count']} Top 10 releases for {genre_slug}") response_data['success'] = True return jsonify(response_data) except Exception as e: - logger.error(f"❌ Error fetching Top 10 releases for {genre_slug}: {e}") + logger.error(f"Error fetching Top 10 releases for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45408,7 +45408,7 @@ def get_beatport_genre_top10_releases(genre_slug, genre_id): def get_beatport_genre_sections(genre_slug, genre_id): """Discover all available sections for a specific Beatport genre""" try: - logger.info(f"🔍 API request for {genre_slug} genre sections discovery (ID: {genre_id})") + logger.info(f"API request for {genre_slug} genre sections discovery (ID: {genre_id})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45423,7 +45423,7 @@ def get_beatport_genre_sections(genre_slug, genre_id): # Discover sections for this genre sections = scraper.discover_genre_page_sections(genre) - logger.info(f"✅ Successfully discovered sections for {genre_slug}") + logger.info(f"Successfully discovered sections for {genre_slug}") return jsonify({ "success": True, @@ -45432,7 +45432,7 @@ def get_beatport_genre_sections(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error discovering sections for {genre_slug}: {e}") + logger.error(f"Error discovering sections for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45443,7 +45443,7 @@ def get_beatport_genre_sections(genre_slug, genre_id): def get_beatport_top_100(): """Get Beatport Top 100 tracks""" try: - logger.info("🔥 API request for Beatport Top 100") + logger.info("API request for Beatport Top 100") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45455,7 +45455,7 @@ def get_beatport_top_100(): # Scrape Top 100 tracks = scraper.scrape_top_100(limit=limit, enrich=enrich) - logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Top 100") + logger.info(f"Successfully scraped {len(tracks)} tracks from Beatport Top 100") return jsonify({ "success": True, @@ -45465,7 +45465,7 @@ def get_beatport_top_100(): }) except Exception as e: - logger.error(f"❌ Error fetching Beatport Top 100: {e}") + logger.error(f"Error fetching Beatport Top 100: {e}") return jsonify({ "success": False, "error": str(e), @@ -45477,7 +45477,7 @@ def get_beatport_top_100(): def get_beatport_genre_image(genre_slug, genre_id): """Get image for a specific Beatport genre""" try: - logger.info(f"🖼️ API request for {genre_slug} genre image") + logger.info(f"API request for {genre_slug} genre image") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45489,7 +45489,7 @@ def get_beatport_genre_image(genre_slug, genre_id): image_url = scraper.get_genre_image(genre_url) if image_url: - logger.info(f"✅ Found image for {genre_slug}") + logger.info(f"Found image for {genre_slug}") return jsonify({ "success": True, "image_url": image_url, @@ -45497,7 +45497,7 @@ def get_beatport_genre_image(genre_slug, genre_id): "genre_id": genre_id }) else: - logger.info(f"⚠️ No image found for {genre_slug}") + logger.info(f"No image found for {genre_slug}") return jsonify({ "success": False, "image_url": None, @@ -45506,7 +45506,7 @@ def get_beatport_genre_image(genre_slug, genre_id): }) except Exception as e: - logger.error(f"❌ Error fetching image for {genre_slug}: {e}") + logger.error(f"Error fetching image for {genre_slug}: {e}") return jsonify({ "success": False, "error": str(e), @@ -45517,7 +45517,7 @@ def get_beatport_genre_image(genre_slug, genre_id): def get_beatport_hype_top_100(): """Get Beatport Hype Top 100 - Improved with fixed URL""" try: - logger.info("🔥 API request for Beatport Hype Top 100") + logger.info("API request for Beatport Hype Top 100") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45529,7 +45529,7 @@ def get_beatport_hype_top_100(): # Scrape Hype Top 100 using improved method tracks = scraper.scrape_hype_top_100(limit=limit, enrich=enrich) - logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Hype Top 100") + logger.info(f"Successfully scraped {len(tracks)} tracks from Beatport Hype Top 100") return jsonify({ "success": True, @@ -45539,7 +45539,7 @@ def get_beatport_hype_top_100(): }) except Exception as e: - logger.error(f"❌ Error fetching Beatport Hype Top 100: {e}") + logger.error(f"Error fetching Beatport Hype Top 100: {e}") return jsonify({ "success": False, "error": str(e), @@ -45551,7 +45551,7 @@ def get_beatport_hype_top_100(): def get_beatport_top_100_releases(): """Get Beatport Top 100 Releases - New endpoint""" try: - logger.info("📊 API request for Beatport Top 100 Releases") + logger.info("API request for Beatport Top 100 Releases") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45562,7 +45562,7 @@ def get_beatport_top_100_releases(): # Scrape Top 100 Releases using new method tracks = scraper.scrape_top_100_releases(limit=limit) - logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Top 100 Releases") + logger.info(f"Successfully scraped {len(tracks)} tracks from Beatport Top 100 Releases") return jsonify({ "success": True, @@ -45572,7 +45572,7 @@ def get_beatport_top_100_releases(): }) except Exception as e: - logger.error(f"❌ Error fetching Beatport Top 100 Releases: {e}") + logger.error(f"Error fetching Beatport Top 100 Releases: {e}") return jsonify({ "success": False, "error": str(e), @@ -45593,7 +45593,7 @@ def get_beatport_homepage_new_releases(): # Get new releases from homepage new_releases = scraper.scrape_new_releases(limit=limit) - logger.info(f"✅ Successfully extracted {len(new_releases)} new releases from homepage") + logger.info(f"Successfully extracted {len(new_releases)} new releases from homepage") return jsonify({ "success": True, @@ -45603,7 +45603,7 @@ def get_beatport_homepage_new_releases(): }) except Exception as e: - logger.error(f"❌ Error getting Beatport homepage new releases: {e}") + logger.error(f"Error getting Beatport homepage new releases: {e}") return jsonify({ "success": False, "error": str(e), @@ -45616,7 +45616,7 @@ def get_beatport_homepage_hype_picks(): """Get Beatport Hype Picks from homepage section""" try: limit = int(request.args.get('limit', 40)) - logger.info(f"🔥 API request for Beatport homepage Hype Picks (limit: {limit})") + logger.info(f"API request for Beatport homepage Hype Picks (limit: {limit})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45624,7 +45624,7 @@ def get_beatport_homepage_hype_picks(): # Get hype picks from homepage hype_picks = scraper.scrape_hype_picks_homepage(limit=limit) - logger.info(f"✅ Successfully extracted {len(hype_picks)} hype picks from homepage") + logger.info(f"Successfully extracted {len(hype_picks)} hype picks from homepage") return jsonify({ "success": True, @@ -45634,7 +45634,7 @@ def get_beatport_homepage_hype_picks(): }) except Exception as e: - logger.error(f"❌ Error getting Beatport homepage hype picks: {e}") + logger.error(f"Error getting Beatport homepage hype picks: {e}") return jsonify({ "success": False, "error": str(e), @@ -45647,7 +45647,7 @@ def get_beatport_homepage_top_10_releases(): """Get Beatport Top 10 Releases from homepage section""" try: limit = int(request.args.get('limit', 10)) - logger.info(f"🔟 API request for Beatport homepage Top 10 Releases (limit: {limit})") + logger.info(f"API request for Beatport homepage Top 10 Releases (limit: {limit})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45655,7 +45655,7 @@ def get_beatport_homepage_top_10_releases(): # Get top 10 releases from homepage top_10_releases = scraper.scrape_top_10_releases_homepage(limit=limit) - logger.info(f"✅ Successfully extracted {len(top_10_releases)} top 10 releases from homepage") + logger.info(f"Successfully extracted {len(top_10_releases)} top 10 releases from homepage") return jsonify({ "success": True, @@ -45665,7 +45665,7 @@ def get_beatport_homepage_top_10_releases(): }) except Exception as e: - logger.error(f"❌ Error getting Beatport homepage top 10 releases: {e}") + logger.error(f"Error getting Beatport homepage top 10 releases: {e}") return jsonify({ "success": False, "error": str(e), @@ -45677,17 +45677,17 @@ def get_beatport_homepage_top_10_releases(): def get_beatport_homepage_top10_lists(): """Get Beatport Top 10 Lists from homepage - both Beatport Top 10 and Hype Top 10""" try: - logger.info("🏆 API request for Beatport homepage Top 10 Lists") + logger.info("API request for Beatport homepage Top 10 Lists") # Check cache first cached_data = get_cached_beatport_data('homepage', 'top_10_lists') if cached_data: - logger.info("🏆 Returning cached top 10 lists data") + logger.info("Returning cached top 10 lists data") response = jsonify(cached_data) return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh top 10 lists data...") + logger.info("Cache miss - scraping fresh top 10 lists data...") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45695,7 +45695,7 @@ def get_beatport_homepage_top10_lists(): # Get top 10 lists from homepage top10_lists = scraper.scrape_homepage_top10_lists() - logger.info(f"✅ Successfully extracted Beatport Top 10: {len(top10_lists['beatport_top10'])}, Hype Top 10: {len(top10_lists['hype_top10'])}") + logger.info(f"Successfully extracted Beatport Top 10: {len(top10_lists['beatport_top10'])}, Hype Top 10: {len(top10_lists['hype_top10'])}") # Prepare response data response_data = { @@ -45714,7 +45714,7 @@ def get_beatport_homepage_top10_lists(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error getting Beatport homepage top 10 lists: {e}") + logger.error(f"Error getting Beatport homepage top 10 lists: {e}") return jsonify({ "success": False, "error": str(e), @@ -45728,17 +45728,17 @@ def get_beatport_homepage_top10_lists(): def get_beatport_homepage_top10_releases_cards(): """Get Beatport Top 10 Releases CARDS from homepage (not individual tracks)""" try: - logger.info("💿 API request for Beatport homepage Top 10 Releases CARDS") + logger.info("API request for Beatport homepage Top 10 Releases CARDS") # Check cache first cached_data = get_cached_beatport_data('homepage', 'top_10_releases') if cached_data: - logger.info("💿 Returning cached top 10 releases data") + logger.info("Returning cached top 10 releases data") response = jsonify(cached_data) return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh top 10 releases data...") + logger.info("Cache miss - scraping fresh top 10 releases data...") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45746,13 +45746,13 @@ def get_beatport_homepage_top10_releases_cards(): # Get top 10 releases from homepage top10_releases = scraper.scrape_homepage_top10_releases() - logger.info(f"✅ API extracted {len(top10_releases)} Top 10 Release Cards") + logger.info(f"API extracted {len(top10_releases)} Top 10 Release Cards") # Debug: Log first release if any if top10_releases: logger.info(f"First release: {top10_releases[0].get('title', 'No title')} by {top10_releases[0].get('artist', 'No artist')}") else: - logger.warning("❌ No releases found by scraper") + logger.warning("No releases found by scraper") # Prepare response data response_data = { @@ -45769,7 +45769,7 @@ def get_beatport_homepage_top10_releases_cards(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error getting Beatport homepage Top 10 Releases cards: {e}") + logger.error(f"Error getting Beatport homepage Top 10 Releases cards: {e}") import traceback logger.error(f"Full traceback: {traceback.format_exc()}") return jsonify({ @@ -45803,7 +45803,7 @@ def scrape_beatport_releases(): "track_count": 0 }), 400 - logger.info(f"🎯 API request to scrape {len(release_urls)} release URLs with source: {source_name}") + logger.info(f"API request to scrape {len(release_urls)} release URLs with source: {source_name}") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -45811,7 +45811,7 @@ def scrape_beatport_releases(): # Use our new general scraper function tracks = scraper.scrape_multiple_releases(release_urls, source_name) - logger.info(f"✅ Successfully extracted {len(tracks)} tracks from {len(release_urls)} releases") + logger.info(f"Successfully extracted {len(tracks)} tracks from {len(release_urls)} releases") # Apply text cleaning to track data cleaned_tracks = [] @@ -45834,7 +45834,7 @@ def scrape_beatport_releases(): }) except Exception as e: - logger.error(f"❌ Error scraping releases: {e}") + logger.error(f"Error scraping releases: {e}") import traceback logger.error(f"Full traceback: {traceback.format_exc()}") return jsonify({ @@ -45856,7 +45856,7 @@ def get_beatport_release_metadata(): if not release_url: return jsonify({"success": False, "error": "No release_url provided"}), 400 - logger.info(f"🎯 API request for release metadata: {release_url}") + logger.info(f"API request for release metadata: {release_url}") scraper = BeatportUnifiedScraper() result = scraper.get_release_metadata(release_url) @@ -45877,12 +45877,12 @@ def get_beatport_release_metadata(): # Update the embedded album name too track['album']['name'] = album['name'] - logger.info(f"✅ Release metadata: {album['name']} by {artist['name']} ({len(result['tracks'])} tracks)") + logger.info(f"Release metadata: {album['name']} by {artist['name']} ({len(result['tracks'])} tracks)") return jsonify(result) except Exception as e: - logger.error(f"❌ Error getting release metadata: {e}") + logger.error(f"Error getting release metadata: {e}") import traceback logger.error(f"Full traceback: {traceback.format_exc()}") return jsonify({"success": False, "error": str(e)}), 500 @@ -45905,7 +45905,7 @@ def enrich_beatport_tracks(): enrichment_id = data.get('enrichment_id', str(uuid.uuid4())) - logger.info(f"🎯 Enriching {len(tracks)} Beatport tracks with per-track metadata (id: {enrichment_id})") + logger.info(f"Enriching {len(tracks)} Beatport tracks with per-track metadata (id: {enrichment_id})") # --- Check enrichment cache (fast, do before spawning background) --- cached_results = {} @@ -45927,7 +45927,7 @@ def enrich_beatport_tracks(): cache_hits = len(cached_results) cache_misses = len(uncached_tracks) - logger.info(f"📦 Enrichment cache: {cache_hits} hits, {cache_misses} misses") + logger.info(f"Enrichment cache: {cache_hits} hits, {cache_misses} misses") # All cached — return immediately (no background task needed) if cache_misses == 0: @@ -45960,7 +45960,7 @@ def enrich_beatport_tracks(): task['completed'] = new_completed task['current_track'] = track_name else: - logger.warning(f"⚠️ on_progress: task {enrichment_id} not found in _enrichment_tasks!") + logger.warning(f"on_progress: task {enrichment_id} not found in _enrichment_tasks!") scraper = BeatportUnifiedScraper() newly_enriched = scraper.enrich_chart_tracks(uncached_tracks, progress_callback=on_progress) @@ -45990,7 +45990,7 @@ def enrich_beatport_tracks(): if merged[i] is None: merged[i] = tracks[i] - logger.info(f"✅ Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)") + logger.info(f"Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)") with _enrichment_tasks_lock: task = _enrichment_tasks.get(enrichment_id) @@ -46000,7 +46000,7 @@ def enrich_beatport_tracks(): task['completed'] = len(tracks) except Exception as e: - logger.error(f"❌ Error enriching tracks: {e}") + logger.error(f"Error enriching tracks: {e}") import traceback logger.error(f"Full traceback: {traceback.format_exc()}") with _enrichment_tasks_lock: @@ -46015,7 +46015,7 @@ def enrich_beatport_tracks(): return jsonify({"success": True, "enrichment_id": enrichment_id, "async": True}) except Exception as e: - logger.error(f"❌ Error starting enrichment: {e}") + logger.error(f"Error starting enrichment: {e}") import traceback logger.error(f"Full traceback: {traceback.format_exc()}") return jsonify({"success": False, "error": str(e)}), 500 @@ -46051,7 +46051,7 @@ def get_beatport_homepage_featured_charts(): """Get Beatport Featured Charts from homepage section""" try: limit = int(request.args.get('limit', 20)) - logger.info(f"📊 API request for Beatport homepage Featured Charts (limit: {limit})") + logger.info(f"API request for Beatport homepage Featured Charts (limit: {limit})") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -46059,7 +46059,7 @@ def get_beatport_homepage_featured_charts(): # Get featured charts from homepage featured_charts = scraper.scrape_featured_charts(limit=limit) - logger.info(f"✅ Successfully extracted {len(featured_charts)} featured charts from homepage") + logger.info(f"Successfully extracted {len(featured_charts)} featured charts from homepage") return jsonify({ "success": True, @@ -46069,7 +46069,7 @@ def get_beatport_homepage_featured_charts(): }) except Exception as e: - logger.error(f"❌ Error getting Beatport homepage featured charts: {e}") + logger.error(f"Error getting Beatport homepage featured charts: {e}") return jsonify({ "success": False, "error": str(e), @@ -46081,7 +46081,7 @@ def get_beatport_homepage_featured_charts(): def get_beatport_chart_sections(): """Get dynamically discovered Beatport chart sections""" try: - logger.info("🔍 API request for Beatport chart sections discovery") + logger.info("API request for Beatport chart sections discovery") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -46089,7 +46089,7 @@ def get_beatport_chart_sections(): # Discover chart sections dynamically chart_sections = scraper.discover_chart_sections() - logger.info(f"✅ Successfully discovered chart sections") + logger.info(f"Successfully discovered chart sections") return jsonify({ "success": True, @@ -46098,7 +46098,7 @@ def get_beatport_chart_sections(): }) except Exception as e: - logger.error(f"❌ Error discovering Beatport chart sections: {e}") + logger.error(f"Error discovering Beatport chart sections: {e}") return jsonify({ "success": False, "error": str(e), @@ -46110,7 +46110,7 @@ def get_beatport_chart_sections(): def get_beatport_dj_charts_improved(): """Get Beatport DJ Charts using improved method""" try: - logger.info("🎧 API request for Beatport DJ Charts (improved)") + logger.info("API request for Beatport DJ Charts (improved)") # Initialize the Beatport scraper scraper = BeatportUnifiedScraper() @@ -46121,7 +46121,7 @@ def get_beatport_dj_charts_improved(): # Scrape DJ Charts using improved method charts = scraper.scrape_dj_charts(limit=limit) - logger.info(f"✅ Successfully scraped {len(charts)} DJ charts") + logger.info(f"Successfully scraped {len(charts)} DJ charts") return jsonify({ "success": True, @@ -46131,7 +46131,7 @@ def get_beatport_dj_charts_improved(): }) except Exception as e: - logger.error(f"❌ Error fetching Beatport DJ Charts: {e}") + logger.error(f"Error fetching Beatport DJ Charts: {e}") return jsonify({ "success": False, "error": str(e), @@ -46144,17 +46144,17 @@ def get_beatport_dj_charts_improved(): def get_beatport_hype_picks(): """Get Beatport Hype Picks for the rebuild slider grid (EXACT same pattern as new-releases)""" try: - logger.info("🔥 Fetching Beatport hype picks...") + logger.info("Fetching Beatport hype picks...") # Check cache first cached_data = get_cached_beatport_data('homepage', 'hype_picks') if cached_data: - logger.info("🔥 Returning cached hype picks data") + logger.info("Returning cached hype picks data") response = jsonify(cached_data) return add_cache_headers(response, 3600) # 1 hour # Cache miss - scrape fresh data - logger.info("🔄 Cache miss - scraping fresh hype picks data...") + logger.info("Cache miss - scraping fresh hype picks data...") # Initialize scraper scraper = BeatportUnifiedScraper() @@ -46168,7 +46168,7 @@ def get_beatport_hype_picks(): hype_pick_cards = soup.select('[data-testid="hype-picks"]') releases = [] - logger.info(f"🔍 Found {len(hype_pick_cards)} hype pick cards") + logger.info(f"Found {len(hype_pick_cards)} hype pick cards") for i, card in enumerate(hype_pick_cards[:100]): # Limit to 100 for 10 slides (same as new-releases) release_data = {} @@ -46225,7 +46225,7 @@ def get_beatport_hype_picks(): releases.append(release_data) - logger.info(f"✅ Successfully extracted {len(releases)} hype picks") + logger.info(f"Successfully extracted {len(releases)} hype picks") # Prepare response data response_data = { @@ -46243,7 +46243,7 @@ def get_beatport_hype_picks(): return add_cache_headers(response, 3600) # 1 hour except Exception as e: - logger.error(f"❌ Error getting Beatport hype picks: {e}") + logger.error(f"Error getting Beatport hype picks: {e}") return jsonify({ 'success': False, 'error': str(e), @@ -46257,25 +46257,25 @@ def start_beatport_discovery(url_hash): """Start Spotify discovery for Beatport chart tracks""" import json try: - logger.info(f"🔍 Starting Beatport discovery for: {url_hash}") + logger.info(f"Starting Beatport discovery for: {url_hash}") # Get chart data from request body data = request.get_json() or {} - print(f"🔍 Raw request data: {data}") + print(f"Raw request data: {data}") chart_data = data.get('chart_data') - print(f"🔍 Chart data extracted: {chart_data is not None}") + print(f"Chart data extracted: {chart_data is not None}") # Debug logging if chart_data: - print(f"🔍 Chart data keys: {list(chart_data.keys()) if isinstance(chart_data, dict) else 'Not a dict'}") - print(f"🔍 Chart name: {chart_data.get('name') if isinstance(chart_data, dict) else 'N/A'}") + print(f"Chart data keys: {list(chart_data.keys()) if isinstance(chart_data, dict) else 'Not a dict'}") + print(f"Chart name: {chart_data.get('name') if isinstance(chart_data, dict) else 'N/A'}") if isinstance(chart_data, dict) and 'tracks' in chart_data: - print(f"🔍 Number of tracks: {len(chart_data['tracks'])}") + print(f"Number of tracks: {len(chart_data['tracks'])}") if chart_data['tracks']: - print(f"🔍 First track: {chart_data['tracks'][0]}") + print(f"First track: {chart_data['tracks'][0]}") else: - print("🔍 No chart data received") + print("No chart data received") if not chart_data or not chart_data.get('tracks'): return jsonify({"error": "Chart data with tracks is required"}), 400 @@ -46308,18 +46308,18 @@ def start_beatport_discovery(url_hash): # Add activity for discovery start chart_name = chart_data.get('name', 'Unknown Chart') track_count = len(chart_data['tracks']) - add_activity_item("🔍", "Beatport Discovery Started", f"'{chart_name}' - {track_count} tracks", "Now") + add_activity_item("", "Beatport Discovery Started", f"'{chart_name}' - {track_count} tracks", "Now") # Start discovery worker (capture profile ID while we have Flask context) beatport_chart_states[url_hash]['_profile_id'] = get_current_profile_id() future = beatport_discovery_executor.submit(_run_beatport_discovery_worker, url_hash) state['discovery_future'] = future - print(f"🔍 Started Spotify discovery for Beatport chart: {chart_name}") + print(f"Started Spotify discovery for Beatport chart: {chart_name}") return jsonify({"success": True, "message": "Discovery started", "status": "discovering"}) except Exception as e: - logger.error(f"❌ Error starting Beatport discovery: {e}") + logger.error(f"Error starting Beatport discovery: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/beatport/discovery/status/', methods=['GET']) @@ -46345,7 +46345,7 @@ def get_beatport_discovery_status(url_hash): return jsonify(response) except Exception as e: - logger.error(f"❌ Error getting Beatport discovery status: {e}") + logger.error(f"Error getting Beatport discovery status: {e}") return jsonify({"error": str(e)}), 500 @@ -46375,7 +46375,7 @@ def update_beatport_discovery_match(): old_status = result.get('status') # Update with user-selected track - result['status'] = '✅ Found' + result['status'] = 'Found' result['status_class'] = 'found' result['spotify_track'] = spotify_track['name'] result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists']) @@ -46403,16 +46403,16 @@ def update_beatport_discovery_match(): result['manual_match'] = True # Flag for tracking # Update match count if status changed from not found/error - if old_status != 'found' and old_status != '✅ Found': + if old_status != 'found' and old_status != 'Found': state['spotify_matches'] = state.get('spotify_matches', 0) + 1 - logger.info(f"✅ Manual match updated: beatport - {identifier} - track {track_index}") + logger.info(f"Manual match updated: beatport - {identifier} - track {track_index}") logger.info(f" → {result['spotify_artist']} - {result['spotify_track']}") return jsonify({'success': True, 'result': result}) except Exception as e: - logger.error(f"❌ Error updating Beatport discovery match: {e}") + logger.error(f"Error updating Beatport discovery match: {e}") return jsonify({'error': str(e)}), 500 @@ -46449,7 +46449,7 @@ def _run_beatport_discovery_worker(url_hash): if not use_spotify: itunes_client_instance = _get_metadata_fallback_client() - print(f"🔍 Starting {discovery_source.upper()} discovery for {len(tracks)} Beatport tracks...") + print(f"Starting {discovery_source.upper()} discovery for {len(tracks)} Beatport tracks...") # Store discovery source in state for frontend state['discovery_source'] = discovery_source @@ -46459,7 +46459,7 @@ def _run_beatport_discovery_worker(url_hash): try: # Check for cancellation if state.get('phase') != 'discovering': - print(f"🛑 Beatport discovery cancelled (phase changed to '{state.get('phase')}')") + print(f"Beatport discovery cancelled (phase changed to '{state.get('phase')}')") return # Update progress @@ -46478,7 +46478,7 @@ def _run_beatport_discovery_worker(url_hash): else: track_artist = clean_beatport_text(str(track_artists)) - print(f"🔍 Searching {discovery_source.upper()} for: '{track_artist}' - '{track_title}'") + print(f"Searching {discovery_source.upper()} for: '{track_artist}' - '{track_title}'") # Check discovery cache first cache_key = _get_discovery_cache_key(track_title, track_artist) @@ -46486,7 +46486,7 @@ def _run_beatport_discovery_worker(url_hash): cache_db = get_database() cached_match = cache_db.get_discovery_cache_match(cache_key[0], cache_key[1], discovery_source) if cached_match and _validate_discovery_cache_artist(track_artist, cached_match): - print(f"⚡ CACHE HIT [{i+1}/{len(tracks)}]: {track_artist} - {track_title}") + print(f"CACHE HIT [{i+1}/{len(tracks)}]: {track_artist} - {track_title}") # Convert artists from ['str'] to [{'name': 'str'}] for Beatport frontend format beatport_artists = cached_match.get('artists', []) if beatport_artists and isinstance(beatport_artists[0], str): @@ -46506,7 +46506,7 @@ def _run_beatport_discovery_worker(url_hash): state['discovery_results'].append(result_entry) continue except Exception as cache_err: - print(f"⚠️ Cache lookup error: {cache_err}") + print(f"Cache lookup error: {cache_err}") # Use matching engine for track matching found_track = None @@ -46522,9 +46522,9 @@ def _run_beatport_discovery_worker(url_hash): 'album': None })() search_queries = matching_engine.generate_download_queries(temp_track) - print(f"🔍 Generated {len(search_queries)} search queries using matching engine") + print(f"Generated {len(search_queries)} search queries using matching engine") except Exception as e: - print(f"⚠️ Matching engine failed for Beatport, falling back to basic queries: {e}") + print(f"Matching engine failed for Beatport, falling back to basic queries: {e}") if use_spotify: search_queries = [ f"{track_artist} {track_title}", @@ -46540,7 +46540,7 @@ def _run_beatport_discovery_worker(url_hash): for query_idx, search_query in enumerate(search_queries): try: - print(f"🔍 Query {query_idx + 1}/{len(search_queries)}: {search_query} ({discovery_source.upper()})") + print(f"Query {query_idx + 1}/{len(search_queries)}: {search_query} ({discovery_source.upper()})") search_results = None @@ -46565,19 +46565,19 @@ def _run_beatport_discovery_worker(url_hash): best_raw_track = _cache.get_entity('spotify', 'track', match.id) else: best_raw_track = None - print(f"✅ New best Beatport match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"New best Beatport match: {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") if best_confidence >= 0.9: - print(f"🎯 High confidence match found ({best_confidence:.3f}), stopping search") + print(f"High confidence match found ({best_confidence:.3f}), stopping search") break except Exception as e: - print(f"❌ Error in {discovery_source.upper()} search for query '{search_query}': {e}") + print(f"Error in {discovery_source.upper()} search for query '{search_query}': {e}") continue # Strategy 4: Extended search with higher limit (last resort) if not found_track: - print(f"🔄 Beatport Strategy 4: Extended search with limit=50") + print(f"Beatport Strategy 4: Extended search with limit=50") query = f"{track_artist} {track_title}" if use_spotify: extended_results = spotify_client.search_tracks(query, limit=50) @@ -46590,12 +46590,12 @@ def _run_beatport_discovery_worker(url_hash): if match and confidence >= min_confidence: found_track = match best_confidence = confidence - print(f"✅ Strategy 4 Beatport match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") + print(f"Strategy 4 Beatport match (extended): {match.artists[0]} - {match.name} (confidence: {confidence:.3f})") if found_track: - print(f"✅ Final Beatport match: {found_track.artists[0]} - {found_track.name} (confidence: {best_confidence:.3f})") + print(f"Final Beatport match: {found_track.artists[0]} - {found_track.name} (confidence: {best_confidence:.3f})") else: - print(f"❌ No suitable match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})") + print(f"No suitable match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})") # Create result entry result_entry = { @@ -46614,7 +46614,7 @@ def _run_beatport_discovery_worker(url_hash): if use_spotify: # SPOTIFY result formatting # Debug: show available attributes - print(f"🔍 Spotify track attributes: {dir(found_track)}") + print(f"Spotify track attributes: {dir(found_track)}") # Format artists correctly for frontend compatibility formatted_artists = [] @@ -46691,9 +46691,9 @@ def _run_beatport_discovery_worker(url_hash): cache_key[0], cache_key[1], discovery_source, best_confidence, cache_data, track_title, track_artist ) - print(f"💾 CACHE SAVED: {track_artist} - {track_title} (confidence: {best_confidence:.3f})") + print(f"CACHE SAVED: {track_artist} - {track_title} (confidence: {best_confidence:.3f})") except Exception as cache_err: - print(f"⚠️ Cache save error: {cache_err}") + print(f"Cache save error: {cache_err}") state['discovery_results'].append(result_entry) @@ -46701,7 +46701,7 @@ def _run_beatport_discovery_worker(url_hash): time.sleep(0.1) except Exception as e: - print(f"❌ Error processing Beatport track {i}: {e}") + print(f"Error processing Beatport track {i}: {e}") # Add error result state['discovery_results'].append({ 'index': i, # Add index for frontend table row identification @@ -46723,16 +46723,16 @@ def _run_beatport_discovery_worker(url_hash): # Add activity for completion chart_name = chart.get('name', 'Unknown Chart') source_label = discovery_source.upper() - add_activity_item("✅", f"Beatport Discovery Complete ({source_label})", + add_activity_item("", f"Beatport Discovery Complete ({source_label})", f"'{chart_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now") - print(f"✅ Beatport discovery complete ({source_label}): {state['spotify_matches']}/{len(tracks)} tracks found") + print(f"Beatport discovery complete ({source_label}): {state['spotify_matches']}/{len(tracks)} tracks found") # Sync discovery results back to mirrored playlist _sync_discovery_results_to_mirrored('beatport', url_hash, state.get('discovery_results', []), discovery_source, profile_id=state.get('_profile_id', 1)) except Exception as e: - print(f"❌ Error in Beatport discovery worker: {e}") + print(f"Error in Beatport discovery worker: {e}") if url_hash in beatport_chart_states: beatport_chart_states[url_hash]['status'] = 'error' beatport_chart_states[url_hash]['phase'] = 'fresh' @@ -46743,19 +46743,19 @@ def _run_beatport_discovery_worker(url_hash): def start_beatport_sync(url_hash): """Start sync process for a Beatport chart using discovered Spotify tracks""" try: - print(f"🎧 Beatport sync start requested for: {url_hash}") + print(f"Beatport sync start requested for: {url_hash}") if url_hash not in beatport_chart_states: - print(f"❌ Beatport chart not found: {url_hash}") + print(f"Beatport chart not found: {url_hash}") return jsonify({"error": "Beatport chart not found"}), 404 state = beatport_chart_states[url_hash] state['last_accessed'] = time.time() # Update access time - print(f"🎧 Beatport chart state: phase={state.get('phase')}, has_discovery_results={len(state.get('discovery_results', []))}") + print(f"Beatport chart state: phase={state.get('phase')}, has_discovery_results={len(state.get('discovery_results', []))}") if state['phase'] not in ['discovered', 'sync_complete', 'download_complete']: - print(f"❌ Beatport chart not ready for sync: {state['phase']}") + print(f"Beatport chart not ready for sync: {state['phase']}") return jsonify({"error": "Beatport chart not ready for sync"}), 400 # Convert discovery results to Spotify tracks format @@ -46789,11 +46789,11 @@ def start_beatport_sync(url_hash): future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['name'], spotify_tracks, None, get_current_profile_id()) state['sync_future'] = future - print(f"🎧 Started Beatport sync for chart: {state['chart']['name']}") + print(f"Started Beatport sync for chart: {state['chart']['name']}") return jsonify({"success": True, "sync_id": sync_playlist_id}) except Exception as e: - print(f"❌ Error starting Beatport sync: {e}") + print(f"Error starting Beatport sync: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/beatport/sync/status/', methods=['GET']) @@ -46828,16 +46828,16 @@ def get_beatport_sync_status(url_hash): result = sync_state.get('result', {}) state['converted_spotify_playlist_id'] = result.get('spotify_playlist_id') chart_name = state.get('chart', {}).get('name', 'Unknown Chart') - add_activity_item("🔄", "Sync Complete", f"Beatport chart '{chart_name}' synced successfully", "Now") + add_activity_item("", "Sync Complete", f"Beatport chart '{chart_name}' synced successfully", "Now") elif sync_state.get('status') == 'error': state['phase'] = 'discovered' # Revert on error chart_name = state.get('chart', {}).get('name', 'Unknown Chart') - add_activity_item("❌", "Sync Failed", f"Beatport chart '{chart_name}' sync failed", "Now") + add_activity_item("", "Sync Failed", f"Beatport chart '{chart_name}' sync failed", "Now") return jsonify(response) except Exception as e: - print(f"❌ Error getting Beatport sync status: {e}") + print(f"Error getting Beatport sync status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/beatport/sync/cancel/', methods=['POST']) @@ -46865,11 +46865,11 @@ def cancel_beatport_sync(url_hash): state['sync_playlist_id'] = None state['sync_progress'] = {} - print(f"🎧 Cancelled Beatport sync for: {url_hash}") + print(f"Cancelled Beatport sync for: {url_hash}") return jsonify({"success": True}) except Exception as e: - print(f"❌ Error cancelling Beatport sync: {e}") + print(f"Error cancelling Beatport sync: {e}") return jsonify({"error": str(e)}), 500 # =================================================================== @@ -46909,13 +46909,13 @@ def get_beatport_charts(): # Remove old charts for chart_hash in to_remove: del beatport_chart_states[chart_hash] - logger.info(f"🧹 Cleaned up old Beatport chart: {chart_hash}") + logger.info(f"Cleaned up old Beatport chart: {chart_hash}") - logger.info(f"📊 Returning {len(charts)} Beatport charts for hydration") + logger.info(f"Returning {len(charts)} Beatport charts for hydration") return jsonify(charts) except Exception as e: - logger.error(f"❌ Error getting Beatport charts: {e}") + logger.error(f"Error getting Beatport charts: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/beatport/charts/status/', methods=['GET']) @@ -46947,7 +46947,7 @@ def get_beatport_chart_status(chart_hash): return jsonify(response) except Exception as e: - logger.error(f"❌ Error getting Beatport chart status: {e}") + logger.error(f"Error getting Beatport chart status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/beatport/charts/update-phase/', methods=['POST']) @@ -46978,7 +46978,7 @@ def update_beatport_chart_phase(chart_hash): state['download_process_id'] = None state['sync_playlist_id'] = None state['sync_progress'] = {} - logger.info(f"🎧 Reset Beatport chart {chart_hash} to fresh state") + logger.info(f"Reset Beatport chart {chart_hash} to fresh state") else: # Handle other phase updates (like download phase transitions) converted_playlist_id = data.get('converted_spotify_playlist_id') @@ -46989,11 +46989,11 @@ def update_beatport_chart_phase(chart_hash): if download_process_id: state['download_process_id'] = download_process_id - logger.info(f"🎧 Updated Beatport chart {chart_hash} phase to: {new_phase}") + logger.info(f"Updated Beatport chart {chart_hash} phase to: {new_phase}") return jsonify({"success": True, "phase": new_phase}) except Exception as e: - logger.error(f"❌ Error updating Beatport chart phase: {e}") + logger.error(f"Error updating Beatport chart phase: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/beatport/charts/delete/', methods=['DELETE']) @@ -47006,11 +47006,11 @@ def delete_beatport_chart(chart_hash): chart_name = beatport_chart_states[chart_hash]['chart']['name'] del beatport_chart_states[chart_hash] - logger.info(f"🗑️ Deleted Beatport chart: {chart_name}") + logger.info(f"Deleted Beatport chart: {chart_name}") return jsonify({"success": True, "message": f"Deleted chart: {chart_name}"}) except Exception as e: - logger.error(f"❌ Error deleting Beatport chart: {e}") + logger.error(f"Error deleting Beatport chart: {e}") return jsonify({"error": str(e)}), 500 # ── Mirrored Playlists ──────────────────────────────────────────────── @@ -47354,7 +47354,7 @@ def prepare_mirrored_discovery(playlist_id): 'index': idx, 'yt_track': track['name'], 'yt_artist': track['artists'][0] if track['artists'] else 'Unknown', - 'status': '🔄 Provider changed', + 'status': 'Provider changed', 'status_class': 'not-found', 'spotify_track': '', 'spotify_artist': '', @@ -47378,7 +47378,7 @@ def prepare_mirrored_discovery(playlist_id): 'index': idx, 'yt_track': track['name'], 'yt_artist': track['artists'][0] if track['artists'] else 'Unknown', - 'status': '✅ Found', + 'status': 'Found', 'status_class': 'found', 'spotify_track': matched.get('name', ''), 'spotify_artist': artist_str, @@ -47403,7 +47403,7 @@ def prepare_mirrored_discovery(playlist_id): 'index': idx, 'yt_track': track['name'], 'yt_artist': track['artists'][0] if track['artists'] else 'Unknown', - 'status': '🔄 Provider changed' if cached_provider != _current_provider else '❌ Not Found', + 'status': 'Provider changed' if cached_provider != _current_provider else 'Not Found', 'status_class': 'not-found', 'spotify_track': '', 'spotify_artist': '', @@ -47530,13 +47530,13 @@ def retry_failed_mirrored_discovery(playlist_id): 'discovery_attempted': False, }) except Exception as db_err: - print(f"⚠️ Error clearing discovery_attempted in DB: {db_err}") + print(f"Error clearing discovery_attempted in DB: {db_err}") # Submit worker future = youtube_discovery_executor.submit(_run_youtube_discovery_worker, url_hash) state['discovery_future'] = future - print(f"🔄 Retrying failed discovery for {url_hash}: {retry_count} tracks to retry, {already_found} already found") + print(f"Retrying failed discovery for {url_hash}: {retry_count} tracks to retry, {already_found} already found") return jsonify({ "success": True, "retry_count": retry_count, @@ -48031,7 +48031,7 @@ class WebMetadataUpdateWorker: if not all_artists: metadata_update_state['status'] = 'error' metadata_update_state['error'] = f"No artists found in {self.server_type.title()} library" - add_activity_item("❌", "Metadata Update", metadata_update_state['error'], "Now") + add_activity_item("", "Metadata Update", metadata_update_state['error'], "Now") return # Filter artists that need processing @@ -48042,10 +48042,10 @@ class WebMetadataUpdateWorker: if len(artists_to_process) == 0: metadata_update_state['status'] = 'completed' metadata_update_state['completed_at'] = datetime.now() - add_activity_item("✅", "Metadata Update", "All artists already have good metadata", "Now") + add_activity_item("", "Metadata Update", "All artists already have good metadata", "Now") return else: - add_activity_item("🎵", "Metadata Update", f"Processing {len(artists_to_process)} of {len(all_artists)} artists", "Now") + add_activity_item("", "Metadata Update", f"Processing {len(artists_to_process)} of {len(all_artists)} artists", "Now") if not artists_to_process: metadata_update_state['status'] = 'completed' @@ -48111,13 +48111,13 @@ class WebMetadataUpdateWorker: metadata_update_state['current_artist'] = 'Completed' summary = f"Processed {self.processed_count} artists: {self.successful_count} updated, {self.failed_count} failed" - add_activity_item("🎵", "Metadata Complete", summary, "Now") + add_activity_item("", "Metadata Complete", summary, "Now") except Exception as e: print(f"Metadata update failed: {e}") metadata_update_state['status'] = 'error' metadata_update_state['error'] = str(e) - add_activity_item("❌", "Metadata Error", str(e), "Now") + add_activity_item("", "Metadata Error", str(e), "Now") def artist_needs_processing(self, artist): """Check if an artist needs metadata processing using age-based detection - EXACT copy from dashboard.py""" @@ -48217,7 +48217,7 @@ class WebMetadataUpdateWorker: if raw and 'name' in raw: spotify_artist = SpotifyArtistDC.from_spotify_artist(raw) highest_score = 1.0 - print(f"✅ Metadata updater: direct Spotify lookup for '{artist_name}' via cached ID {db_spotify_id}") + print(f"Metadata updater: direct Spotify lookup for '{artist_name}' via cached ID {db_spotify_id}") except Exception as e: print(f"Direct Spotify lookup failed for {db_spotify_id}: {e}") spotify_artist = None @@ -48326,15 +48326,15 @@ class WebMetadataUpdateWorker: try: # Check if artist already has a good photo (skip check for Jellyfin) if self.server_type != "jellyfin" and self.artist_has_valid_photo(artist): - print(f"🖼️ Skipping {artist.title}: already has valid photo ({getattr(artist, 'thumb', 'None')})") + print(f"Skipping {artist.title}: already has valid photo ({getattr(artist, 'thumb', 'None')})") return False # Get the image URL from Spotify if not spotify_artist.image_url: - print(f"🚫 Skipping {artist.title}: no Spotify image URL available") + print(f"Skipping {artist.title}: no Spotify image URL available") return False - print(f"📸 Processing {artist.title}: downloading from Spotify...") + print(f"Processing {artist.title}: downloading from Spotify...") image_url = spotify_artist.image_url @@ -48346,7 +48346,7 @@ class WebMetadataUpdateWorker: if self.server_type == "jellyfin": # For Jellyfin, use raw image data to preserve original format image_data = response.content - print(f"📸 Using raw image data for Jellyfin ({len(image_data)} bytes)") + print(f"Using raw image data for Jellyfin ({len(image_data)} bytes)") else: # For other servers, validate and convert image_data = self.validate_and_convert_image(response.content) @@ -48591,7 +48591,7 @@ class WebMetadataUpdateWorker: jellyfin_token = jellyfin_config.get('api_key', '') if not jellyfin_base_url or not jellyfin_token: - print("❌ Jellyfin configuration missing for image upload") + print("Jellyfin configuration missing for image upload") return False upload_url = f"{jellyfin_base_url.rstrip('/')}/Items/{artist.ratingKey}/Images/Primary" @@ -48706,7 +48706,7 @@ def start_oauth_callback_servers(): if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'): spotify_enrichment_worker.client.reload_config() spotify_enrichment_worker.client._invalidate_auth_cache() - add_activity_item("✅", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now") + add_activity_item("", "Spotify Auth Complete", "Successfully authenticated with Spotify", "Now") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() @@ -48717,7 +48717,7 @@ def start_oauth_callback_servers(): raise Exception("Failed to exchange authorization code for access token") except Exception as e: _oauth_logger.error(f"Spotify token processing error: {e}") - add_activity_item("❌", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now") + add_activity_item("", "Spotify Auth Failed", f"Token processing failed: {str(e)}", "Now") self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() @@ -48726,7 +48726,7 @@ def start_oauth_callback_servers(): error = query_params['error'][0] _oauth_logger.error(f"Spotify OAuth error returned by Spotify: {error}") _oauth_logger.error(f"Full callback URL: {self.path}") - add_activity_item("❌", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now") + add_activity_item("", "Spotify Auth Failed", f"Spotify returned error: {error}", "Now") self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() @@ -48769,26 +48769,26 @@ def start_oauth_callback_servers(): bind_addr = ('0.0.0.0', 8888) spotify_server = HTTPServer(bind_addr, SpotifyCallbackHandler) _oauth_logger.info(f"Spotify OAuth callback server listening on {bind_addr[0]}:{bind_addr[1]}") - print(f"🎵 Started Spotify OAuth callback server on {bind_addr[0]}:{bind_addr[1]}") + print(f"Started Spotify OAuth callback server on {bind_addr[0]}:{bind_addr[1]}") spotify_server.serve_forever() except OSError as e: _oauth_logger.error(f"Failed to start Spotify callback server on port 8888: {e} — port may already be in use") - print(f"🔴 Failed to start Spotify callback server on port 8888: {e}") + print(f"Failed to start Spotify callback server on port 8888: {e}") except Exception as e: _oauth_logger.error(f"Failed to start Spotify callback server: {e}") - print(f"🔴 Failed to start Spotify callback server: {e}") + print(f"Failed to start Spotify callback server: {e}") # Tidal callback server class TidalCallbackHandler(BaseHTTPRequestHandler): def do_GET(self): - print("🎶🎶🎶 TIDAL CALLBACK SERVER RECEIVED REQUEST 🎶🎶🎶") + print("TIDAL CALLBACK SERVER RECEIVED REQUEST ") parsed_url = urllib.parse.urlparse(self.path) query_params = urllib.parse.parse_qs(parsed_url.query) - print(f"🎶 Callback path: {self.path}") + print(f"Callback path: {self.path}") if 'code' in query_params: auth_code = query_params['code'][0] - print(f"🎶 Received Tidal authorization code: {auth_code[:10]}...") + print(f"Received Tidal authorization code: {auth_code[:10]}...") # Exchange the authorization code for tokens try: @@ -48803,7 +48803,7 @@ def start_oauth_callback_servers(): temp_client.code_verifier = tidal_oauth_state["code_verifier"] temp_client.code_challenge = tidal_oauth_state["code_challenge"] - print(f"🔐 Restored PKCE - verifier: {temp_client.code_verifier[:20] if temp_client.code_verifier else 'None'}... challenge: {temp_client.code_challenge[:20] if temp_client.code_challenge else 'None'}...") + print(f"Restored PKCE - verifier: {temp_client.code_verifier[:20] if temp_client.code_verifier else 'None'}... challenge: {temp_client.code_challenge[:20] if temp_client.code_challenge else 'None'}...") success = temp_client.fetch_token_from_code(auth_code) @@ -48814,7 +48814,7 @@ def start_oauth_callback_servers(): if tidal_enrichment_worker: tidal_enrichment_worker.client = tidal_client - add_activity_item("✅", "Tidal Auth Complete", "Successfully authenticated with Tidal", "Now") + add_activity_item("", "Tidal Auth Complete", "Successfully authenticated with Tidal", "Now") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() @@ -48823,16 +48823,16 @@ def start_oauth_callback_servers(): raise Exception("Failed to exchange authorization code for tokens") except Exception as e: - print(f"🔴 Tidal token processing error: {e}") - add_activity_item("❌", "Tidal Auth Failed", f"Token processing failed: {str(e)}", "Now") + print(f"Tidal token processing error: {e}") + add_activity_item("", "Tidal Auth Failed", f"Token processing failed: {str(e)}", "Now") self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(f'

    Tidal Authentication Failed

    {str(e)}

    '.encode()) else: error = query_params.get('error', ['Unknown error'])[0] - print(f"🔴 Tidal OAuth error: {error}") - add_activity_item("❌", "Tidal Auth Failed", f"OAuth error: {error}", "Now") + print(f"Tidal OAuth error: {error}") + add_activity_item("", "Tidal Auth Failed", f"OAuth error: {error}", "Now") self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() @@ -48844,13 +48844,13 @@ def start_oauth_callback_servers(): def run_tidal_server(): try: tidal_server = HTTPServer(('0.0.0.0', 8889), TidalCallbackHandler) - print("🎶 Started Tidal OAuth callback server on port 8889") - print(f"🎶 Tidal server listening on all interfaces, port 8889") + print("Started Tidal OAuth callback server on port 8889") + print(f"Tidal server listening on all interfaces, port 8889") tidal_server.serve_forever() except Exception as e: - print(f"🔴 Failed to start Tidal callback server: {e}") + print(f"Failed to start Tidal callback server: {e}") import traceback - print(f"🔴 Full error: {traceback.format_exc()}") + print(f"Full error: {traceback.format_exc()}") # Start both servers in background threads spotify_thread = threading.Thread(target=run_spotify_server, daemon=True) @@ -48859,7 +48859,7 @@ def start_oauth_callback_servers(): spotify_thread.start() tidal_thread.start() - print("✅ OAuth callback servers started") + print("OAuth callback servers started") # =============================================== # Artist Detail Spotify Integration Functions @@ -48870,7 +48870,7 @@ def get_spotify_artist_discography(artist_name): try: from core.matching_engine import MusicMatchingEngine - print(f"🎵 Searching Spotify for artist: {artist_name}") + print(f"Searching Spotify for artist: {artist_name}") # Reuse cached profile-aware Spotify client spotify_client = get_spotify_client_for_profile() @@ -48898,7 +48898,7 @@ def get_spotify_artist_discography(artist_name): # Step 1: Try exact case-insensitive match for spotify_artist in artists: if artist_name.lower().strip() == spotify_artist.name.lower().strip(): - print(f"🎯 Exact match found: '{spotify_artist.name}'") + print(f"Exact match found: '{spotify_artist.name}'") best_match = spotify_artist highest_score = 1.0 break @@ -48911,7 +48911,7 @@ def get_spotify_artist_discography(artist_name): spotify_artist_normalized = matching_engine.normalize_string(spotify_artist.name) score = matching_engine.similarity_score(db_artist_normalized, spotify_artist_normalized) - print(f"🔍 Fuzzy match candidate: '{spotify_artist.name}' (score: {score:.3f})") + print(f"Fuzzy match candidate: '{spotify_artist.name}' (score: {score:.3f})") if score > highest_score: highest_score = score @@ -48927,7 +48927,7 @@ def get_spotify_artist_discography(artist_name): artist = best_match spotify_artist_id = artist.id - print(f"🎵 Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})") + print(f"Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})") # Get all albums (albums, singles, and compilations) all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation', limit=50) @@ -48938,7 +48938,7 @@ def get_spotify_artist_discography(artist_name): 'error': f'No albums found for artist "{artist_name}"' } - print(f"📀 Found {len(all_albums)} releases on Spotify") + print(f"Found {len(all_albums)} releases on Spotify") # Categorize releases albums = [] @@ -49003,7 +49003,7 @@ def get_spotify_artist_discography(artist_name): eps = _dedup_releases(eps) singles = _dedup_releases(singles) - print(f"📀 Categorized Spotify releases - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}") + print(f"Categorized Spotify releases - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}") return { 'success': True, @@ -49016,7 +49016,7 @@ def get_spotify_artist_discography(artist_name): } except Exception as e: - print(f"❌ Error getting Spotify discography for {artist_name}: {e}") + print(f"Error getting Spotify discography for {artist_name}: {e}") return { 'success': False, 'error': str(e) @@ -49025,7 +49025,7 @@ def get_spotify_artist_discography(artist_name): def merge_discography_data(owned_releases, spotify_discography, db=None, artist_name=None): """Build discography from Spotify data with 'checking' state - ownership is resolved via SSE stream""" try: - print("🔄 Building discography cards (fast path - no DB matching)...") + print("Building discography cards (fast path - no DB matching)...") def build_category(spotify_category, category_name): """Build cards for a category with checking state""" @@ -49052,7 +49052,7 @@ def merge_discography_data(owned_releases, spotify_discography, db=None, artist_ eps = build_category(spotify_discography['eps'], 'EPs') singles = build_category(spotify_discography['singles'], 'Singles') - print(f"✅ Built discography cards - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}") + print(f"Built discography cards - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}") return { 'success': True, @@ -49062,7 +49062,7 @@ def merge_discography_data(owned_releases, spotify_discography, db=None, artist_ } except Exception as e: - print(f"❌ Error building discography: {e}") + print(f"Error building discography: {e}") import traceback traceback.print_exc() return { @@ -49092,11 +49092,11 @@ try: mb_worker.start() if config_manager.get('musicbrainz_enrichment_paused', False): mb_worker.pause() - print("✅ MusicBrainz enrichment worker initialized (paused — restored from config)") + print("MusicBrainz enrichment worker initialized (paused — restored from config)") else: - print("✅ MusicBrainz enrichment worker initialized and started") + print("MusicBrainz enrichment worker initialized and started") except Exception as e: - print(f"⚠️ MusicBrainz worker initialization failed: {e}") + print(f"MusicBrainz worker initialization failed: {e}") mb_worker = None # --- MusicBrainz API Endpoints --- @@ -49169,11 +49169,11 @@ try: audiodb_worker.start() if config_manager.get('audiodb_enrichment_paused', False): audiodb_worker.pause() - print("✅ AudioDB enrichment worker initialized (paused — restored from config)") + print("AudioDB enrichment worker initialized (paused — restored from config)") else: - print("✅ AudioDB enrichment worker initialized and started") + print("AudioDB enrichment worker initialized and started") except Exception as e: - print(f"⚠️ AudioDB worker initialization failed: {e}") + print(f"AudioDB worker initialization failed: {e}") audiodb_worker = None # --- AudioDB API Endpoints --- @@ -49242,11 +49242,11 @@ try: discogs_worker.start() if config_manager.get('discogs_enrichment_paused', False): discogs_worker.pause() - print("✅ Discogs enrichment worker initialized (paused — restored from config)") + print("Discogs enrichment worker initialized (paused — restored from config)") else: - print("✅ Discogs enrichment worker initialized and started") + print("Discogs enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Discogs worker initialization failed: {e}") + print(f"Discogs worker initialization failed: {e}") discogs_worker = None # --- Discogs API Endpoints --- @@ -49304,11 +49304,11 @@ try: deezer_worker.start() if config_manager.get('deezer_enrichment_paused', False): deezer_worker.pause() - print("✅ Deezer enrichment worker initialized (paused — restored from config)") + print("Deezer enrichment worker initialized (paused — restored from config)") else: - print("✅ Deezer enrichment worker initialized and started") + print("Deezer enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Deezer worker initialization failed: {e}") + print(f"Deezer worker initialization failed: {e}") deezer_worker = None # --- Deezer API Endpoints --- @@ -49382,11 +49382,11 @@ try: spotify_enrichment_worker.paused = True # Set BEFORE start() to prevent race condition spotify_enrichment_worker.start() if spotify_enrichment_worker.paused: - print("✅ Spotify enrichment worker initialized (paused — restored from config)") + print("Spotify enrichment worker initialized (paused — restored from config)") else: - print("✅ Spotify enrichment worker initialized and started") + print("Spotify enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Spotify enrichment worker initialization failed: {e}") + print(f"Spotify enrichment worker initialization failed: {e}") spotify_enrichment_worker = None # --- API Rate Monitor Endpoints --- @@ -49481,11 +49481,11 @@ try: itunes_enrichment_worker.start() if config_manager.get('itunes_enrichment_paused', False): itunes_enrichment_worker.pause() - print("✅ iTunes enrichment worker initialized (paused — restored from config)") + print("iTunes enrichment worker initialized (paused — restored from config)") else: - print("✅ iTunes enrichment worker initialized and started") + print("iTunes enrichment worker initialized and started") except Exception as e: - print(f"⚠️ iTunes enrichment worker initialization failed: {e}") + print(f"iTunes enrichment worker initialization failed: {e}") itunes_enrichment_worker = None # --- iTunes API Endpoints --- @@ -49557,11 +49557,11 @@ try: lastfm_worker.start() if config_manager.get('lastfm_enrichment_paused', False): lastfm_worker.pause() - print("✅ Last.fm enrichment worker initialized (paused — restored from config)") + print("Last.fm enrichment worker initialized (paused — restored from config)") else: - print("✅ Last.fm enrichment worker initialized and started") + print("Last.fm enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Last.fm worker initialization failed: {e}") + print(f"Last.fm worker initialization failed: {e}") lastfm_worker = None # --- Last.fm API Endpoints --- @@ -49700,11 +49700,11 @@ try: genius_worker.paused = True genius_worker.start() if genius_worker.paused: - print("✅ Genius enrichment worker initialized (paused — restored from config)") + print("Genius enrichment worker initialized (paused — restored from config)") else: - print("✅ Genius enrichment worker initialized and started") + print("Genius enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Genius worker initialization failed: {e}") + print(f"Genius worker initialization failed: {e}") genius_worker = None # --- Genius API Endpoints --- @@ -49777,11 +49777,11 @@ try: tidal_enrichment_worker.start() if config_manager.get('tidal_enrichment_paused', False): tidal_enrichment_worker.pause() - print("✅ Tidal enrichment worker initialized (paused — restored from config)") + print("Tidal enrichment worker initialized (paused — restored from config)") else: - print("✅ Tidal enrichment worker initialized and started") + print("Tidal enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Tidal worker initialization failed: {e}") + print(f"Tidal worker initialization failed: {e}") tidal_enrichment_worker = None # --- Tidal Enrichment API Endpoints --- @@ -49851,11 +49851,11 @@ try: qobuz_enrichment_worker.start() if config_manager.get('qobuz_enrichment_paused', False): qobuz_enrichment_worker.pause() - print("✅ Qobuz enrichment worker initialized (paused — restored from config)") + print("Qobuz enrichment worker initialized (paused — restored from config)") else: - print("✅ Qobuz enrichment worker initialized and started") + print("Qobuz enrichment worker initialized and started") except Exception as e: - print(f"⚠️ Qobuz worker initialization failed: {e}") + print(f"Qobuz worker initialization failed: {e}") qobuz_enrichment_worker = None # --- Qobuz Enrichment API Endpoints --- @@ -49929,13 +49929,13 @@ try: hydrabase_worker = HydrabaseWorker(get_ws_and_lock=_get_hydrabase_ws_and_lock) hydrabase_worker.start() hydrabase_client = HydrabaseClient(get_ws_and_lock=_get_hydrabase_ws_and_lock) - print("✅ Hydrabase P2P mirror worker and metadata client initialized") + print("Hydrabase P2P mirror worker and metadata client initialized") # Update API blueprint references if hasattr(app, 'soulsync'): app.soulsync['hydrabase_client'] = hydrabase_client app.soulsync['hydrabase_worker'] = hydrabase_worker except Exception as e: - print(f"⚠️ Hydrabase initialization failed: {e}") + print(f"Hydrabase initialization failed: {e}") hydrabase_worker = None hydrabase_client = None @@ -49952,9 +49952,9 @@ try: _hydrabase_ws = _auto_ws # Don't auto-enable dev mode — user must explicitly activate dev mode # Auto-connect just establishes the WebSocket for fallback/search tab use - print(f"✅ Hydrabase auto-connected to {_hydra_cfg['url']}") + print(f"Hydrabase auto-connected to {_hydra_cfg['url']}") except Exception as e: - print(f"⚠️ Hydrabase auto-reconnect failed: {e}") + print(f"Hydrabase auto-reconnect failed: {e}") # --- Hydrabase Worker API Endpoints --- @@ -50026,9 +50026,9 @@ try: soulid_db = MusicDatabase() soulid_worker = SoulIDWorker(database=soulid_db) soulid_worker.start() - print("✅ SoulID worker initialized and started") + print("SoulID worker initialized and started") except Exception as e: - print(f"⚠️ SoulID worker initialization failed: {e}") + print(f"SoulID worker initialization failed: {e}") soulid_worker = None @app.route('/api/soulid/status', methods=['GET']) @@ -50060,9 +50060,9 @@ try: navidrome_client=navidrome_client, ) listening_stats_worker.start() - print("✅ Listening stats worker initialized and started") + print("Listening stats worker initialized and started") except Exception as e: - print(f"⚠️ Listening stats worker initialization failed: {e}") + print(f"Listening stats worker initialization failed: {e}") listening_stats_worker = None # --- Stats API Endpoints --- @@ -50353,13 +50353,13 @@ def listening_stats_sync(): import threading def _do_sync(): try: - print("🔄 [Stats Sync] Starting manual poll...") + print("[Stats Sync] Starting manual poll...") listening_stats_worker._poll() listening_stats_worker.stats['polls_completed'] += 1 listening_stats_worker.stats['last_poll'] = time.strftime('%Y-%m-%d %H:%M:%S') - print("✅ [Stats Sync] Manual poll completed") + print("[Stats Sync] Manual poll completed") except Exception as e: - print(f"❌ [Stats Sync] Manual poll failed: {e}") + print(f"[Stats Sync] Manual poll failed: {e}") import traceback traceback.print_exc() logger.error(f"Manual stats sync failed: {e}") @@ -50449,9 +50449,9 @@ try: repair_worker._progress_lock_ref = repair_job_progress_lock repair_worker._progress_states_ref = repair_job_progress_states repair_worker.start() - print("✅ Repair worker initialized and started") + print("Repair worker initialized and started") except Exception as e: - print(f"⚠️ Repair worker initialization failed: {e}") + print(f"Repair worker initialization failed: {e}") repair_worker = None # --- Repair Worker API Endpoints --- @@ -51297,7 +51297,7 @@ def import_album_process(): errors.append(err_msg) logger.error(f"Import processing error: {err_msg}") - add_activity_item("📥", "Album Imported", f"{album_name} by {artist_name} ({processed}/{len(matches)} tracks)", "Now") + add_activity_item("", "Album Imported", f"{album_name} by {artist_name} ({processed}/{len(matches)} tracks)", "Now") # Emit events through automation engine — same chain as download batches # batch_complete → auto-scan → library_scan_completed → auto-update DB @@ -51557,7 +51557,7 @@ def import_singles_process(): errors.append(err_msg) logger.error(f"Import single processing error: {err_msg}") - add_activity_item("📥", "Singles Imported", f"{processed}/{len(files)} tracks processed", "Now") + add_activity_item("", "Singles Imported", f"{processed}/{len(files)} tracks processed", "Now") # Emit events through automation engine — same chain as download batches # batch_complete → auto-scan → library_scan_completed → auto-update DB @@ -51815,13 +51815,13 @@ def _hydrabase_reconnect_loop(): ) _hydrabase_ws = ws _consecutive_failures = 0 - print(f"🔄 [Hydrabase] Auto-reconnected to {hydra_cfg['url']}") + print(f"[Hydrabase] Auto-reconnected to {hydra_cfg['url']}") except Exception as e: _consecutive_failures += 1 if _consecutive_failures <= 3: - print(f"⚠️ [Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}") + print(f"[Hydrabase] Reconnect attempt failed ({_consecutive_failures}): {e}") elif _consecutive_failures == 4: - print(f"⚠️ [Hydrabase] Reconnect failing repeatedly — suppressing further logs until success") + print(f"[Hydrabase] Reconnect failing repeatedly — suppressing further logs until success") except Exception: pass # Don't crash the monitor loop @@ -52363,30 +52363,30 @@ if __name__ == '__main__': log_path = config_manager.get('logging.path', 'logs/app.log') logger = setup_logging(log_level, log_path) - print("🚀 Starting SoulSync Web UI Server...") + print("Starting SoulSync Web UI Server...") print("Open your browser and navigate to http://127.0.0.1:8008") # Start OAuth callback servers - print("🔧 Starting OAuth callback servers...") + print("Starting OAuth callback servers...") start_oauth_callback_servers() # Startup diagnostics: Check and recover stuck flags - print("🔍 Running startup diagnostics...") + print("Running startup diagnostics...") stuck_flags_recovered = check_and_recover_stuck_flags() if stuck_flags_recovered: - print("⚠️ Recovered stuck flags from previous session") + print("Recovered stuck flags from previous session") else: - print("✅ No stuck flags detected - system healthy") + print("No stuck flags detected - system healthy") # Start simple background monitor when server starts - print("🔧 Starting simple background monitor...") + print("Starting simple background monitor...") start_simple_background_monitor() - print("✅ Simple background monitor started (includes automatic search cleanup)") + print("Simple background monitor started (includes automatic search cleanup)") # Wishlist/watchlist timers are now managed by AutomationEngine system automations # Pre-build import suggestions cache in background - print("🔧 Pre-building import suggestions cache...") + print("Pre-building import suggestions cache...") start_import_suggestions_cache() # Initialize app start time for uptime tracking @@ -52397,26 +52397,26 @@ if __name__ == '__main__': _register_automation_handlers() if automation_engine: try: - print("🔧 Starting automation engine...") + print("Starting automation engine...") automation_engine.start() - print("✅ Automation engine started") + print("Automation engine started") try: automation_engine.emit('app_started', {}) except Exception: pass except AttributeError as e: - print(f"⚠️ Automation engine failed to start: {e}") + print(f"Automation engine failed to start: {e}") print(" If using Docker, check that your volume mount is /app/data (not /app/database)") logger.error(f"Automation engine start error (possible stale Docker volume): {e}") except Exception as e: - print(f"⚠️ Automation engine failed to start: {e}") + print(f"Automation engine failed to start: {e}") logger.error(f"Automation engine start error: {e}") # Add startup activity - add_activity_item("🚀", "System Started", "SoulSync Web UI Server initialized", "Now") + add_activity_item("", "System Started", "SoulSync Web UI Server initialized", "Now") # Start WebSocket background emitters - print("🔧 Starting WebSocket background emitters...") + print("Starting WebSocket background emitters...") # Phase 1: Global pollers socketio.start_background_task(_emit_service_status_loop) socketio.start_background_task(_emit_watchlist_count_loop) @@ -52442,6 +52442,6 @@ if __name__ == '__main__': socketio.start_background_task(_hydrabase_reconnect_loop) # API Rate Monitor — 1s push for speedometer gauges socketio.start_background_task(_emit_rate_monitor_loop) - print("✅ WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor)") + print("WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor)") socketio.run(app, host='0.0.0.0', port=8008, debug=False, allow_unsafe_werkzeug=True)