diff --git a/web_server.py b/web_server.py
index 13adc258..2135ee10 100644
--- a/web_server.py
+++ b/web_server.py
@@ -5516,6 +5516,19 @@ def get_debug_info():
info['os'] = f"{platform.system()} {platform.release()}"
info['python'] = sys.version.split()[0]
info['docker'] = os.path.exists('/.dockerenv')
+ info['runner'] = 'gunicorn' if not _DIRECT_RUN else 'direct (python web_server.py)'
+
+ # ffmpeg version
+ try:
+ import subprocess
+ result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
+ first_line = result.stdout.split('\n')[0] if result.stdout else ''
+ # e.g. "ffmpeg version 6.1.1 Copyright ..."
+ info['ffmpeg'] = first_line.split('Copyright')[0].replace('ffmpeg version', '').strip() if first_line else 'installed (version unknown)'
+ except FileNotFoundError:
+ info['ffmpeg'] = 'NOT INSTALLED'
+ except Exception:
+ info['ffmpeg'] = 'unknown'
# Uptime
start_time = getattr(app, 'start_time', time.time())
@@ -5525,7 +5538,7 @@ def get_debug_info():
# Paths
download_path = config_manager.get('soulseek.download_path', './downloads')
transfer_folder = config_manager.get('soulseek.transfer_path', './Transfer')
- staging_folder = config_manager.get('import.staging_folder', '')
+ staging_folder = config_manager.get('import.staging_path', '')
info['paths'] = {
'download_path': download_path,
'download_path_exists': os.path.isdir(download_path) if download_path else False,
@@ -5536,6 +5549,21 @@ def get_debug_info():
'staging_folder': staging_folder,
'staging_folder_exists': os.path.isdir(staging_folder) if staging_folder else False,
}
+ # Music library paths (Settings > Library)
+ music_paths = config_manager.get('library.music_paths', [])
+ if isinstance(music_paths, list) and music_paths:
+ info['paths']['music_library_paths'] = []
+ for p in music_paths:
+ if p and isinstance(p, str):
+ info['paths']['music_library_paths'].append({
+ 'path': p,
+ 'exists': os.path.isdir(p),
+ })
+ # Music videos directory
+ music_videos_path = config_manager.get('library.music_videos_path', '')
+ if music_videos_path:
+ info['paths']['music_videos_path'] = music_videos_path
+ info['paths']['music_videos_path_exists'] = os.path.isdir(music_videos_path)
# Services from status cache
spotify_cache = _status_cache.get('spotify', {})
@@ -5555,16 +5583,16 @@ def get_debug_info():
# Enrichment workers
workers = {}
- worker_names = ['musicbrainz', 'audiodb', 'deezer', 'spotify', 'itunes', 'lastfm', 'genius', 'tidal', 'qobuz']
+ worker_names = ['musicbrainz', 'audiodb', 'deezer', 'spotify', 'itunes', 'lastfm', 'genius', 'discogs', 'tidal', 'qobuz']
for name in worker_names:
paused_key = f'{name}_enrichment_paused'
workers[name] = 'paused' if config_manager.get(paused_key, False) else 'active'
info['enrichment_workers'] = workers
- # Library stats
+ # Library stats — use same method as dashboard (filters by active server)
try:
- db = get_db()
- lib_stats = db.get_statistics()
+ db = get_database()
+ lib_stats = db.get_database_info_for_server()
info['library'] = {
'artists': lib_stats.get('artists', 0),
'albums': lib_stats.get('albums', 0),
@@ -5580,6 +5608,13 @@ def get_debug_info():
except Exception:
info['watchlist_count'] = 0
+ # Wishlist pending count
+ try:
+ db = get_db()
+ info['wishlist_count'] = db.get_wishlist_count()
+ except Exception:
+ info['wishlist_count'] = 0
+
# Automation count
try:
db = get_db()
@@ -5613,15 +5648,29 @@ def get_debug_info():
info['active_syncs'] = active_syncs
# Config settings relevant to troubleshooting
+ source_mode = config_manager.get('download_source.mode', 'hybrid')
info['config'] = {
- 'source_mode': config_manager.get('download_source.mode', 'hybrid'),
+ 'source_mode': source_mode,
'quality_profile': config_manager.get('download_source.quality_profile', 'default'),
'organization_template': config_manager.get('organization.folder_template', ''),
'post_processing_enabled': config_manager.get('post_processing.enabled', True),
'acoustid_enabled': bool(config_manager.get('acoustid.api_key', '')),
'auto_scan_enabled': config_manager.get('watchlist.auto_scan', False),
'm3u_export_enabled': config_manager.get('m3u.enabled', False),
+ 'log_level': config_manager.get('logging.level', 'INFO'),
+ 'primary_metadata_source': config_manager.get('metadata.fallback_source', 'deezer'),
+ 'lossy_copy_enabled': config_manager.get('post_processing.lossy_copy.enabled', False),
+ 'lossy_copy_format': config_manager.get('post_processing.lossy_copy.format', 'mp3'),
+ 'lossy_copy_bitrate': config_manager.get('post_processing.lossy_copy.bitrate', 320),
+ 'allow_duplicate_tracks': config_manager.get('library.allow_duplicate_tracks', False),
+ 'replace_lower_quality': config_manager.get('import.replace_lower_quality', False),
+ 'auto_import_enabled': config_manager.get('import.auto_import_enabled', False),
}
+ # Hybrid source priority order
+ if source_mode == 'hybrid':
+ info['config']['hybrid_sources'] = config_manager.get('download_source.hybrid_order', [])
+ # Discogs connection status
+ info['services']['discogs_connected'] = bool(config_manager.get('discogs.token', ''))
# Download client init failures
info['download_client_failures'] = []
diff --git a/webui/index.html b/webui/index.html
index abfb7bb6..b065c029 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -4865,19 +4865,6 @@
-
@@ -5737,13 +5724,22 @@
-
+
-
Logging Information
+
Logging
diff --git a/webui/static/docs.js b/webui/static/docs.js
index a1ba1ded..1c335e73 100644
--- a/webui/static/docs.js
+++ b/webui/static/docs.js
@@ -1458,7 +1458,7 @@ const DOCS_SECTIONS = [
UI Appearance — Custom accent colors with persistent preference. Changes apply immediately across the entire interface. Choose from different sidebar visualizer types for the media player audio visualization.
API Keys — Generate and manage API keys for the REST API. Keys use a sk_ prefix and are shown once at creation — only a SHA-256 hash is stored for security.
Path Templates — Configure how files are organized in your library. The default template is Artist/Album/TrackNum - Title.ext
- Log Level — Set the application log verbosity (DEBUG, INFO, WARNING, ERROR) from the Settings page. Changes take effect immediately without restart. Useful for troubleshooting issues.
+ Log Level — Set log verbosity (DEBUG, INFO, WARNING, ERROR) in Settings → Advanced → Logging. Changes take effect immediately. See Troubleshooting → Understanding Logs for details.
WebSocket — Real-time status updates are delivered via WebSocket. All downloads, enrichment progress, scan status, and system events push to the UI without polling.
Music Library Paths — In Settings > Library, add folder paths where your music files live. Required for tag writing, streaming, and file detection when your media server stores files at a different path than SoulSync can see. Docker users: mount your music folder(s) with read-write access, then add the container-side path.
Replace Lower Quality on Import — Opt-in toggle in Settings > Library. When importing from Staging, if a track already exists at lower quality (e.g. MP3), it gets replaced with the higher quality version (e.g. FLAC). Disabled by default.
@@ -1541,6 +1541,100 @@ const DOCS_SECTIONS = [
`
},
+ {
+ id: 'troubleshooting',
+ title: 'Troubleshooting',
+ icon: '/static/settings.jpg',
+ children: [
+ { id: 'ts-logs', title: 'Understanding Logs' },
+ { id: 'ts-debug', title: 'Copy Debug Info' },
+ { id: 'ts-common', title: 'Common Issues' },
+ { id: 'ts-reporting', title: 'Reporting Issues' }
+ ],
+ content: () => `
+
+
Understanding Logs
+
SoulSync writes several log files that are critical for diagnosing issues. All logs are in the logs/ directory (Docker: /app/logs/).
+
+ | File | What It Contains | When to Check |
+
+ app.log | Main application log — API calls, downloads, library scans, enrichment, errors | First place to look for any issue |
+ post_processing.log | File processing pipeline — tagging, organization, conversion, file moves | Files not appearing in library, wrong tags, conversion failures |
+ acoustid.log | Audio fingerprint verification results | Wrong tracks being downloaded, verification failures |
+ source_reuse.log | Soulseek source reuse decisions for album consistency | Albums downloading from mixed sources |
+
+
+
Setting Log Level
+
Go to Settings → Advanced → Logging and change the log level:
+
+ - DEBUG — Maximum detail. Use this when troubleshooting — shows every decision the app makes
+ - INFO — Normal operations (default). Good for day-to-day use
+ - WARNING — Only problems and unusual situations
+ - ERROR — Only failures. Minimal output
+
+
💡Reproducing a bug? Set log level to DEBUG, reproduce the issue, then grab the logs. The extra detail makes it much easier to identify the problem.
+
+
+
Copy Debug Info
+
The Copy Debug Info button in the sidebar collects a complete snapshot of your SoulSync instance in one click:
+
+ - System info (version, OS, Python, uptime, memory, CPU)
+ - Service connection status (Spotify, Soulseek, Tidal, Qobuz, Discogs, media server)
+ - Library stats and database size
+ - All configured paths with accessibility checks
+ - Config settings (download source, quality profile, post-processing, etc.)
+ - Enrichment worker status
+ - API rate usage and any active rate limits
+ - Recent log lines from the selected log file
+
+
Use the dropdowns to choose how many log lines to include (20–500) and which log file to pull from.
+
⚠️Debug info does not include API keys, tokens, or passwords — it is safe to share publicly.
+
+
+
Common Issues
+
Downloads complete but tracks don't appear in library
+
+ - Check that your Transfer path is correct and writable (see Paths in debug info)
+ - If using a media server, trigger a library scan after downloads complete
+ - Check
post_processing.log for file move errors
+
+
Soulseek search returns no results
+
+ - Verify slskd is running and the API key is correct in Settings
+ - Check that slskd shows as connected on the Dashboard
+ - Try searching directly in the slskd web UI to rule out network issues
+
+
Spotify shows "Rate Limited"
+
+ - This is temporary — Spotify throttles API calls when limits are hit
+ - SoulSync automatically falls back to other metadata sources during a ban
+ - The rate limit modal shows a countdown. Enrichment workers auto-pause and resume when the ban lifts
+
+
Docker: paths not found / permission denied
+
+ - Paths in Settings must be container paths (e.g.
/app/downloads), not host paths
+ - Ensure your Docker volume mounts match the container paths
+ - Set PUID/PGID to match your host user (Unraid default: 99/100)
+
+
Wrong track downloaded
+
+ - Enable AcoustID verification in Settings to catch mismatches
+ - Check
acoustid.log for fingerprint comparison details
+ - Use the track redownload feature to try a different source
+
+
+
+
Reporting Issues
+
When reporting a bug on GitHub Issues or in the Discord, include:
+
+ - Debug info snapshot — Click Copy Debug Info in the Help sidebar and paste the output
+ - Steps to reproduce — What you did, what you expected, what happened instead
+ - Relevant log lines — Set log level to DEBUG, reproduce the issue, and include the relevant log section. The debug info includes recent lines, but for longer issues you may need to pull from the full log file
+
+
💡The more context you provide, the faster the fix. A debug info snapshot + steps to reproduce + DEBUG log excerpt is the ideal bug report. Even if you think you know the cause, the logs often reveal something unexpected.
+
+ `
+ },
{
id: 'api',
title: 'REST API',
@@ -2376,6 +2470,8 @@ function initializeDocsPage() {
text += `Version: ${data.version}\n`;
text += `OS: ${data.os}${data.docker ? ' (Docker)' : ''}\n`;
text += `Python: ${data.python}\n`;
+ text += `ffmpeg: ${data.ffmpeg || 'unknown'}\n`;
+ text += `Runner: ${data.runner || 'unknown'}\n`;
text += `Uptime: ${data.uptime || 'unknown'}\n`;
text += `Memory: ${data.memory_usage || '?'} (system: ${data.system_memory || '?'})\n`;
text += `CPU: ${data.cpu_percent || '?'}\n`;
@@ -2388,6 +2484,7 @@ function initializeDocsPage() {
text += `Soulseek: ${data.services?.soulseek_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
text += `Tidal: ${data.services?.tidal_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
text += `Qobuz: ${data.services?.qobuz_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
+ text += `Discogs: ${data.services?.discogs_connected ? ck + ' Connected' : ex + ' No Token'}\n`;
text += `Download Mode: ${data.services?.download_source || 'unknown'}\n\n`;
text += '── Library ──\n';
@@ -2396,6 +2493,7 @@ function initializeDocsPage() {
text += `Tracks: ${data.library?.tracks?.toLocaleString() || '0'}\n`;
text += `Database: ${data.database_size || 'unknown'}\n`;
text += `Watchlist: ${data.watchlist_count || 0} artists\n`;
+ text += `Wishlist: ${data.wishlist_count || 0} pending\n`;
text += `Automations: ${data.automations?.enabled || 0} enabled / ${data.automations?.total || 0} total\n\n`;
text += '── Active ──\n';
@@ -2406,16 +2504,37 @@ function initializeDocsPage() {
const pathStatus = (exists, writable) => exists ? (writable ? ck + ' ok' : ck + ' exists ' + ex + ' not writable') : ex + ' missing';
text += `Download: ${data.paths?.download_path || '(not set)'} [${pathStatus(data.paths?.download_path_exists, data.paths?.download_path_writable)}]\n`;
text += `Transfer: ${data.paths?.transfer_folder || '(not set)'} [${pathStatus(data.paths?.transfer_folder_exists, data.paths?.transfer_folder_writable)}]\n`;
- text += `Staging: ${data.paths?.staging_folder || '(not set)'} [${data.paths?.staging_folder_exists ? ck + ' ok' : ex + ' missing'}]\n\n`;
+ text += `Staging: ${data.paths?.staging_folder ? data.paths.staging_folder + ' [' + (data.paths.staging_folder_exists ? ck + ' ok' : ex + ' missing') + ']' : '(not configured — optional)'}\n`;
+ if (data.paths?.music_videos_path) {
+ text += `Videos: ${data.paths.music_videos_path} [${data.paths.music_videos_path_exists ? ck + ' ok' : ex + ' missing'}]\n`;
+ }
+ if (data.paths?.music_library_paths?.length) {
+ text += `Library Paths:\n`;
+ data.paths.music_library_paths.forEach(p => {
+ text += ` ${p.path} [${p.exists ? ck + ' ok' : ex + ' missing'}]\n`;
+ });
+ }
+ text += '\n';
text += '── Config ──\n';
if (data.config) {
+ text += `Log Level: ${data.config.log_level || 'INFO'}\n`;
text += `Source Mode: ${data.config.source_mode || 'unknown'}\n`;
+ if (data.config.source_mode === 'hybrid' && data.config.hybrid_sources?.length) {
+ text += `Hybrid Priority: ${data.config.hybrid_sources.join(' → ')}\n`;
+ }
+ text += `Metadata Source: ${data.config.primary_metadata_source || 'deezer'}\n`;
text += `Quality Profile: ${data.config.quality_profile || 'default'}\n`;
text += `Folder Template: ${data.config.organization_template || '(default)'}\n`;
text += `Post-Processing: ${data.config.post_processing_enabled ? 'enabled' : 'disabled'}\n`;
+ if (data.config.lossy_copy_enabled) {
+ text += `Lossy Copy: ${data.config.lossy_copy_format?.toUpperCase()} @ ${data.config.lossy_copy_bitrate}kbps\n`;
+ }
text += `AcoustID: ${data.config.acoustid_enabled ? 'enabled' : 'disabled'}\n`;
text += `Auto Scan: ${data.config.auto_scan_enabled ? 'enabled' : 'disabled'}\n`;
+ text += `Auto Import: ${data.config.auto_import_enabled ? 'enabled' : 'disabled'}\n`;
+ text += `Duplicate Tracks: ${data.config.allow_duplicate_tracks ? 'allowed' : 'rejected'}\n`;
+ text += `Replace Quality: ${data.config.replace_lower_quality ? 'enabled' : 'disabled'}\n`;
text += `M3U Export: ${data.config.m3u_export_enabled ? 'enabled' : 'disabled'}\n`;
}
text += '\n';
@@ -2460,12 +2579,21 @@ function initializeDocsPage() {
}
text += '\n';
+ if (data.available_logs?.length) {
+ text += '── Log Files ──\n';
+ data.available_logs.forEach(log => {
+ text += ` ${log.file.padEnd(24)} ${log.size}\n`;
+ });
+ text += '\n';
+ }
+
text += `── Logs: ${data.log_source || 'app'}.log (last ${data.recent_logs?.length || 0} lines) ──\n`;
if (data.recent_logs?.length) {
data.recent_logs.forEach(line => { text += line + '\n'; });
} else {
text += '(no log lines)\n';
}
+ text += '\n---\nPaste this output into your GitHub issue at https://github.com/Nezreka/SoulSync/issues\n';
// Copy to clipboard — navigator.clipboard requires HTTPS/localhost,
// so fall back to execCommand for Docker/LAN HTTP access
diff --git a/webui/static/script.js b/webui/static/script.js
index 974bbfce..a7edcd20 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -6235,8 +6235,9 @@ async function loadSettingsData() {
if (reduceCheckbox) reduceCheckbox.checked = reduceEffects;
applyReduceEffects(reduceEffects);
- // Populate Logging information (read-only)
- document.getElementById('log-level-display').textContent = settings.logging?.level || 'INFO';
+ // Populate Logging information
+ const logLevelSelect = document.getElementById('log-level-select');
+ if (logLevelSelect) logLevelSelect.value = settings.logging?.level || 'INFO';
document.getElementById('log-path-display').textContent = settings.logging?.path || 'logs/app.log';
// Load Discovery Lookback Period setting