Add stream source setting, auto-update yt-dlp on container start
Stream source: - New setting in Settings → Downloads: "Stream / Preview Source" - Options: YouTube (instant, default) or Active Download Source - YouTube streams require no auth and are instant - If active source is Soulseek, automatically falls back to YouTube - Uses direct client search (bypasses orchestrator's download mode) - Config key: download_source.stream_source Docker: - entrypoint.sh now runs pip install -U yt-dlp on every container start, so Docker users always have the latest yt-dlp without rebuilding the image
This commit is contained in:
parent
6a41f5c0d7
commit
9fcbd323a5
5 changed files with 60 additions and 13 deletions
|
|
@ -380,6 +380,7 @@ class ConfigManager:
|
||||||
"hybrid_primary": "soulseek", # Legacy: primary source for hybrid mode
|
"hybrid_primary": "soulseek", # Legacy: primary source for hybrid mode
|
||||||
"hybrid_secondary": "youtube", # Legacy: fallback source for hybrid mode
|
"hybrid_secondary": "youtube", # Legacy: fallback source for hybrid mode
|
||||||
"hybrid_order": [], # Ordered list of sources for hybrid mode (overrides primary/secondary)
|
"hybrid_order": [], # Ordered list of sources for hybrid mode (overrides primary/secondary)
|
||||||
|
"stream_source": "youtube", # Options: "youtube" (instant, default), "active" (use download source; falls back to youtube if soulseek)
|
||||||
},
|
},
|
||||||
"tidal_download": {
|
"tidal_download": {
|
||||||
"quality": "lossless", # Options: "low", "high", "lossless", "hires"
|
"quality": "lossless", # Options: "low", "high", "lossless", "hires"
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ chown -R soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/T
|
||||||
|
|
||||||
echo "✅ Configuration initialized successfully"
|
echo "✅ Configuration initialized successfully"
|
||||||
|
|
||||||
|
# Auto-update yt-dlp — YouTube changes their API frequently and stale versions break downloads
|
||||||
|
echo "🔄 Updating yt-dlp..."
|
||||||
|
pip install -U yt-dlp --quiet --no-cache-dir 2>/dev/null && echo " ✅ yt-dlp updated" || echo " ⚠️ yt-dlp update failed (will use existing version)"
|
||||||
|
|
||||||
# Display final user info
|
# Display final user info
|
||||||
echo "👤 Running as:"
|
echo "👤 Running as:"
|
||||||
echo " User: $(id -u soulsync):$(id -g soulsync) ($(id -un soulsync):$(id -gn soulsync))"
|
echo " User: $(id -u soulsync):$(id -g soulsync) ($(id -un soulsync):$(id -gn soulsync))"
|
||||||
|
|
|
||||||
|
|
@ -7234,18 +7234,34 @@ def stream_enhanced_search_track():
|
||||||
'duration_ms': duration_ms
|
'duration_ms': duration_ms
|
||||||
})()
|
})()
|
||||||
|
|
||||||
# Generate search queries based on download source mode
|
# Determine effective stream source
|
||||||
# - Soulseek: Track name only (avoids keyword filtering on artist names)
|
# stream_source: "youtube" (default, instant) or "active" (use download source)
|
||||||
# - YouTube: Include artist name (provides context to find actual music)
|
stream_source = config_manager.get('download_source.stream_source', 'youtube')
|
||||||
download_mode = config_manager.get('download_source.mode', 'soulseek')
|
download_mode = config_manager.get('download_source.mode', 'soulseek')
|
||||||
|
|
||||||
|
# Resolve the effective mode for streaming
|
||||||
|
if stream_source == 'youtube':
|
||||||
|
effective_mode = 'youtube'
|
||||||
|
else:
|
||||||
|
# "active" mode — use download source, but fall back to YouTube if Soulseek
|
||||||
|
_hybrid_order = config_manager.get('download_source.hybrid_order', [])
|
||||||
|
_hybrid_first = _hybrid_order[0] if _hybrid_order else config_manager.get('download_source.hybrid_primary', 'soulseek')
|
||||||
|
if download_mode == 'soulseek' or (download_mode == 'hybrid' and _hybrid_first == 'soulseek'):
|
||||||
|
effective_mode = 'youtube' # Soulseek is too slow for streaming preview
|
||||||
|
logger.info("▶️ Stream source is 'active' but primary is Soulseek — falling back to YouTube")
|
||||||
|
elif download_mode == 'hybrid':
|
||||||
|
effective_mode = _hybrid_first
|
||||||
|
else:
|
||||||
|
effective_mode = download_mode
|
||||||
|
|
||||||
|
logger.info(f"▶️ Stream source: {stream_source} → effective: {effective_mode}")
|
||||||
|
|
||||||
|
# Generate search queries based on effective stream mode
|
||||||
search_queries = []
|
search_queries = []
|
||||||
import re
|
import re
|
||||||
|
|
||||||
_hybrid_order = config_manager.get('download_source.hybrid_order', [])
|
if effective_mode in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl'):
|
||||||
_hybrid_first = _hybrid_order[0] if _hybrid_order else config_manager.get('download_source.hybrid_primary', 'soulseek')
|
# Streaming sources: Include artist for better context
|
||||||
if download_mode in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl') or (download_mode == 'hybrid' and _hybrid_first in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl')):
|
|
||||||
# YouTube/Tidal mode: Include artist for better context
|
|
||||||
# Primary query: Artist + Track
|
|
||||||
if artist_name and track_name:
|
if artist_name and track_name:
|
||||||
search_queries.append(f"{artist_name} {track_name}".strip())
|
search_queries.append(f"{artist_name} {track_name}".strip())
|
||||||
|
|
||||||
|
|
@ -7255,14 +7271,12 @@ def stream_enhanced_search_track():
|
||||||
if cleaned_name and cleaned_name.lower() != track_name.lower():
|
if cleaned_name and cleaned_name.lower() != track_name.lower():
|
||||||
search_queries.append(f"{artist_name} {cleaned_name}".strip())
|
search_queries.append(f"{artist_name} {cleaned_name}".strip())
|
||||||
|
|
||||||
logger.info(f"🔍 {download_mode.title()} mode: Searching with artist + track name: {search_queries}")
|
logger.info(f"🔍 {effective_mode.title()} stream: Searching with artist + track name: {search_queries}")
|
||||||
else:
|
else:
|
||||||
# Soulseek mode: Track name only to avoid keyword filtering
|
# Soulseek mode: Track name only to avoid keyword filtering
|
||||||
# Primary query: Full track name
|
|
||||||
if track_name.strip():
|
if track_name.strip():
|
||||||
search_queries.append(track_name.strip())
|
search_queries.append(track_name.strip())
|
||||||
|
|
||||||
# Cleaned query: Remove parentheses and brackets
|
|
||||||
cleaned_name = re.sub(r'\s*\([^)]*\)', '', track_name).strip()
|
cleaned_name = re.sub(r'\s*\([^)]*\)', '', track_name).strip()
|
||||||
cleaned_name = re.sub(r'\s*\[[^\]]*\]', '', cleaned_name).strip()
|
cleaned_name = re.sub(r'\s*\[[^\]]*\]', '', cleaned_name).strip()
|
||||||
|
|
||||||
|
|
@ -7281,13 +7295,27 @@ def stream_enhanced_search_track():
|
||||||
|
|
||||||
search_queries = unique_queries
|
search_queries = unique_queries
|
||||||
|
|
||||||
|
# Select the search client based on effective stream mode
|
||||||
|
_stream_clients = {
|
||||||
|
'youtube': soulseek_client.youtube,
|
||||||
|
'tidal': soulseek_client.tidal,
|
||||||
|
'qobuz': soulseek_client.qobuz,
|
||||||
|
'hifi': soulseek_client.hifi,
|
||||||
|
'deezer_dl': soulseek_client.deezer_dl,
|
||||||
|
}
|
||||||
|
stream_client = _stream_clients.get(effective_mode)
|
||||||
|
use_direct_client = stream_client is not None
|
||||||
|
|
||||||
# Try queries sequentially until we find a good match
|
# Try queries sequentially until we find a good match
|
||||||
for query_index, query in enumerate(search_queries):
|
for query_index, query in enumerate(search_queries):
|
||||||
logger.info(f"🔍 Query {query_index + 1}/{len(search_queries)}: '{query}'")
|
logger.info(f"🔍 Query {query_index + 1}/{len(search_queries)}: '{query}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Search slskd with timeout
|
# Search using the stream source client (not the download source)
|
||||||
tracks_result, _ = run_async(soulseek_client.search(query, timeout=15))
|
if use_direct_client:
|
||||||
|
tracks_result, _ = run_async(stream_client.search(query, timeout=15))
|
||||||
|
else:
|
||||||
|
tracks_result, _ = run_async(soulseek_client.search(query, timeout=15))
|
||||||
|
|
||||||
if tracks_result:
|
if tracks_result:
|
||||||
logger.info(f"✅ Found {len(tracks_result)} results for query: '{query}'")
|
logger.info(f"✅ Found {len(tracks_result)} results for query: '{query}'")
|
||||||
|
|
|
||||||
|
|
@ -3991,6 +3991,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Stream / Preview Source:</label>
|
||||||
|
<select id="stream-source" class="form-select">
|
||||||
|
<option value="youtube">YouTube (Instant)</option>
|
||||||
|
<option value="active">Active Download Source</option>
|
||||||
|
</select>
|
||||||
|
<div class="setting-help-text">
|
||||||
|
Where to stream audio when previewing tracks. YouTube is instant and requires no setup.
|
||||||
|
"Active Download Source" uses your configured source (Tidal, Qobuz, etc.) — if that's Soulseek, YouTube is used automatically.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hybrid Mode Settings (shown only when hybrid is selected) -->
|
<!-- Hybrid Mode Settings (shown only when hybrid is selected) -->
|
||||||
<div id="hybrid-settings-container" style="display: none;">
|
<div id="hybrid-settings-container" style="display: none;">
|
||||||
<div class="setting-help-text">
|
<div class="setting-help-text">
|
||||||
|
|
|
||||||
|
|
@ -5756,6 +5756,7 @@ async function loadSettingsData() {
|
||||||
|
|
||||||
// Populate Download Source settings
|
// Populate Download Source settings
|
||||||
document.getElementById('download-source-mode').value = settings.download_source?.mode || 'soulseek';
|
document.getElementById('download-source-mode').value = settings.download_source?.mode || 'soulseek';
|
||||||
|
document.getElementById('stream-source').value = settings.download_source?.stream_source || 'youtube';
|
||||||
loadHybridSourceOrder(settings);
|
loadHybridSourceOrder(settings);
|
||||||
document.getElementById('tidal-download-quality').value = settings.tidal_download?.quality || 'lossless';
|
document.getElementById('tidal-download-quality').value = settings.tidal_download?.quality || 'lossless';
|
||||||
document.getElementById('tidal-allow-fallback').checked = settings.tidal_download?.allow_fallback !== false;
|
document.getElementById('tidal-allow-fallback').checked = settings.tidal_download?.allow_fallback !== false;
|
||||||
|
|
@ -6784,6 +6785,7 @@ async function saveSettings(quiet = false) {
|
||||||
hybrid_primary: document.getElementById('hybrid-primary-source').value,
|
hybrid_primary: document.getElementById('hybrid-primary-source').value,
|
||||||
hybrid_secondary: document.getElementById('hybrid-secondary-source').value,
|
hybrid_secondary: document.getElementById('hybrid-secondary-source').value,
|
||||||
hybrid_order: getHybridOrder(),
|
hybrid_order: getHybridOrder(),
|
||||||
|
stream_source: document.getElementById('stream-source').value,
|
||||||
},
|
},
|
||||||
tidal_download: {
|
tidal_download: {
|
||||||
quality: document.getElementById('tidal-download-quality').value || 'lossless',
|
quality: document.getElementById('tidal-download-quality').value || 'lossless',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue