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",
|
||||
"level": "INFO"
|
||||
},
|
||||
"settings": {
|
||||
"audio_quality": "flac"
|
||||
},
|
||||
"database": {
|
||||
"path": "database/music_library.db",
|
||||
"max_workers": 5
|
||||
|
|
|
|||
|
|
@ -140,11 +140,7 @@ class ConfigManager:
|
|||
validation['jellyfin'] = bool(self.get('jellyfin.base_url')) and bool(self.get('jellyfin.api_key'))
|
||||
validation['navidrome'] = bool(self.get('navidrome.base_url')) and bool(self.get('navidrome.username')) and bool(self.get('navidrome.password'))
|
||||
validation['active_media_server'] = active_server
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -1246,25 +1246,25 @@ class SoulseekClient:
|
|||
logger.error(f"Error during search history buffer maintenance: {e}")
|
||||
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)
|
||||
|
||||
|
||||
if not results:
|
||||
logger.warning(f"No results found for: {query}")
|
||||
return None
|
||||
|
||||
# Use the new quality filtering
|
||||
filtered_results = self.filter_results_by_quality_preference(results, preferred_quality)
|
||||
|
||||
|
||||
# Use quality profile filtering
|
||||
filtered_results = self.filter_results_by_quality_preference(results)
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -1280,73 +1280,131 @@ 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]:
|
||||
def filter_results_by_quality_preference(self, results: List[TrackResult]) -> List[TrackResult]:
|
||||
"""
|
||||
Filter and sort results by quality preference with smart fallback.
|
||||
Prefers exact match, then higher quality, then lower quality.
|
||||
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
|
||||
|
||||
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'
|
||||
|
||||
# Get quality profile from database
|
||||
db = MusicDatabase()
|
||||
profile = db.get_quality_profile()
|
||||
|
||||
logger.debug(f"Quality Filter: Using profile preset '{profile.get('preset', 'custom')}', filtering {len(results)} candidates")
|
||||
|
||||
# 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:
|
||||
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
|
||||
|
||||
# Track all candidates that pass size checks (for fallback)
|
||||
size_filtered_all = []
|
||||
|
||||
for candidate in results:
|
||||
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:
|
||||
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:
|
||||
# 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
|
||||
logger.warning(f"Quality Filter: No enabled qualities matched and fallback is disabled, returning empty")
|
||||
return []
|
||||
|
||||
async def get_session_info(self) -> Optional[Dict[str, Any]]:
|
||||
"""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.")
|
||||
|
||||
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
|
||||
# Apply quality profile filtering before returning
|
||||
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
|
||||
verified_candidates
|
||||
)
|
||||
|
||||
|
||||
if 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:
|
||||
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_version = getattr(verified_candidates[0], 'version_type', 'unknown')
|
||||
|
|
|
|||
|
|
@ -1323,22 +1323,18 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
|||
try:
|
||||
# Update status: Starting search
|
||||
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
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
try:
|
||||
# Update status: Found candidates, analyzing
|
||||
self.signals.status_updated.emit(self.download_index, "🔎 Analyzing results...")
|
||||
|
||||
|
||||
# Use the enhanced search method that provides more feedback
|
||||
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:
|
||||
|
|
@ -1369,35 +1365,33 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
|||
except Exception as 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"""
|
||||
try:
|
||||
# Emit search progress
|
||||
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
|
||||
# but in a real implementation, you'd want to separate search from download
|
||||
|
||||
|
||||
# This is a simplified version - in practice you'd want to:
|
||||
# 1. Search for candidates
|
||||
# 2. Filter by quality
|
||||
# 2. Filter by quality profile
|
||||
# 3. Return the results for manual download
|
||||
|
||||
|
||||
# For now, let's use a direct approach
|
||||
from core.soulseek_client import SoulseekClient
|
||||
if hasattr(self.soulseek_client, 'search_tracks'):
|
||||
results = await self.soulseek_client.search_tracks(query)
|
||||
|
||||
|
||||
if results:
|
||||
# Filter by quality preference if needed
|
||||
filtered_results = self.soulseek_client.filter_results_by_quality_preference(
|
||||
results, quality_preference
|
||||
)
|
||||
# Filter by quality profile
|
||||
filtered_results = self.soulseek_client.filter_results_by_quality_preference(results)
|
||||
return filtered_results
|
||||
|
||||
|
||||
return []
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in search with progress: {e}")
|
||||
return []
|
||||
|
|
@ -4149,10 +4143,6 @@ class AutoWishlistProcessorWorker(QRunnable):
|
|||
def run(self):
|
||||
"""Run automatic wishlist processing"""
|
||||
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)
|
||||
wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download()
|
||||
|
||||
|
|
@ -4188,9 +4178,9 @@ class AutoWishlistProcessorWorker(QRunnable):
|
|||
|
||||
try:
|
||||
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')
|
||||
|
||||
if download_id and track_id:
|
||||
|
|
|
|||
|
|
@ -1258,23 +1258,7 @@ 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)
|
||||
|
||||
|
||||
# Load metadata enhancement settings
|
||||
metadata_config = config_manager.get('metadata_enhancement', {})
|
||||
if hasattr(self, 'metadata_enabled_checkbox'):
|
||||
|
|
@ -1329,21 +1313,7 @@ class SettingsPage(QWidget):
|
|||
if hasattr(self, 'max_workers_combo'):
|
||||
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())
|
||||
|
|
@ -2721,22 +2691,7 @@ class SettingsPage(QWidget):
|
|||
download_layout = QVBoxLayout(download_group)
|
||||
download_layout.setContentsMargins(16, 20, 16, 16)
|
||||
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
|
||||
path_container = QVBoxLayout()
|
||||
path_label = QLabel("Slskd Download Dir:")
|
||||
|
|
@ -2776,8 +2731,7 @@ class SettingsPage(QWidget):
|
|||
transfer_input_layout.addWidget(self.transfer_path_input)
|
||||
transfer_input_layout.addWidget(transfer_browse_btn)
|
||||
transfer_path_container.addLayout(transfer_input_layout)
|
||||
|
||||
download_layout.addLayout(quality_layout)
|
||||
|
||||
download_layout.addLayout(path_container)
|
||||
download_layout.addLayout(transfer_path_container)
|
||||
|
||||
|
|
|
|||
|
|
@ -3068,40 +3068,9 @@ class SyncOptionsPanel(QFrame):
|
|||
border: 2px solid #1db954;
|
||||
}
|
||||
""")
|
||||
|
||||
# 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(self.download_missing)
|
||||
layout.addLayout(quality_layout)
|
||||
|
||||
class SyncPage(QWidget):
|
||||
# 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.")
|
||||
|
||||
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
|
||||
# Apply quality profile filtering before returning
|
||||
if hasattr(self.parent_page, 'soulseek_client'):
|
||||
quality_filtered = self.parent_page.soulseek_client.filter_results_by_quality_preference(
|
||||
verified_candidates, quality_preference
|
||||
verified_candidates
|
||||
)
|
||||
|
||||
|
||||
if 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:
|
||||
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_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 ==
|
||||
# ===============================
|
||||
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
return []
|
||||
|
||||
# Filter by user's quality preference before artist verification
|
||||
quality_filtered_candidates = _filter_candidates_by_quality_preference(initial_candidates)
|
||||
# Filter by user's quality profile before artist verification
|
||||
# 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 no candidates match preference, fall back to all candidates
|
||||
# If no candidates match profile, fall back to all candidates
|
||||
quality_filtered_candidates = initial_candidates
|
||||
|
||||
verified_candidates = []
|
||||
|
|
|
|||
|
|
@ -2269,18 +2269,7 @@
|
|||
<!-- Download Settings -->
|
||||
<div class="settings-group">
|
||||
<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">
|
||||
<label>Slskd Download Dir:</label>
|
||||
<div class="path-input-group">
|
||||
|
|
|
|||
|
|
@ -1466,9 +1466,8 @@ async function loadSettingsData() {
|
|||
// Populate Soulseek settings
|
||||
document.getElementById('soulseek-url').value = settings.soulseek?.slskd_url || '';
|
||||
document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || '';
|
||||
|
||||
|
||||
// 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('transfer-path').value = settings.soulseek?.transfer_path || './Transfer';
|
||||
|
||||
|
|
@ -1776,9 +1775,6 @@ async function saveSettings() {
|
|||
download_path: document.getElementById('download-path').value,
|
||||
transfer_path: document.getElementById('transfer-path').value
|
||||
},
|
||||
settings: {
|
||||
audio_quality: document.getElementById('preferred-quality').value
|
||||
},
|
||||
database: {
|
||||
max_workers: parseInt(document.getElementById('max-workers').value)
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue