diff --git a/config/__pycache__/settings.cpython-310.pyc b/config/__pycache__/settings.cpython-310.pyc new file mode 100644 index 0000000..3dd0e12 Binary files /dev/null and b/config/__pycache__/settings.cpython-310.pyc differ diff --git a/config/__pycache__/settings.cpython-312.pyc b/config/__pycache__/settings.cpython-312.pyc index 0e1aa59..6ff5c11 100644 Binary files a/config/__pycache__/settings.cpython-312.pyc and b/config/__pycache__/settings.cpython-312.pyc differ diff --git a/config/settings.py b/config/settings.py index bda94eb..cd9331c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -95,5 +95,9 @@ class ConfigManager: 'plex': bool(self.get('plex.base_url')) and bool(self.get('plex.token')), 'soulseek': bool(self.get('soulseek.slskd_url')) } + + def get_quality_preference(self) -> str: + """Get the user's preferred audio quality setting""" + return self.get('settings.audio_quality', 'flac') config_manager = ConfigManager() \ No newline at end of file diff --git a/core/__pycache__/soulseek_client.cpython-312.pyc b/core/__pycache__/soulseek_client.cpython-312.pyc index a836a13..71dc380 100644 Binary files a/core/__pycache__/soulseek_client.cpython-312.pyc and b/core/__pycache__/soulseek_client.cpython-312.pyc differ diff --git a/core/soulseek_client.py b/core/soulseek_client.py index 2deb03d..5df5400 100644 --- a/core/soulseek_client.py +++ b/core/soulseek_client.py @@ -1137,15 +1137,19 @@ class SoulseekClient: logger.warning(f"No results found for: {query}") return None - preferred_results = [r for r in results if r.quality.lower() == preferred_quality.lower()] + # Use the new quality filtering + filtered_results = self.filter_results_by_quality_preference(results, preferred_quality) - if preferred_results: - best_result = preferred_results[0] - else: - best_result = results[0] - logger.info(f"Preferred quality {preferred_quality} not found, using {best_result.quality}") - - logger.info(f"Downloading: {best_result.filename} ({best_result.quality}) from {best_result.username}") + if not filtered_results: + logger.warning(f"No suitable quality results found for: {query}") + return None + + best_result = filtered_results[0] + quality_info = f"{best_result.quality.upper()}" + if best_result.bitrate: + quality_info += f" {best_result.bitrate}kbps" + + logger.info(f"Downloading: {best_result.filename} ({quality_info}) from {best_result.username}") return await self.download(best_result.username, best_result.filename, best_result.size) async def check_connection(self) -> bool: @@ -1160,6 +1164,74 @@ class SoulseekClient: logger.debug(f"Connection check failed: {e}") return False + def filter_results_by_quality_preference(self, results: List[TrackResult], preferred_quality: str) -> List[TrackResult]: + """ + Filter and sort results by quality preference with smart fallback. + Prefers exact match, then higher quality, then lower quality. + """ + if not results: + return [] + + # Normalize preference to match our quality strings + quality_map = { + 'flac': 'flac', + 'mp3_320': ('mp3', 320), + 'mp3_256': ('mp3', 256), + 'mp3_192': ('mp3', 192), + 'any': 'any' + } + + if preferred_quality not in quality_map: + return results # Return all if unknown preference + + if preferred_quality == 'any': + # Sort by quality score for "any" preference + return sorted(results, key=lambda x: x.quality_score, reverse=True) + + # Separate results by quality categories + exact_matches = [] + higher_quality = [] + lower_quality = [] + + if preferred_quality == 'flac': + for result in results: + if result.quality.lower() == 'flac': + exact_matches.append(result) + elif result.quality.lower() == 'mp3' and result.bitrate and result.bitrate >= 320: + higher_quality.append(result) # High-quality MP3 as fallback + else: + lower_quality.append(result) + else: + # MP3 preference with specific bitrate + pref_format, pref_bitrate = quality_map[preferred_quality] + + for result in results: + if result.quality.lower() == 'flac': + higher_quality.append(result) # FLAC is always higher quality + elif result.quality.lower() == pref_format: + if result.bitrate: + if result.bitrate == pref_bitrate: + exact_matches.append(result) + elif result.bitrate > pref_bitrate: + higher_quality.append(result) + else: + lower_quality.append(result) + else: + exact_matches.append(result) # Unknown bitrate, assume match + else: + lower_quality.append(result) + + # Sort each category by quality score and upload speed + def sort_key(result): + return (result.quality_score, result.upload_speed) + + exact_matches.sort(key=sort_key, reverse=True) + higher_quality.sort(key=sort_key, reverse=True) + lower_quality.sort(key=sort_key, reverse=True) + + # Return in preference order: exact > higher > lower + return exact_matches + higher_quality + lower_quality + async def get_session_info(self) -> Optional[Dict[str, Any]]: """Get slskd session information including version""" if not self.base_url: diff --git a/ui/pages/__pycache__/artists.cpython-312.pyc b/ui/pages/__pycache__/artists.cpython-312.pyc index e208ce2..d278e66 100644 Binary files a/ui/pages/__pycache__/artists.cpython-312.pyc and b/ui/pages/__pycache__/artists.cpython-312.pyc differ diff --git a/ui/pages/__pycache__/settings.cpython-312.pyc b/ui/pages/__pycache__/settings.cpython-312.pyc index d29e439..0372775 100644 Binary files a/ui/pages/__pycache__/settings.cpython-312.pyc and b/ui/pages/__pycache__/settings.cpython-312.pyc differ diff --git a/ui/pages/__pycache__/sync.cpython-312.pyc b/ui/pages/__pycache__/sync.cpython-312.pyc index 6489d90..45bd8e5 100644 Binary files a/ui/pages/__pycache__/sync.cpython-312.pyc and b/ui/pages/__pycache__/sync.cpython-312.pyc differ diff --git a/ui/pages/artists.py b/ui/pages/artists.py index 2c3e8fd..3fcef07 100644 --- a/ui/pages/artists.py +++ b/ui/pages/artists.py @@ -2714,9 +2714,26 @@ class DownloadMissingAlbumTracksModal(QDialog): print(f"❌ Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.") if verified_candidates: + # Apply quality preference filtering before returning + from config.settings import config_manager + quality_preference = config_manager.get_quality_preference() + + # Filter candidates by quality preference with smart fallback + if hasattr(self.parent_artists_page, 'soulseek_client'): + quality_filtered = self.parent_artists_page.soulseek_client.filter_results_by_quality_preference( + verified_candidates, quality_preference + ) + + if quality_filtered: + verified_candidates = quality_filtered + print(f"🎯 Applied quality filtering ({quality_preference}): {len(verified_candidates)} candidates remain") + else: + print(f"⚠️ Quality filtering ({quality_preference}) removed all candidates, keeping originals") + best_confidence = verified_candidates[0].confidence best_version = getattr(verified_candidates[0], 'version_type', 'unknown') - print(f"✅ Found {len(verified_candidates)} VERIFIED matches for '{spotify_track.name}'. Best: {best_confidence:.2f} ({best_version})") + 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()})") # Log version breakdown for debugging version_counts = {} @@ -2724,7 +2741,9 @@ class DownloadMissingAlbumTracksModal(QDialog): version = getattr(candidate, 'version_type', 'unknown') version_counts[version] = version_counts.get(version, 0) + 1 penalty = getattr(candidate, 'version_penalty', 0.0) - print(f" 🎵 {candidate.confidence:.2f} - {version} (penalty: {penalty:.2f}) - {candidate.filename[:100]}...") + 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]}...") else: print(f"⚠️ No verified matches found for '{spotify_track.name}' after checking file paths.") diff --git a/ui/pages/settings.py b/ui/pages/settings.py index 90e96f7..9df5dde 100644 --- a/ui/pages/settings.py +++ b/ui/pages/settings.py @@ -579,6 +579,22 @@ class SettingsPage(QWidget): if hasattr(self, 'log_path_display'): self.log_path_display.setText(logging_config.get('path', 'logs/app.log')) + # Load quality preference + if hasattr(self, 'quality_combo'): + audio_quality = config_manager.get('settings.audio_quality', 'FLAC') + # Map config values to combo box text + quality_mapping = { + 'flac': 'FLAC', + 'mp3_320': '320 kbps MP3', + 'mp3_256': '256 kbps MP3', + 'mp3_192': '192 kbps MP3', + 'any': 'Any' + } + display_quality = quality_mapping.get(audio_quality.lower(), audio_quality) + index = self.quality_combo.findText(display_quality) + if index >= 0: + self.quality_combo.setCurrentIndex(index) + except Exception as e: QMessageBox.warning(self, "Error", f"Failed to load configuration: {e}") @@ -604,6 +620,20 @@ class SettingsPage(QWidget): max_workers = int(self.max_workers_combo.currentText()) config_manager.set('database.max_workers', max_workers) + # Save Quality preference + if hasattr(self, 'quality_combo'): + quality_text = self.quality_combo.currentText() + # Map combo box text to config values + config_mapping = { + 'FLAC': 'flac', + '320 kbps MP3': 'mp3_320', + '256 kbps MP3': 'mp3_256', + '192 kbps MP3': 'mp3_192', + 'Any': 'any' + } + config_value = config_mapping.get(quality_text, 'flac') + config_manager.set('settings.audio_quality', config_value) + # Emit signals for path changes to update other pages immediately self.settings_changed.emit('soulseek.download_path', self.download_path_input.text()) self.settings_changed.emit('soulseek.transfer_path', self.transfer_path_input.text()) @@ -1218,14 +1248,15 @@ class SettingsPage(QWidget): quality_label = QLabel("Preferred Quality:") quality_label.setStyleSheet("color: #ffffff; font-size: 12px;") - quality_combo = QComboBox() - quality_combo.addItems(["FLAC", "320 kbps MP3", "256 kbps MP3", "192 kbps MP3", "Any"]) - quality_combo.setCurrentText("FLAC") - quality_combo.setStyleSheet(self.get_combo_style()) - quality_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.quality_combo = QComboBox() + self.quality_combo.addItems(["FLAC", "320 kbps MP3", "256 kbps MP3", "192 kbps MP3", "Any"]) + self.quality_combo.setCurrentText("FLAC") + self.quality_combo.setStyleSheet(self.get_combo_style()) + self.quality_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.form_inputs['settings.audio_quality'] = self.quality_combo quality_layout.addWidget(quality_label) - quality_layout.addWidget(quality_combo) + quality_layout.addWidget(self.quality_combo) # Download path path_container = QVBoxLayout() diff --git a/ui/pages/sync.py b/ui/pages/sync.py index d6e64f4..cbe6e17 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -291,12 +291,13 @@ class TrackDownloadWorkerSignals(QObject): class TrackDownloadWorker(QRunnable): """Background worker to download individual tracks via Soulseek""" - def __init__(self, spotify_track, soulseek_client, download_index, track_index): + def __init__(self, spotify_track, soulseek_client, download_index, track_index, quality_preference=None): super().__init__() self.spotify_track = spotify_track self.soulseek_client = soulseek_client self.download_index = download_index self.track_index = track_index + self.quality_preference = quality_preference or 'flac' self.signals = TrackDownloadWorkerSignals() self._cancelled = False @@ -337,7 +338,7 @@ class TrackDownloadWorker(QRunnable): try: download_id = loop.run_until_complete( - self.soulseek_client.search_and_download_best(query) + self.soulseek_client.search_and_download_best(query, self.quality_preference) ) if download_id: break # Success - stop trying other queries @@ -4628,15 +4629,34 @@ class DownloadMissingTracksModal(QDialog): print(f"❌ Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.") if verified_candidates: + # Apply quality preference filtering before returning + from config.settings import config_manager + quality_preference = config_manager.get_quality_preference() + + # Filter candidates by quality preference with smart fallback + if hasattr(self.parent_page, 'soulseek_client'): + quality_filtered = self.parent_page.soulseek_client.filter_results_by_quality_preference( + verified_candidates, quality_preference + ) + + if quality_filtered: + verified_candidates = quality_filtered + print(f"🎯 Applied quality filtering ({quality_preference}): {len(verified_candidates)} candidates remain") + else: + print(f"⚠️ Quality filtering ({quality_preference}) removed all candidates, keeping originals") + best_confidence = verified_candidates[0].confidence best_version = getattr(verified_candidates[0], 'version_type', 'unknown') - print(f"✅ Found {len(verified_candidates)} VERIFIED matches for '{spotify_track.name}'. Best: {best_confidence:.2f} ({best_version})") + 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()})") # Log version breakdown for debugging for candidate in verified_candidates[:3]: # Show top 3 version = getattr(candidate, 'version_type', 'unknown') penalty = getattr(candidate, 'version_penalty', 0.0) - print(f" 🎵 {candidate.confidence:.2f} - {version} (penalty: {penalty:.2f}) - {candidate.filename[:80]}...") + 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]}...") else: print(f"⚠️ No verified matches found for '{spotify_track.name}' after checking file paths.")