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 @@ -
- - -
- Controls the level of detail in application logs. DEBUG shows all details, INFO - shows general operations, WARNING and ERROR show only issues. -
-
@@ -5737,13 +5724,22 @@ - +
-

Logging Information

+

Logging

-
DEBUG
+ +
+ Controls the level of detail in application logs. DEBUG shows all details, INFO + shows general operations, WARNING and ERROR show only issues. +
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/).

    + + + + + + + + +
    FileWhat It ContainsWhen to Check
    app.logMain application log — API calls, downloads, library scans, enrichment, errorsFirst place to look for any issue
    post_processing.logFile processing pipeline — tagging, organization, conversion, file movesFiles not appearing in library, wrong tags, conversion failures
    acoustid.logAudio fingerprint verification resultsWrong tracks being downloaded, verification failures
    source_reuse.logSoulseek source reuse decisions for album consistencyAlbums downloading from mixed sources
    +

    Setting Log Level

    +

    Go to Settings → Advanced → Logging and change the log level:

    + +
    💡
    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:

    + +

    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

    + +

    Soulseek search returns no results

    + +

    Spotify shows "Rate Limited"

    + +

    Docker: paths not found / permission denied

    + +

    Wrong track downloaded

    + +
    +
    +

    Reporting Issues

    +

    When reporting a bug on GitHub Issues or in the Discord, include:

    +
      +
    1. Debug info snapshot — Click Copy Debug Info in the Help sidebar and paste the output
    2. +
    3. Steps to reproduce — What you did, what you expected, what happened instead
    4. +
    5. 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
    6. +
    +
    💡
    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