Quality filter: use bitrate density instead of file size, cache Library/Discover pages, extend Beatport cache to 24h
This commit is contained in:
parent
acb26777ca
commit
8cf0950d3b
5 changed files with 152 additions and 130 deletions
|
|
@ -1453,11 +1453,28 @@ class SoulseekClient:
|
|||
logger.debug(f"Connection check failed: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _calculate_effective_kbps(size_bytes: int, duration_ms: Optional[int]) -> Optional[float]:
|
||||
"""Calculate effective bitrate in kbps from file size and duration."""
|
||||
if not duration_ms or duration_ms <= 0 or not size_bytes or size_bytes <= 0:
|
||||
return None
|
||||
duration_seconds = duration_ms / 1000.0
|
||||
return (size_bytes * 8) / duration_seconds / 1000.0
|
||||
|
||||
# Internal fallback size limits (MB) when duration is unavailable — generous to catch only extreme outliers
|
||||
_FALLBACK_SIZE_LIMITS = {
|
||||
'flac': (1, 500),
|
||||
'mp3_320': (1, 50),
|
||||
'mp3_256': (1, 40),
|
||||
'mp3_192': (1, 30),
|
||||
'other': (0, 500),
|
||||
}
|
||||
|
||||
def filter_results_by_quality_preference(self, results: List[TrackResult]) -> List[TrackResult]:
|
||||
"""
|
||||
Filter candidates based on user's quality profile with file size constraints.
|
||||
Filter candidates based on user's quality profile with bitrate density 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.
|
||||
Returns candidates matching quality profile constraints, sorted by confidence and effective bitrate.
|
||||
"""
|
||||
from database.music_database import MusicDatabase
|
||||
|
||||
|
|
@ -1470,7 +1487,7 @@ class SoulseekClient:
|
|||
|
||||
logger.debug(f"Quality Filter: Using profile preset '{profile.get('preset', 'custom')}', filtering {len(results)} candidates")
|
||||
|
||||
# Categorize candidates by quality with file size constraints
|
||||
# Categorize candidates by quality with bitrate density constraints
|
||||
quality_buckets = {
|
||||
'flac': [],
|
||||
'mp3_320': [],
|
||||
|
|
@ -1479,8 +1496,8 @@ class SoulseekClient:
|
|||
'other': []
|
||||
}
|
||||
|
||||
# Track all candidates that pass size checks (for fallback)
|
||||
size_filtered_all = []
|
||||
# Track all candidates that pass checks (for fallback)
|
||||
density_filtered_all = []
|
||||
|
||||
for candidate in results:
|
||||
if not candidate.quality:
|
||||
|
|
@ -1489,26 +1506,11 @@ class SoulseekClient:
|
|||
|
||||
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
|
||||
# Determine quality key
|
||||
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:
|
||||
logger.debug(f"Quality Filter: FLAC file rejected - {file_size_mb:.1f}MB outside range {min_mb}-{max_mb}MB")
|
||||
|
||||
quality_key = 'flac'
|
||||
elif track_format == 'mp3':
|
||||
# Determine MP3 quality tier based on bitrate
|
||||
if track_bitrate >= 320:
|
||||
quality_key = 'mp3_320'
|
||||
elif track_bitrate >= 256:
|
||||
|
|
@ -1518,31 +1520,44 @@ class SoulseekClient:
|
|||
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)
|
||||
continue
|
||||
|
||||
# Sort each bucket by quality score and size
|
||||
quality_config = profile['qualities'].get(quality_key, {})
|
||||
min_kbps = quality_config.get('min_kbps', 0)
|
||||
max_kbps = quality_config.get('max_kbps', 99999)
|
||||
|
||||
effective_kbps = self._calculate_effective_kbps(candidate.size, candidate.duration)
|
||||
|
||||
if effective_kbps is not None:
|
||||
# Primary: bitrate density check
|
||||
if min_kbps <= effective_kbps <= max_kbps:
|
||||
if quality_config.get('enabled', False):
|
||||
quality_buckets[quality_key].append(candidate)
|
||||
density_filtered_all.append(candidate)
|
||||
else:
|
||||
logger.debug(f"Quality Filter: {quality_key} rejected - {effective_kbps:.0f} kbps outside {min_kbps}-{max_kbps} kbps range")
|
||||
else:
|
||||
# Fallback: duration unavailable, use generous raw-size sanity check
|
||||
file_size_mb = candidate.size / (1024 * 1024)
|
||||
size_min, size_max = self._FALLBACK_SIZE_LIMITS.get(quality_key, (0, 500))
|
||||
if size_min <= file_size_mb <= size_max:
|
||||
if quality_config.get('enabled', False):
|
||||
quality_buckets[quality_key].append(candidate)
|
||||
density_filtered_all.append(candidate)
|
||||
logger.debug(f"Quality Filter: {quality_key} accepted via size fallback ({file_size_mb:.1f} MB, no duration available)")
|
||||
else:
|
||||
logger.debug(f"Quality Filter: {quality_key} rejected via size fallback - {file_size_mb:.1f} MB outside {size_min}-{size_max} MB safety limits")
|
||||
|
||||
# Sort each bucket by quality score and effective bitrate
|
||||
for bucket in quality_buckets.values():
|
||||
bucket.sort(key=lambda x: (x.quality_score, x.size), reverse=True)
|
||||
bucket.sort(key=lambda x: (x.quality_score, self._calculate_effective_kbps(x.size, x.duration) or 0), 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)")
|
||||
logger.debug(f"Quality Filter: Found {len(bucket)} '{quality}' candidates (after bitrate filtering)")
|
||||
|
||||
# Waterfall priority logic: try qualities in priority order
|
||||
# Build priority list from enabled qualities
|
||||
|
|
@ -1564,16 +1579,13 @@ class SoulseekClient:
|
|||
|
||||
# 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
|
||||
logger.warning(f"Quality Filter: No enabled qualities matched, falling back to density-filtered candidates")
|
||||
if density_filtered_all:
|
||||
density_filtered_all.sort(key=lambda x: (x.quality_score, self._calculate_effective_kbps(x.size, x.duration) or 0), reverse=True)
|
||||
logger.info(f"Quality Filter: Returning {len(density_filtered_all)} fallback candidates (bitrate-filtered, any quality)")
|
||||
return density_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)")
|
||||
logger.warning(f"Quality Filter: All candidates failed bitrate checks, returning empty (respecting constraints)")
|
||||
return []
|
||||
else:
|
||||
logger.warning(f"Quality Filter: No enabled qualities matched and fallback is disabled, returning empty")
|
||||
|
|
|
|||
|
|
@ -2849,37 +2849,45 @@ class MusicDatabase:
|
|||
|
||||
if profile_json:
|
||||
try:
|
||||
return json.loads(profile_json)
|
||||
profile = json.loads(profile_json)
|
||||
# Migrate v1 profiles (min_mb/max_mb) to v2 (min_kbps/max_kbps)
|
||||
if profile.get('version', 1) < 2:
|
||||
logger.info("Migrating quality profile from v1 (file size) to v2 (bitrate density)")
|
||||
return self._get_default_quality_profile()
|
||||
return profile
|
||||
except json.JSONDecodeError:
|
||||
logger.error("Failed to parse quality profile JSON, returning default")
|
||||
|
||||
# Return smart defaults (balanced preset)
|
||||
return self._get_default_quality_profile()
|
||||
|
||||
def _get_default_quality_profile(self) -> dict:
|
||||
"""Return the default v2 quality profile (balanced preset)"""
|
||||
return {
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"preset": "balanced",
|
||||
"qualities": {
|
||||
"flac": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 150,
|
||||
"min_kbps": 500,
|
||||
"max_kbps": 10000,
|
||||
"priority": 1
|
||||
},
|
||||
"mp3_320": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 20,
|
||||
"min_kbps": 280,
|
||||
"max_kbps": 500,
|
||||
"priority": 2
|
||||
},
|
||||
"mp3_256": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 15,
|
||||
"min_kbps": 200,
|
||||
"max_kbps": 400,
|
||||
"priority": 3
|
||||
},
|
||||
"mp3_192": {
|
||||
"enabled": False,
|
||||
"min_mb": 0,
|
||||
"max_mb": 12,
|
||||
"min_kbps": 150,
|
||||
"max_kbps": 300,
|
||||
"priority": 4
|
||||
}
|
||||
},
|
||||
|
|
@ -2903,93 +2911,93 @@ class MusicDatabase:
|
|||
"""Get a predefined quality preset"""
|
||||
presets = {
|
||||
"audiophile": {
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"preset": "audiophile",
|
||||
"qualities": {
|
||||
"flac": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 200,
|
||||
"min_kbps": 500,
|
||||
"max_kbps": 10000,
|
||||
"priority": 1
|
||||
},
|
||||
"mp3_320": {
|
||||
"enabled": False,
|
||||
"min_mb": 0,
|
||||
"max_mb": 20,
|
||||
"min_kbps": 280,
|
||||
"max_kbps": 500,
|
||||
"priority": 2
|
||||
},
|
||||
"mp3_256": {
|
||||
"enabled": False,
|
||||
"min_mb": 0,
|
||||
"max_mb": 15,
|
||||
"min_kbps": 200,
|
||||
"max_kbps": 400,
|
||||
"priority": 3
|
||||
},
|
||||
"mp3_192": {
|
||||
"enabled": False,
|
||||
"min_mb": 0,
|
||||
"max_mb": 12,
|
||||
"min_kbps": 150,
|
||||
"max_kbps": 300,
|
||||
"priority": 4
|
||||
}
|
||||
},
|
||||
"fallback_enabled": False
|
||||
},
|
||||
"balanced": {
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"preset": "balanced",
|
||||
"qualities": {
|
||||
"flac": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 150,
|
||||
"min_kbps": 500,
|
||||
"max_kbps": 10000,
|
||||
"priority": 1
|
||||
},
|
||||
"mp3_320": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 20,
|
||||
"min_kbps": 280,
|
||||
"max_kbps": 500,
|
||||
"priority": 2
|
||||
},
|
||||
"mp3_256": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 15,
|
||||
"min_kbps": 200,
|
||||
"max_kbps": 400,
|
||||
"priority": 3
|
||||
},
|
||||
"mp3_192": {
|
||||
"enabled": False,
|
||||
"min_mb": 0,
|
||||
"max_mb": 12,
|
||||
"min_kbps": 150,
|
||||
"max_kbps": 300,
|
||||
"priority": 4
|
||||
}
|
||||
},
|
||||
"fallback_enabled": True
|
||||
},
|
||||
"space_saver": {
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"preset": "space_saver",
|
||||
"qualities": {
|
||||
"flac": {
|
||||
"enabled": False,
|
||||
"min_mb": 0,
|
||||
"max_mb": 150,
|
||||
"min_kbps": 500,
|
||||
"max_kbps": 10000,
|
||||
"priority": 4
|
||||
},
|
||||
"mp3_320": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 15,
|
||||
"min_kbps": 280,
|
||||
"max_kbps": 500,
|
||||
"priority": 1
|
||||
},
|
||||
"mp3_256": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 12,
|
||||
"min_kbps": 200,
|
||||
"max_kbps": 400,
|
||||
"priority": 2
|
||||
},
|
||||
"mp3_192": {
|
||||
"enabled": True,
|
||||
"min_mb": 0,
|
||||
"max_mb": 10,
|
||||
"min_kbps": 150,
|
||||
"max_kbps": 300,
|
||||
"priority": 3
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -392,13 +392,13 @@ def get_cached_transfer_data():
|
|||
# Cache Beatport scraping data to reduce load times and avoid hammering Beatport.com
|
||||
beatport_data_cache = {
|
||||
'homepage': {
|
||||
'hero_tracks': {'data': None, 'timestamp': 0, 'ttl': 3600}, # 1 hour
|
||||
'top_10_lists': {'data': None, 'timestamp': 0, 'ttl': 3600}, # 1 hour
|
||||
'top_10_releases': {'data': None, 'timestamp': 0, 'ttl': 3600}, # 1 hour
|
||||
'new_releases': {'data': None, 'timestamp': 0, 'ttl': 3600}, # 1 hour
|
||||
'hype_picks': {'data': None, 'timestamp': 0, 'ttl': 3600}, # 1 hour
|
||||
'featured_charts': {'data': None, 'timestamp': 0, 'ttl': 3600}, # 1 hour
|
||||
'dj_charts': {'data': None, 'timestamp': 0, 'ttl': 3600} # 1 hour
|
||||
'hero_tracks': {'data': None, 'timestamp': 0, 'ttl': 86400}, # 24 hours
|
||||
'top_10_lists': {'data': None, 'timestamp': 0, 'ttl': 86400}, # 24 hours
|
||||
'top_10_releases': {'data': None, 'timestamp': 0, 'ttl': 86400}, # 24 hours
|
||||
'new_releases': {'data': None, 'timestamp': 0, 'ttl': 86400}, # 24 hours
|
||||
'hype_picks': {'data': None, 'timestamp': 0, 'ttl': 86400}, # 24 hours
|
||||
'featured_charts': {'data': None, 'timestamp': 0, 'ttl': 86400}, # 24 hours
|
||||
'dj_charts': {'data': None, 'timestamp': 0, 'ttl': 86400} # 24 hours
|
||||
},
|
||||
'genre': {
|
||||
# Future expansion for genre-specific caching
|
||||
|
|
|
|||
|
|
@ -3001,20 +3001,20 @@
|
|||
</div>
|
||||
<div class="quality-tier-sliders" id="sliders-flac">
|
||||
<div class="slider-group">
|
||||
<label>File Size Range:</label>
|
||||
<label>Bitrate Range:</label>
|
||||
<div class="dual-slider-container">
|
||||
<input type="range" class="range-slider range-slider-min" id="flac-min"
|
||||
min="0" max="200" value="0" step="5"
|
||||
min="0" max="10000" value="500" step="100"
|
||||
oninput="updateQualityRange('flac')">
|
||||
<input type="range" class="range-slider range-slider-max" id="flac-max"
|
||||
min="0" max="200" value="150" step="5"
|
||||
min="0" max="10000" value="10000" step="100"
|
||||
oninput="updateQualityRange('flac')">
|
||||
<div class="range-slider-track"></div>
|
||||
</div>
|
||||
<div class="slider-values">
|
||||
<span id="flac-min-value">0 MB</span>
|
||||
<span id="flac-min-value">500 kbps</span>
|
||||
<span>-</span>
|
||||
<span id="flac-max-value">150 MB</span>
|
||||
<span id="flac-max-value">10000 kbps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3032,20 +3032,20 @@
|
|||
</div>
|
||||
<div class="quality-tier-sliders" id="sliders-mp3_320">
|
||||
<div class="slider-group">
|
||||
<label>File Size Range:</label>
|
||||
<label>Bitrate Range:</label>
|
||||
<div class="dual-slider-container">
|
||||
<input type="range" class="range-slider range-slider-min"
|
||||
id="mp3_320-min" min="0" max="50" value="0" step="1"
|
||||
id="mp3_320-min" min="0" max="500" value="280" step="10"
|
||||
oninput="updateQualityRange('mp3_320')">
|
||||
<input type="range" class="range-slider range-slider-max"
|
||||
id="mp3_320-max" min="0" max="50" value="20" step="1"
|
||||
id="mp3_320-max" min="0" max="500" value="500" step="10"
|
||||
oninput="updateQualityRange('mp3_320')">
|
||||
<div class="range-slider-track"></div>
|
||||
</div>
|
||||
<div class="slider-values">
|
||||
<span id="mp3_320-min-value">0 MB</span>
|
||||
<span id="mp3_320-min-value">280 kbps</span>
|
||||
<span>-</span>
|
||||
<span id="mp3_320-max-value">20 MB</span>
|
||||
<span id="mp3_320-max-value">500 kbps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3063,20 +3063,20 @@
|
|||
</div>
|
||||
<div class="quality-tier-sliders" id="sliders-mp3_256">
|
||||
<div class="slider-group">
|
||||
<label>File Size Range:</label>
|
||||
<label>Bitrate Range:</label>
|
||||
<div class="dual-slider-container">
|
||||
<input type="range" class="range-slider range-slider-min"
|
||||
id="mp3_256-min" min="0" max="40" value="0" step="1"
|
||||
id="mp3_256-min" min="0" max="400" value="200" step="10"
|
||||
oninput="updateQualityRange('mp3_256')">
|
||||
<input type="range" class="range-slider range-slider-max"
|
||||
id="mp3_256-max" min="0" max="40" value="15" step="1"
|
||||
id="mp3_256-max" min="0" max="400" value="400" step="10"
|
||||
oninput="updateQualityRange('mp3_256')">
|
||||
<div class="range-slider-track"></div>
|
||||
</div>
|
||||
<div class="slider-values">
|
||||
<span id="mp3_256-min-value">0 MB</span>
|
||||
<span id="mp3_256-min-value">200 kbps</span>
|
||||
<span>-</span>
|
||||
<span id="mp3_256-max-value">15 MB</span>
|
||||
<span id="mp3_256-max-value">400 kbps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3094,20 +3094,20 @@
|
|||
</div>
|
||||
<div class="quality-tier-sliders disabled" id="sliders-mp3_192">
|
||||
<div class="slider-group">
|
||||
<label>File Size Range:</label>
|
||||
<label>Bitrate Range:</label>
|
||||
<div class="dual-slider-container">
|
||||
<input type="range" class="range-slider range-slider-min"
|
||||
id="mp3_192-min" min="0" max="30" value="0" step="1"
|
||||
id="mp3_192-min" min="0" max="300" value="150" step="10"
|
||||
oninput="updateQualityRange('mp3_192')">
|
||||
<input type="range" class="range-slider range-slider-max"
|
||||
id="mp3_192-max" min="0" max="30" value="12" step="1"
|
||||
id="mp3_192-max" min="0" max="300" value="300" step="10"
|
||||
oninput="updateQualityRange('mp3_192')">
|
||||
<div class="range-slider-track"></div>
|
||||
</div>
|
||||
<div class="slider-values">
|
||||
<span id="mp3_192-min-value">0 MB</span>
|
||||
<span id="mp3_192-min-value">150 kbps</span>
|
||||
<span>-</span>
|
||||
<span id="mp3_192-max-value">12 MB</span>
|
||||
<span id="mp3_192-max-value">300 kbps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3124,9 +3124,9 @@
|
|||
<div class="help-text">
|
||||
<strong>How it works:</strong> Downloads try each enabled quality in priority order
|
||||
(1 = highest).
|
||||
File size constraints filter out outliers - MAX limits catch fake files (500MB
|
||||
"FLACs"), MIN limits (optional) can enforce minimum quality.
|
||||
Set MIN to 0 to accept all file sizes (recommended for most users).
|
||||
MIN bitrate catches fake/transcoded files (e.g., FLAC below 500 kbps is likely a
|
||||
re-encoded MP3). MAX bitrate limits hi-res files if you want to save space.
|
||||
When track duration is unavailable, a generous file-size safety net is used instead.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -568,27 +568,28 @@ async function loadPageData(pageId) {
|
|||
case 'library':
|
||||
// Check if we should return to artist detail view instead of list
|
||||
if (artistDetailPageState.currentArtistId && artistDetailPageState.currentArtistName) {
|
||||
console.log(`🔄 Returning to artist detail: ${artistDetailPageState.currentArtistName}`);
|
||||
navigateToPage('artist-detail');
|
||||
if (!artistDetailPageState.isInitialized) {
|
||||
initializeArtistDetailPage();
|
||||
loadArtistDetailData(artistDetailPageState.currentArtistId, artistDetailPageState.currentArtistName);
|
||||
}
|
||||
loadArtistDetailData(artistDetailPageState.currentArtistId, artistDetailPageState.currentArtistName);
|
||||
// Already initialized — DOM content persists, no reload needed
|
||||
} else {
|
||||
// Initialize and load library data
|
||||
if (!libraryPageState.isInitialized) {
|
||||
initializeLibraryPage();
|
||||
} else {
|
||||
// Refresh data when returning to page
|
||||
await loadLibraryArtists();
|
||||
}
|
||||
// Already initialized — DOM content persists, no reload needed
|
||||
}
|
||||
break;
|
||||
case 'artist-detail':
|
||||
// Artist detail page is handled separately by navigateToArtistDetail()
|
||||
break;
|
||||
case 'discover':
|
||||
await loadDiscoverPage();
|
||||
if (!discoverPageInitialized) {
|
||||
await loadDiscoverPage();
|
||||
discoverPageInitialized = true;
|
||||
}
|
||||
// Already initialized — DOM content persists, no reload needed
|
||||
break;
|
||||
case 'settings':
|
||||
initializeSettings();
|
||||
|
|
@ -1944,8 +1945,8 @@ function populateQualityProfileUI(profile) {
|
|||
const minSlider = document.getElementById(`${quality}-min`);
|
||||
const maxSlider = document.getElementById(`${quality}-max`);
|
||||
if (minSlider && maxSlider) {
|
||||
minSlider.value = config.min_mb;
|
||||
maxSlider.value = config.max_mb;
|
||||
minSlider.value = config.min_kbps;
|
||||
maxSlider.value = config.max_kbps;
|
||||
updateQualityRange(quality);
|
||||
}
|
||||
|
||||
|
|
@ -1997,8 +1998,8 @@ function updateQualityRange(quality) {
|
|||
maxSlider.value = max;
|
||||
}
|
||||
|
||||
minValue.textContent = `${min} MB`;
|
||||
maxValue.textContent = `${max} MB`;
|
||||
minValue.textContent = `${min} kbps`;
|
||||
maxValue.textContent = `${max} kbps`;
|
||||
}
|
||||
|
||||
function toggleQuality(quality) {
|
||||
|
|
@ -2049,7 +2050,7 @@ async function applyQualityPreset(presetName) {
|
|||
|
||||
function collectQualityProfileFromUI() {
|
||||
const profile = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
preset: 'custom', // Will be overridden if a preset is active
|
||||
qualities: {},
|
||||
fallback_enabled: document.getElementById('quality-fallback-enabled')?.checked ?? true
|
||||
|
|
@ -2065,8 +2066,8 @@ function collectQualityProfileFromUI() {
|
|||
|
||||
profile.qualities[quality] = {
|
||||
enabled: enabled,
|
||||
min_mb: parseInt(minSlider?.value || 0),
|
||||
max_mb: parseInt(maxSlider?.value || 999),
|
||||
min_kbps: parseInt(minSlider?.value || 0),
|
||||
max_kbps: parseInt(maxSlider?.value || 99999),
|
||||
priority: index + 1 // 1-4 based on order
|
||||
};
|
||||
});
|
||||
|
|
@ -30908,6 +30909,7 @@ async function selectJellyfinLibrary() {
|
|||
let discoverHeroIndex = 0;
|
||||
let discoverHeroArtists = [];
|
||||
let discoverHeroInterval = null;
|
||||
let discoverPageInitialized = false;
|
||||
|
||||
// Store discover playlist tracks for download/sync functionality
|
||||
let discoverReleaseRadarTracks = [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue