enable quality pref.
This commit is contained in:
parent
2ad0dddba4
commit
ed91099a1c
11 changed files with 166 additions and 20 deletions
BIN
config/__pycache__/settings.cpython-310.pyc
Normal file
BIN
config/__pycache__/settings.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -95,5 +95,9 @@ class ConfigManager:
|
||||||
'plex': bool(self.get('plex.base_url')) and bool(self.get('plex.token')),
|
'plex': bool(self.get('plex.base_url')) and bool(self.get('plex.token')),
|
||||||
'soulseek': bool(self.get('soulseek.slskd_url'))
|
'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()
|
config_manager = ConfigManager()
|
||||||
Binary file not shown.
|
|
@ -1137,15 +1137,19 @@ class SoulseekClient:
|
||||||
logger.warning(f"No results found for: {query}")
|
logger.warning(f"No results found for: {query}")
|
||||||
return None
|
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:
|
if not filtered_results:
|
||||||
best_result = preferred_results[0]
|
logger.warning(f"No suitable quality results found for: {query}")
|
||||||
else:
|
return None
|
||||||
best_result = results[0]
|
|
||||||
logger.info(f"Preferred quality {preferred_quality} not found, using {best_result.quality}")
|
best_result = filtered_results[0]
|
||||||
|
quality_info = f"{best_result.quality.upper()}"
|
||||||
logger.info(f"Downloading: {best_result.filename} ({best_result.quality}) from {best_result.username}")
|
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)
|
return await self.download(best_result.username, best_result.filename, best_result.size)
|
||||||
|
|
||||||
async def check_connection(self) -> bool:
|
async def check_connection(self) -> bool:
|
||||||
|
|
@ -1160,6 +1164,74 @@ class SoulseekClient:
|
||||||
logger.debug(f"Connection check failed: {e}")
|
logger.debug(f"Connection check failed: {e}")
|
||||||
return False
|
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]]:
|
async def get_session_info(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Get slskd session information including version"""
|
"""Get slskd session information including version"""
|
||||||
if not self.base_url:
|
if not self.base_url:
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2714,9 +2714,26 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
||||||
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:
|
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_confidence = verified_candidates[0].confidence
|
||||||
best_version = getattr(verified_candidates[0], 'version_type', 'unknown')
|
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
|
# Log version breakdown for debugging
|
||||||
version_counts = {}
|
version_counts = {}
|
||||||
|
|
@ -2724,7 +2741,9 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
||||||
version = getattr(candidate, 'version_type', 'unknown')
|
version = getattr(candidate, 'version_type', 'unknown')
|
||||||
version_counts[version] = version_counts.get(version, 0) + 1
|
version_counts[version] = version_counts.get(version, 0) + 1
|
||||||
penalty = getattr(candidate, 'version_penalty', 0.0)
|
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:
|
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.")
|
||||||
|
|
|
||||||
|
|
@ -579,6 +579,22 @@ class SettingsPage(QWidget):
|
||||||
if hasattr(self, 'log_path_display'):
|
if hasattr(self, 'log_path_display'):
|
||||||
self.log_path_display.setText(logging_config.get('path', 'logs/app.log'))
|
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:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, "Error", f"Failed to load configuration: {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())
|
max_workers = int(self.max_workers_combo.currentText())
|
||||||
config_manager.set('database.max_workers', max_workers)
|
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
|
# 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.download_path', self.download_path_input.text())
|
||||||
self.settings_changed.emit('soulseek.transfer_path', self.transfer_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 = QLabel("Preferred Quality:")
|
||||||
quality_label.setStyleSheet("color: #ffffff; font-size: 12px;")
|
quality_label.setStyleSheet("color: #ffffff; font-size: 12px;")
|
||||||
|
|
||||||
quality_combo = QComboBox()
|
self.quality_combo = QComboBox()
|
||||||
quality_combo.addItems(["FLAC", "320 kbps MP3", "256 kbps MP3", "192 kbps MP3", "Any"])
|
self.quality_combo.addItems(["FLAC", "320 kbps MP3", "256 kbps MP3", "192 kbps MP3", "Any"])
|
||||||
quality_combo.setCurrentText("FLAC")
|
self.quality_combo.setCurrentText("FLAC")
|
||||||
quality_combo.setStyleSheet(self.get_combo_style())
|
self.quality_combo.setStyleSheet(self.get_combo_style())
|
||||||
quality_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
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_label)
|
||||||
quality_layout.addWidget(quality_combo)
|
quality_layout.addWidget(self.quality_combo)
|
||||||
|
|
||||||
# Download path
|
# Download path
|
||||||
path_container = QVBoxLayout()
|
path_container = QVBoxLayout()
|
||||||
|
|
|
||||||
|
|
@ -291,12 +291,13 @@ class TrackDownloadWorkerSignals(QObject):
|
||||||
class TrackDownloadWorker(QRunnable):
|
class TrackDownloadWorker(QRunnable):
|
||||||
"""Background worker to download individual tracks via Soulseek"""
|
"""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__()
|
super().__init__()
|
||||||
self.spotify_track = spotify_track
|
self.spotify_track = spotify_track
|
||||||
self.soulseek_client = soulseek_client
|
self.soulseek_client = soulseek_client
|
||||||
self.download_index = download_index
|
self.download_index = download_index
|
||||||
self.track_index = track_index
|
self.track_index = track_index
|
||||||
|
self.quality_preference = quality_preference or 'flac'
|
||||||
self.signals = TrackDownloadWorkerSignals()
|
self.signals = TrackDownloadWorkerSignals()
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
|
|
||||||
|
|
@ -337,7 +338,7 @@ class TrackDownloadWorker(QRunnable):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
download_id = loop.run_until_complete(
|
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:
|
if download_id:
|
||||||
break # Success - stop trying other queries
|
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.")
|
print(f"❌ Artist '{spotify_artist_name}' NOT found in path: '{slskd_full_path}'. Discarding candidate.")
|
||||||
|
|
||||||
if verified_candidates:
|
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_confidence = verified_candidates[0].confidence
|
||||||
best_version = getattr(verified_candidates[0], 'version_type', 'unknown')
|
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
|
# Log version breakdown for debugging
|
||||||
for candidate in verified_candidates[:3]: # Show top 3
|
for candidate in verified_candidates[:3]: # Show top 3
|
||||||
version = getattr(candidate, 'version_type', 'unknown')
|
version = getattr(candidate, 'version_type', 'unknown')
|
||||||
penalty = getattr(candidate, 'version_penalty', 0.0)
|
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:
|
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.")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue