utilize quality profile over preferred.
This commit is contained in:
parent
14cfbd01f4
commit
a3f98f29d3
10 changed files with 166 additions and 345 deletions
|
|
@ -36,9 +36,6 @@
|
||||||
"path": "logs/app.log",
|
"path": "logs/app.log",
|
||||||
"level": "INFO"
|
"level": "INFO"
|
||||||
},
|
},
|
||||||
"settings": {
|
|
||||||
"audio_quality": "flac"
|
|
||||||
},
|
|
||||||
"database": {
|
"database": {
|
||||||
"path": "database/music_library.db",
|
"path": "database/music_library.db",
|
||||||
"max_workers": 5
|
"max_workers": 5
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,4 @@ class ConfigManager:
|
||||||
|
|
||||||
return validation
|
return validation
|
||||||
|
|
||||||
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()
|
||||||
|
|
@ -1246,15 +1246,15 @@ class SoulseekClient:
|
||||||
logger.error(f"Error during search history buffer maintenance: {e}")
|
logger.error(f"Error during search history buffer maintenance: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def search_and_download_best(self, query: str, preferred_quality: str = 'flac') -> Optional[str]:
|
async def search_and_download_best(self, query: str) -> Optional[str]:
|
||||||
results = await self.search(query)
|
results = await self.search(query)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
logger.warning(f"No results found for: {query}")
|
logger.warning(f"No results found for: {query}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Use the new quality filtering
|
# Use quality profile filtering
|
||||||
filtered_results = self.filter_results_by_quality_preference(results, preferred_quality)
|
filtered_results = self.filter_results_by_quality_preference(results)
|
||||||
|
|
||||||
if not filtered_results:
|
if not filtered_results:
|
||||||
logger.warning(f"No suitable quality results found for: {query}")
|
logger.warning(f"No suitable quality results found for: {query}")
|
||||||
|
|
@ -1280,73 +1280,131 @@ 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]:
|
def filter_results_by_quality_preference(self, results: List[TrackResult]) -> List[TrackResult]:
|
||||||
"""
|
"""
|
||||||
Filter and sort results by quality preference with smart fallback.
|
Filter candidates based on user's quality profile with file size constraints.
|
||||||
Prefers exact match, then higher quality, then lower quality.
|
Uses priority waterfall logic: tries highest priority quality first, falls back to lower priorities.
|
||||||
|
Returns candidates matching quality profile constraints, sorted by confidence and size.
|
||||||
"""
|
"""
|
||||||
|
from database.music_database import MusicDatabase
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Normalize preference to match our quality strings
|
# Get quality profile from database
|
||||||
quality_map = {
|
db = MusicDatabase()
|
||||||
'flac': 'flac',
|
profile = db.get_quality_profile()
|
||||||
'mp3_320': ('mp3', 320),
|
|
||||||
'mp3_256': ('mp3', 256),
|
logger.debug(f"Quality Filter: Using profile preset '{profile.get('preset', 'custom')}', filtering {len(results)} candidates")
|
||||||
'mp3_192': ('mp3', 192),
|
|
||||||
'any': 'any'
|
# Categorize candidates by quality with file size constraints
|
||||||
|
quality_buckets = {
|
||||||
|
'flac': [],
|
||||||
|
'mp3_320': [],
|
||||||
|
'mp3_256': [],
|
||||||
|
'mp3_192': [],
|
||||||
|
'other': []
|
||||||
}
|
}
|
||||||
|
|
||||||
if preferred_quality not in quality_map:
|
# Track all candidates that pass size checks (for fallback)
|
||||||
return results # Return all if unknown preference
|
size_filtered_all = []
|
||||||
|
|
||||||
if preferred_quality == 'any':
|
for candidate in results:
|
||||||
# Sort by quality score for "any" preference
|
if not candidate.quality:
|
||||||
return sorted(results, key=lambda x: x.quality_score, reverse=True)
|
quality_buckets['other'].append(candidate)
|
||||||
|
continue
|
||||||
|
|
||||||
# Separate results by quality categories
|
track_format = candidate.quality.lower()
|
||||||
exact_matches = []
|
track_bitrate = candidate.bitrate or 0
|
||||||
higher_quality = []
|
file_size_mb = candidate.size / (1024 * 1024) # Convert bytes to MB
|
||||||
lower_quality = []
|
|
||||||
|
|
||||||
if preferred_quality == 'flac':
|
# Categorize and apply file size constraints
|
||||||
for result in results:
|
if track_format == 'flac':
|
||||||
if result.quality.lower() == 'flac':
|
quality_config = profile['qualities'].get('flac', {})
|
||||||
exact_matches.append(result)
|
min_mb = quality_config.get('min_mb', 0)
|
||||||
elif result.quality.lower() == 'mp3' and result.bitrate and result.bitrate >= 320:
|
max_mb = quality_config.get('max_mb', 999)
|
||||||
higher_quality.append(result) # High-quality MP3 as fallback
|
|
||||||
|
# Check if within size range
|
||||||
|
if min_mb <= file_size_mb <= max_mb:
|
||||||
|
# Add to bucket if enabled
|
||||||
|
if quality_config.get('enabled', False):
|
||||||
|
quality_buckets['flac'].append(candidate)
|
||||||
|
# Always track for fallback
|
||||||
|
size_filtered_all.append(candidate)
|
||||||
else:
|
else:
|
||||||
lower_quality.append(result)
|
logger.debug(f"Quality Filter: FLAC file rejected - {file_size_mb:.1f}MB outside range {min_mb}-{max_mb}MB")
|
||||||
|
|
||||||
|
elif track_format == 'mp3':
|
||||||
|
# Determine MP3 quality tier based on bitrate
|
||||||
|
if track_bitrate >= 320:
|
||||||
|
quality_key = 'mp3_320'
|
||||||
|
elif track_bitrate >= 256:
|
||||||
|
quality_key = 'mp3_256'
|
||||||
|
elif track_bitrate >= 192:
|
||||||
|
quality_key = 'mp3_192'
|
||||||
|
else:
|
||||||
|
quality_buckets['other'].append(candidate)
|
||||||
|
continue
|
||||||
|
|
||||||
|
quality_config = profile['qualities'].get(quality_key, {})
|
||||||
|
min_mb = quality_config.get('min_mb', 0)
|
||||||
|
max_mb = quality_config.get('max_mb', 999)
|
||||||
|
|
||||||
|
# Check if within size range
|
||||||
|
if min_mb <= file_size_mb <= max_mb:
|
||||||
|
# Add to bucket if enabled
|
||||||
|
if quality_config.get('enabled', False):
|
||||||
|
quality_buckets[quality_key].append(candidate)
|
||||||
|
# Always track for fallback
|
||||||
|
size_filtered_all.append(candidate)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Quality Filter: {quality_key.upper()} file rejected - {file_size_mb:.1f}MB outside range {min_mb}-{max_mb}MB")
|
||||||
|
else:
|
||||||
|
quality_buckets['other'].append(candidate)
|
||||||
|
|
||||||
|
# Sort each bucket by quality score and size
|
||||||
|
for bucket in quality_buckets.values():
|
||||||
|
bucket.sort(key=lambda x: (x.quality_score, x.size), reverse=True)
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
for quality, bucket in quality_buckets.items():
|
||||||
|
if bucket:
|
||||||
|
logger.debug(f"Quality Filter: Found {len(bucket)} '{quality}' candidates (after size filtering)")
|
||||||
|
|
||||||
|
# Waterfall priority logic: try qualities in priority order
|
||||||
|
# Build priority list from enabled qualities
|
||||||
|
quality_priorities = []
|
||||||
|
for quality_name, quality_config in profile['qualities'].items():
|
||||||
|
if quality_config.get('enabled', False):
|
||||||
|
priority = quality_config.get('priority', 999)
|
||||||
|
quality_priorities.append((priority, quality_name))
|
||||||
|
|
||||||
|
# Sort by priority (lower number = higher priority)
|
||||||
|
quality_priorities.sort()
|
||||||
|
|
||||||
|
# Try each quality in priority order
|
||||||
|
for priority, quality_name in quality_priorities:
|
||||||
|
candidates_for_quality = quality_buckets.get(quality_name, [])
|
||||||
|
if candidates_for_quality:
|
||||||
|
logger.info(f"Quality Filter: Returning {len(candidates_for_quality)} '{quality_name}' candidates (priority {priority})")
|
||||||
|
return candidates_for_quality
|
||||||
|
|
||||||
|
# If no enabled qualities matched, check if fallback is enabled
|
||||||
|
if profile.get('fallback_enabled', True):
|
||||||
|
logger.warning(f"Quality Filter: No enabled qualities matched, falling back to size-filtered candidates")
|
||||||
|
# Return candidates that passed size checks (even if quality disabled)
|
||||||
|
# This respects file size constraints while allowing any quality
|
||||||
|
if size_filtered_all:
|
||||||
|
size_filtered_all.sort(key=lambda x: (x.quality_score, x.size), reverse=True)
|
||||||
|
logger.info(f"Quality Filter: Returning {len(size_filtered_all)} fallback candidates (size-filtered, any quality)")
|
||||||
|
return size_filtered_all
|
||||||
|
else:
|
||||||
|
# All candidates failed size checks - respect user's constraints and fail
|
||||||
|
logger.warning(f"Quality Filter: All candidates failed size checks, returning empty (respecting size constraints)")
|
||||||
|
return []
|
||||||
else:
|
else:
|
||||||
# MP3 preference with specific bitrate
|
logger.warning(f"Quality Filter: No enabled qualities matched and fallback is disabled, returning empty")
|
||||||
pref_format, pref_bitrate = quality_map[preferred_quality]
|
return []
|
||||||
|
|
||||||
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"""
|
||||||
|
|
|
||||||
|
|
@ -3039,21 +3039,17 @@ 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
|
# Apply quality profile 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'):
|
if hasattr(self.parent_artists_page, 'soulseek_client'):
|
||||||
quality_filtered = self.parent_artists_page.soulseek_client.filter_results_by_quality_preference(
|
quality_filtered = self.parent_artists_page.soulseek_client.filter_results_by_quality_preference(
|
||||||
verified_candidates, quality_preference
|
verified_candidates
|
||||||
)
|
)
|
||||||
|
|
||||||
if quality_filtered:
|
if quality_filtered:
|
||||||
verified_candidates = quality_filtered
|
verified_candidates = quality_filtered
|
||||||
print(f"🎯 Applied quality filtering ({quality_preference}): {len(verified_candidates)} candidates remain")
|
print(f"🎯 Applied quality profile filtering: {len(verified_candidates)} candidates remain")
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Quality filtering ({quality_preference}) removed all candidates, keeping originals")
|
print(f"⚠️ Quality profile filtering 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')
|
||||||
|
|
|
||||||
|
|
@ -1324,10 +1324,6 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
||||||
# Update status: Starting search
|
# Update status: Starting search
|
||||||
self.signals.status_updated.emit(self.download_index, "🔍 Searching...")
|
self.signals.status_updated.emit(self.download_index, "🔍 Searching...")
|
||||||
|
|
||||||
# Get quality preference
|
|
||||||
from config.settings import config_manager
|
|
||||||
quality_preference = config_manager.get_quality_preference()
|
|
||||||
|
|
||||||
# Use async method in sync context
|
# Use async method in sync context
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
@ -1338,7 +1334,7 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
||||||
|
|
||||||
# Use the enhanced search method that provides more feedback
|
# Use the enhanced search method that provides more feedback
|
||||||
results = loop.run_until_complete(
|
results = loop.run_until_complete(
|
||||||
self._search_with_progress(self.query, quality_preference)
|
self._search_with_progress(self.query)
|
||||||
)
|
)
|
||||||
|
|
||||||
if results and len(results) > 0:
|
if results and len(results) > 0:
|
||||||
|
|
@ -1369,7 +1365,7 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.signals.download_failed.emit(self.download_index, str(e))
|
self.signals.download_failed.emit(self.download_index, str(e))
|
||||||
|
|
||||||
async def _search_with_progress(self, query, quality_preference):
|
async def _search_with_progress(self, query):
|
||||||
"""Search for tracks with progress updates"""
|
"""Search for tracks with progress updates"""
|
||||||
try:
|
try:
|
||||||
# Emit search progress
|
# Emit search progress
|
||||||
|
|
@ -1381,7 +1377,7 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
||||||
|
|
||||||
# This is a simplified version - in practice you'd want to:
|
# This is a simplified version - in practice you'd want to:
|
||||||
# 1. Search for candidates
|
# 1. Search for candidates
|
||||||
# 2. Filter by quality
|
# 2. Filter by quality profile
|
||||||
# 3. Return the results for manual download
|
# 3. Return the results for manual download
|
||||||
|
|
||||||
# For now, let's use a direct approach
|
# For now, let's use a direct approach
|
||||||
|
|
@ -1390,10 +1386,8 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
||||||
results = await self.soulseek_client.search_tracks(query)
|
results = await self.soulseek_client.search_tracks(query)
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
# Filter by quality preference if needed
|
# Filter by quality profile
|
||||||
filtered_results = self.soulseek_client.filter_results_by_quality_preference(
|
filtered_results = self.soulseek_client.filter_results_by_quality_preference(results)
|
||||||
results, quality_preference
|
|
||||||
)
|
|
||||||
return filtered_results
|
return filtered_results
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
@ -4149,10 +4143,6 @@ class AutoWishlistProcessorWorker(QRunnable):
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run automatic wishlist processing"""
|
"""Run automatic wishlist processing"""
|
||||||
try:
|
try:
|
||||||
# Get quality preference
|
|
||||||
from config.settings import config_manager
|
|
||||||
quality_preference = config_manager.get_quality_preference()
|
|
||||||
|
|
||||||
# Get all wishlist tracks (no limit - process everything)
|
# Get all wishlist tracks (no limit - process everything)
|
||||||
wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download()
|
wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download()
|
||||||
|
|
||||||
|
|
@ -4188,7 +4178,7 @@ class AutoWishlistProcessorWorker(QRunnable):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
download_id = loop.run_until_complete(
|
download_id = loop.run_until_complete(
|
||||||
self.soulseek_client.search_and_download_best(query, quality_preference)
|
self.soulseek_client.search_and_download_best(query)
|
||||||
)
|
)
|
||||||
|
|
||||||
track_id = track_data.get('spotify_track_id')
|
track_id = track_data.get('spotify_track_id')
|
||||||
|
|
|
||||||
|
|
@ -1259,22 +1259,6 @@ 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)
|
|
||||||
|
|
||||||
# Load metadata enhancement settings
|
# Load metadata enhancement settings
|
||||||
metadata_config = config_manager.get('metadata_enhancement', {})
|
metadata_config = config_manager.get('metadata_enhancement', {})
|
||||||
if hasattr(self, 'metadata_enabled_checkbox'):
|
if hasattr(self, 'metadata_enabled_checkbox'):
|
||||||
|
|
@ -1330,20 +1314,6 @@ 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())
|
||||||
|
|
@ -2722,21 +2692,6 @@ class SettingsPage(QWidget):
|
||||||
download_layout.setContentsMargins(16, 20, 16, 16)
|
download_layout.setContentsMargins(16, 20, 16, 16)
|
||||||
download_layout.setSpacing(12)
|
download_layout.setSpacing(12)
|
||||||
|
|
||||||
# Quality preference
|
|
||||||
quality_layout = QHBoxLayout()
|
|
||||||
quality_label = QLabel("Preferred Quality:")
|
|
||||||
quality_label.setStyleSheet(self.get_label_style(12))
|
|
||||||
|
|
||||||
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(self.quality_combo)
|
|
||||||
|
|
||||||
# Download path
|
# Download path
|
||||||
path_container = QVBoxLayout()
|
path_container = QVBoxLayout()
|
||||||
path_label = QLabel("Slskd Download Dir:")
|
path_label = QLabel("Slskd Download Dir:")
|
||||||
|
|
@ -2777,7 +2732,6 @@ class SettingsPage(QWidget):
|
||||||
transfer_input_layout.addWidget(transfer_browse_btn)
|
transfer_input_layout.addWidget(transfer_browse_btn)
|
||||||
transfer_path_container.addLayout(transfer_input_layout)
|
transfer_path_container.addLayout(transfer_input_layout)
|
||||||
|
|
||||||
download_layout.addLayout(quality_layout)
|
|
||||||
download_layout.addLayout(path_container)
|
download_layout.addLayout(path_container)
|
||||||
download_layout.addLayout(transfer_path_container)
|
download_layout.addLayout(transfer_path_container)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3069,39 +3069,8 @@ class SyncOptionsPanel(QFrame):
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Quality selection
|
|
||||||
quality_layout = QHBoxLayout()
|
|
||||||
quality_label = QLabel("Preferred Quality:")
|
|
||||||
quality_label.setStyleSheet("color: #b3b3b3; font-size: 11px;")
|
|
||||||
|
|
||||||
self.quality_combo = QComboBox()
|
|
||||||
self.quality_combo.addItems(["FLAC", "320 kbps MP3", "256 kbps MP3", "Any"])
|
|
||||||
self.quality_combo.setCurrentText("FLAC")
|
|
||||||
self.quality_combo.setStyleSheet("""
|
|
||||||
QComboBox {
|
|
||||||
background: #404040;
|
|
||||||
border: 1px solid #606060;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
QComboBox::drop-down {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
QComboBox::down-arrow {
|
|
||||||
image: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
quality_layout.addWidget(quality_label)
|
|
||||||
quality_layout.addWidget(self.quality_combo)
|
|
||||||
quality_layout.addStretch()
|
|
||||||
|
|
||||||
layout.addWidget(title_label)
|
layout.addWidget(title_label)
|
||||||
layout.addWidget(self.download_missing)
|
layout.addWidget(self.download_missing)
|
||||||
layout.addLayout(quality_layout)
|
|
||||||
|
|
||||||
class SyncPage(QWidget):
|
class SyncPage(QWidget):
|
||||||
# Signals for dashboard activity tracking
|
# Signals for dashboard activity tracking
|
||||||
|
|
@ -8935,21 +8904,17 @@ 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
|
# Apply quality profile 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'):
|
if hasattr(self.parent_page, 'soulseek_client'):
|
||||||
quality_filtered = self.parent_page.soulseek_client.filter_results_by_quality_preference(
|
quality_filtered = self.parent_page.soulseek_client.filter_results_by_quality_preference(
|
||||||
verified_candidates, quality_preference
|
verified_candidates
|
||||||
)
|
)
|
||||||
|
|
||||||
if quality_filtered:
|
if quality_filtered:
|
||||||
verified_candidates = quality_filtered
|
verified_candidates = quality_filtered
|
||||||
print(f"🎯 Applied quality filtering ({quality_preference}): {len(verified_candidates)} candidates remain")
|
print(f"🎯 Applied quality profile filtering: {len(verified_candidates)} candidates remain")
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Quality filtering ({quality_preference}) removed all candidates, keeping originals")
|
print(f"⚠️ Quality profile filtering 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')
|
||||||
|
|
|
||||||
132
web_server.py
132
web_server.py
|
|
@ -8728,129 +8728,6 @@ def stop_duplicate_cleaner():
|
||||||
# == DOWNLOAD MISSING TRACKS ==
|
# == DOWNLOAD MISSING TRACKS ==
|
||||||
# ===============================
|
# ===============================
|
||||||
|
|
||||||
def _filter_candidates_by_quality_preference(candidates):
|
|
||||||
"""
|
|
||||||
Filter candidates based on user's quality profile with file size constraints.
|
|
||||||
Uses priority waterfall logic: tries highest priority quality first, falls back to lower priorities.
|
|
||||||
Returns candidates matching quality profile constraints, sorted by confidence and size.
|
|
||||||
"""
|
|
||||||
from database.music_database import MusicDatabase
|
|
||||||
|
|
||||||
# Get quality profile from database
|
|
||||||
db = MusicDatabase()
|
|
||||||
profile = db.get_quality_profile()
|
|
||||||
|
|
||||||
print(f"🎵 [Quality Filter] Using profile preset: '{profile.get('preset', 'custom')}', filtering {len(candidates)} candidates")
|
|
||||||
|
|
||||||
# Categorize candidates by quality with file size constraints
|
|
||||||
quality_buckets = {
|
|
||||||
'flac': [],
|
|
||||||
'mp3_320': [],
|
|
||||||
'mp3_256': [],
|
|
||||||
'mp3_192': [],
|
|
||||||
'other': []
|
|
||||||
}
|
|
||||||
|
|
||||||
# Track all candidates that pass size checks (for fallback)
|
|
||||||
size_filtered_all = []
|
|
||||||
|
|
||||||
for candidate in candidates:
|
|
||||||
if not candidate.quality:
|
|
||||||
quality_buckets['other'].append(candidate)
|
|
||||||
continue
|
|
||||||
|
|
||||||
track_format = candidate.quality.lower()
|
|
||||||
track_bitrate = candidate.bitrate or 0
|
|
||||||
file_size_mb = candidate.size / (1024 * 1024) # Convert bytes to MB
|
|
||||||
|
|
||||||
# Categorize and apply file size constraints
|
|
||||||
if track_format == 'flac':
|
|
||||||
quality_config = profile['qualities'].get('flac', {})
|
|
||||||
min_mb = quality_config.get('min_mb', 0)
|
|
||||||
max_mb = quality_config.get('max_mb', 999)
|
|
||||||
|
|
||||||
# Check if within size range
|
|
||||||
if min_mb <= file_size_mb <= max_mb:
|
|
||||||
# Add to bucket if enabled
|
|
||||||
if quality_config.get('enabled', False):
|
|
||||||
quality_buckets['flac'].append(candidate)
|
|
||||||
# Always track for fallback
|
|
||||||
size_filtered_all.append(candidate)
|
|
||||||
else:
|
|
||||||
print(f"🎵 [Quality Filter] FLAC file rejected: {file_size_mb:.1f}MB outside range {min_mb}-{max_mb}MB")
|
|
||||||
|
|
||||||
elif track_format == 'mp3':
|
|
||||||
# Determine MP3 quality tier based on bitrate
|
|
||||||
if track_bitrate >= 320:
|
|
||||||
quality_key = 'mp3_320'
|
|
||||||
elif track_bitrate >= 256:
|
|
||||||
quality_key = 'mp3_256'
|
|
||||||
elif track_bitrate >= 192:
|
|
||||||
quality_key = 'mp3_192'
|
|
||||||
else:
|
|
||||||
quality_buckets['other'].append(candidate)
|
|
||||||
continue
|
|
||||||
|
|
||||||
quality_config = profile['qualities'].get(quality_key, {})
|
|
||||||
min_mb = quality_config.get('min_mb', 0)
|
|
||||||
max_mb = quality_config.get('max_mb', 999)
|
|
||||||
|
|
||||||
# Check if within size range
|
|
||||||
if min_mb <= file_size_mb <= max_mb:
|
|
||||||
# Add to bucket if enabled
|
|
||||||
if quality_config.get('enabled', False):
|
|
||||||
quality_buckets[quality_key].append(candidate)
|
|
||||||
# Always track for fallback
|
|
||||||
size_filtered_all.append(candidate)
|
|
||||||
else:
|
|
||||||
print(f"🎵 [Quality Filter] {quality_key.upper()} file rejected: {file_size_mb:.1f}MB outside range {min_mb}-{max_mb}MB")
|
|
||||||
else:
|
|
||||||
quality_buckets['other'].append(candidate)
|
|
||||||
|
|
||||||
# Sort each bucket by quality score and size
|
|
||||||
for bucket in quality_buckets.values():
|
|
||||||
bucket.sort(key=lambda x: (x.quality_score, x.size), reverse=True)
|
|
||||||
|
|
||||||
# Debug logging
|
|
||||||
for quality, bucket in quality_buckets.items():
|
|
||||||
if bucket:
|
|
||||||
print(f"🎵 [Quality Filter] Found {len(bucket)} '{quality}' candidates (after size filtering)")
|
|
||||||
|
|
||||||
# Waterfall priority logic: try qualities in priority order
|
|
||||||
# Build priority list from enabled qualities
|
|
||||||
quality_priorities = []
|
|
||||||
for quality_name, quality_config in profile['qualities'].items():
|
|
||||||
if quality_config.get('enabled', False):
|
|
||||||
priority = quality_config.get('priority', 999)
|
|
||||||
quality_priorities.append((priority, quality_name))
|
|
||||||
|
|
||||||
# Sort by priority (lower number = higher priority)
|
|
||||||
quality_priorities.sort()
|
|
||||||
|
|
||||||
# Try each quality in priority order
|
|
||||||
for priority, quality_name in quality_priorities:
|
|
||||||
candidates_for_quality = quality_buckets.get(quality_name, [])
|
|
||||||
if candidates_for_quality:
|
|
||||||
print(f"🎯 [Quality Filter] Returning {len(candidates_for_quality)} '{quality_name}' candidates (priority {priority})")
|
|
||||||
return candidates_for_quality
|
|
||||||
|
|
||||||
# If no enabled qualities matched, check if fallback is enabled
|
|
||||||
if profile.get('fallback_enabled', True):
|
|
||||||
print(f"⚠️ [Quality Filter] No enabled qualities matched, falling back to size-filtered candidates")
|
|
||||||
# Return candidates that passed size checks (even if quality disabled)
|
|
||||||
# This respects file size constraints while allowing any quality
|
|
||||||
if size_filtered_all:
|
|
||||||
size_filtered_all.sort(key=lambda x: (x.quality_score, x.size), reverse=True)
|
|
||||||
print(f"🎯 [Quality Filter] Returning {len(size_filtered_all)} fallback candidates (size-filtered, any quality)")
|
|
||||||
return size_filtered_all
|
|
||||||
else:
|
|
||||||
# All candidates failed size checks - respect user's constraints and fail
|
|
||||||
print(f"❌ [Quality Filter] All candidates failed size checks, returning empty (respecting size constraints)")
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
print(f"❌ [Quality Filter] No enabled qualities matched and fallback is disabled, returning empty")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_valid_candidates(results, spotify_track, query):
|
def get_valid_candidates(results, spotify_track, query):
|
||||||
"""
|
"""
|
||||||
This function is a direct port from sync.py. It scores and filters
|
This function is a direct port from sync.py. It scores and filters
|
||||||
|
|
@ -8864,10 +8741,13 @@ def get_valid_candidates(results, spotify_track, query):
|
||||||
if not initial_candidates:
|
if not initial_candidates:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Filter by user's quality preference before artist verification
|
# Filter by user's quality profile before artist verification
|
||||||
quality_filtered_candidates = _filter_candidates_by_quality_preference(initial_candidates)
|
# Use shared soulseek_client method for consistency
|
||||||
|
from core.soulseek_client import SoulseekClient
|
||||||
|
temp_client = SoulseekClient()
|
||||||
|
quality_filtered_candidates = temp_client.filter_results_by_quality_preference(initial_candidates)
|
||||||
if not quality_filtered_candidates:
|
if not quality_filtered_candidates:
|
||||||
# If no candidates match preference, fall back to all candidates
|
# If no candidates match profile, fall back to all candidates
|
||||||
quality_filtered_candidates = initial_candidates
|
quality_filtered_candidates = initial_candidates
|
||||||
|
|
||||||
verified_candidates = []
|
verified_candidates = []
|
||||||
|
|
|
||||||
|
|
@ -2270,17 +2270,6 @@
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<h3>Download Settings</h3>
|
<h3>Download Settings</h3>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Preferred Quality:</label>
|
|
||||||
<select id="preferred-quality">
|
|
||||||
<option value="flac">FLAC</option>
|
|
||||||
<option value="mp3_320">320 kbps MP3</option>
|
|
||||||
<option value="mp3_256">256 kbps MP3</option>
|
|
||||||
<option value="mp3_192">192 kbps MP3</option>
|
|
||||||
<option value="any">Any</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Slskd Download Dir:</label>
|
<label>Slskd Download Dir:</label>
|
||||||
<div class="path-input-group">
|
<div class="path-input-group">
|
||||||
|
|
|
||||||
|
|
@ -1468,7 +1468,6 @@ async function loadSettingsData() {
|
||||||
document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || '';
|
document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || '';
|
||||||
|
|
||||||
// Populate Download settings (right column)
|
// Populate Download settings (right column)
|
||||||
document.getElementById('preferred-quality').value = settings.settings?.audio_quality || 'flac';
|
|
||||||
document.getElementById('download-path').value = settings.soulseek?.download_path || './downloads';
|
document.getElementById('download-path').value = settings.soulseek?.download_path || './downloads';
|
||||||
document.getElementById('transfer-path').value = settings.soulseek?.transfer_path || './Transfer';
|
document.getElementById('transfer-path').value = settings.soulseek?.transfer_path || './Transfer';
|
||||||
|
|
||||||
|
|
@ -1776,9 +1775,6 @@ async function saveSettings() {
|
||||||
download_path: document.getElementById('download-path').value,
|
download_path: document.getElementById('download-path').value,
|
||||||
transfer_path: document.getElementById('transfer-path').value
|
transfer_path: document.getElementById('transfer-path').value
|
||||||
},
|
},
|
||||||
settings: {
|
|
||||||
audio_quality: document.getElementById('preferred-quality').value
|
|
||||||
},
|
|
||||||
database: {
|
database: {
|
||||||
max_workers: parseInt(document.getElementById('max-workers').value)
|
max_workers: parseInt(document.getElementById('max-workers').value)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue