diff --git a/core/tag_writer.py b/core/tag_writer.py index 807007ac..760ce79e 100644 --- a/core/tag_writer.py +++ b/core/tag_writer.py @@ -16,7 +16,7 @@ from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm from mutagen.oggvorbis import OggVorbis from mutagen.apev2 import APEv2, APENoHeaderError -logger = logging.getLogger("tag_writer") +logger = logging.getLogger("newmusic.tag_writer") # Supported extensions SUPPORTED_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.oga', '.opus', '.m4a', '.mp4'} diff --git a/database/music_database.py b/database/music_database.py index 8011f40a..8fa40672 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -4206,15 +4206,20 @@ class MusicDatabase: # Use the best title similarity (direct or cleaned) best_title_similarity = max(title_similarity, clean_title_similarity) - + + # Require minimum title similarity to prevent a perfect artist match from + # carrying a bad title match over the threshold (e.g. "Time" vs "Time Flies") + if best_title_similarity < 0.6: + return best_title_similarity * 0.5 # Can never exceed 0.3, well below any threshold + # Weight: 50% title, 50% artist (equal weight to prevent false positives) # Also require minimum artist similarity to prevent matching wrong artists confidence = (best_title_similarity * 0.5) + (artist_similarity * 0.5) - + # Apply artist similarity penalty: if artist match is too low, drastically reduce confidence if artist_similarity < 0.6: # Less than 60% artist match confidence *= 0.3 # Reduce confidence by 70% - + return confidence except Exception as e: diff --git a/web_server.py b/web_server.py index c98307ee..80bb3026 100644 --- a/web_server.py +++ b/web_server.py @@ -3838,6 +3838,86 @@ def get_system_stats(): except Exception as e: return jsonify({'error': str(e)}), 500 + +@app.route('/api/debug-info') +def get_debug_info(): + """Collect system diagnostics for troubleshooting support requests.""" + import platform + import sys + import psutil + + info = {} + + # App info + info['version'] = SOULSYNC_VERSION + info['os'] = f"{platform.system()} {platform.release()}" + info['python'] = sys.version.split()[0] + info['docker'] = os.path.exists('/.dockerenv') + + # Paths + download_path = config_manager.get('download_path', '') + transfer_folder = config_manager.get('transfer_folder', '') + staging_folder = config_manager.get('staging_folder', '') + info['paths'] = { + 'download_path': download_path, + 'download_path_exists': os.path.isdir(download_path) if download_path else False, + 'download_path_writable': os.access(download_path, os.W_OK) if download_path and os.path.isdir(download_path) else False, + 'transfer_folder': transfer_folder, + 'transfer_folder_exists': os.path.isdir(transfer_folder) if transfer_folder else False, + 'transfer_folder_writable': os.access(transfer_folder, os.W_OK) if transfer_folder and os.path.isdir(transfer_folder) else False, + 'staging_folder': staging_folder, + 'staging_folder_exists': os.path.isdir(staging_folder) if staging_folder else False, + } + + # Services from status cache + spotify_cache = _status_cache.get('spotify', {}) + media_server_cache = _status_cache.get('media_server', {}) + soulseek_cache = _status_cache.get('soulseek', {}) + info['services'] = { + 'music_source': spotify_cache.get('source', 'unknown'), + 'spotify_connected': spotify_cache.get('connected', False), + 'spotify_rate_limited': spotify_cache.get('rate_limited', False), + 'media_server_type': media_server_cache.get('type', 'none'), + 'media_server_connected': media_server_cache.get('connected', False), + 'soulseek_connected': soulseek_cache.get('connected', False), + 'download_source': config_manager.get('download_source.mode', 'soulseek'), + } + + # Enrichment workers + workers = {} + worker_names = ['musicbrainz', 'audiodb', 'deezer', 'spotify', 'itunes', 'lastfm', 'genius', '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 + + # Database size + db_path = os.path.join('database', 'music_library.db') + if os.path.exists(db_path): + db_size_mb = os.path.getsize(db_path) / (1024 * 1024) + info['database_size'] = f"{db_size_mb:.1f} MB" + else: + info['database_size'] = 'not found' + + # Memory + process = psutil.Process(os.getpid()) + mem = process.memory_info() + info['memory_usage'] = f"{mem.rss / (1024 * 1024):.0f} MB" + + # Recent log lines + log_path = os.path.join('logs', 'app.log') + info['recent_logs'] = [] + if os.path.exists(log_path): + try: + with open(log_path, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + info['recent_logs'] = [line.rstrip() for line in lines[-20:]] + except Exception: + info['recent_logs'] = ['(could not read log file)'] + + return jsonify(info) + + # Global activity tracking storage activity_feed = [] activity_feed_lock = threading.Lock() @@ -9601,15 +9681,21 @@ def _resolve_library_file_path(file_path): if os.path.exists(file_path): return file_path - # Try resolving server-side paths to local transfer path + # Try resolving server-side paths to local directories + # Check both transfer path (final music location) and download path (intermediate) transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer')) + download_dir = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) path_parts = file_path.replace('\\', '/').split('/') - # Try progressively shorter path suffixes (skip index 0 to avoid drive letter issues) - for i in range(1, len(path_parts)): - candidate = os.path.join(transfer_dir, *path_parts[i:]) - if os.path.exists(candidate): - return candidate + # Try progressively shorter path suffixes against each candidate directory + # (skip index 0 to avoid drive letter issues) + for base_dir in [transfer_dir, download_dir]: + if not base_dir or not os.path.isdir(base_dir): + continue + for i in range(1, len(path_parts)): + candidate = os.path.join(base_dir, *path_parts[i:]) + if os.path.exists(candidate): + return candidate return None diff --git a/webui/static/docs.js b/webui/static/docs.js index 7fdc7ec5..815db5ff 100644 --- a/webui/static/docs.js +++ b/webui/static/docs.js @@ -2,6 +2,10 @@ // HELP & DOCS PAGE // =============================== +function docsImg(src, alt) { + return `${alt}`; +} + const DOCS_SECTIONS = [ { id: 'getting-started', @@ -19,6 +23,7 @@ const DOCS_SECTIONS = [

Overview

SoulSync is a self-hosted music download, sync, and library management platform. It connects to Spotify, Apple Music/iTunes, Tidal, Qobuz, YouTube, and Beatport for metadata, and uses Soulseek (via slskd) as the primary download source. Your library is served through Plex, Jellyfin, or Navidrome.

+ ${docsImg('gs-overview.png', 'SoulSync dashboard overview')}

🎵 Download Music

Search and download tracks in FLAC, MP3, and more from Soulseek, YouTube, Tidal, or Qobuz, with automatic metadata tagging and file organization.

🔄 Playlist Sync

Mirror playlists from Spotify, YouTube, Tidal, and Beatport. Discover official metadata and sync to your media server.

@@ -37,6 +42,7 @@ const DOCS_SECTIONS = [
  • Spotify (Recommended) — Connect Spotify for the richest metadata. Create an app at developer.spotify.com, enter your Client ID and Secret, then click Authenticate.
  • Download Path — Set your download and transfer paths in the Download Settings section. The transfer path should point to your media server's monitored folder.
  • + ${docsImg('gs-first-setup.png', 'Settings page first-time setup')}
    💡
    You can start using SoulSync with just one download source (Soulseek, YouTube, Tidal, or Qobuz). Spotify and other services add metadata enrichment but aren't strictly required — iTunes/Apple Music is always available as a free fallback.
    @@ -60,6 +66,7 @@ const DOCS_SECTIONS = [ ListenBrainzListening history and recommendationsURL + Token + ${docsImg('gs-connecting.png', 'Service credentials connected')}

    Understanding the Interface

    @@ -75,6 +82,7 @@ const DOCS_SECTIONS = [
  • Import — Import music files from a staging folder with album/track matching
  • Settings — Configure services, download preferences, quality profiles, and more
  • + ${docsImg('gs-interface.png', 'SoulSync interface layout')}

    Version & Updates: Click the version number in the sidebar footer to open the What's New modal, which shows detailed release notes for every feature and fix. SoulSync automatically checks for updates by comparing your running version against the latest GitHub commit. If an update is available, a banner appears in the modal. Docker users are notified when a new image has been pushed to the repo.

    @@ -97,6 +105,7 @@ const DOCS_SECTIONS = [ Staging Path/app/StagingFor the Import feature only. Drop audio files here to import them into your library via the Import page. + ${docsImg('gs-folders.png', 'Download settings folder configuration')}

    How Files Flow

    ℹ️
    @@ -206,6 +215,7 @@ const DOCS_SECTIONS = [ # Download Path: /app/downloads
    # Transfer Path: /app/Transfer
    + ${docsImg('gs-docker.png', 'Docker compose configuration')}

    Setup Checklist

    Go through every item. If you miss any single one, the pipeline will break:

    @@ -283,6 +293,140 @@ const DOCS_SECTIONS = [
    ` }, + { + id: 'workflows', + title: 'Quick Start Workflows', + icon: '/static/help.png', + children: [ + { id: 'wf-first', title: 'What Should I Do First?' }, + { id: 'wf-download', title: 'How to: Download an Album' }, + { id: 'wf-sync', title: 'How to: Sync a Spotify Playlist' }, + { id: 'wf-auto', title: 'How to: Set Up Auto-Downloads' }, + { id: 'wf-import', title: 'How to: Import Existing Music' }, + { id: 'wf-media', title: 'How to: Connect Your Media Server' } + ], + content: () => ` +
    +

    What Should I Do First?

    +

    SoulSync can do a lot, but you don't need to learn everything at once. Here are the 6 essential workflows that cover 90% of what most users need. Start with whichever one matches your goal, and explore the rest later.

    +
    +
    +
    🎵
    +
    Download an Album
    + 5 steps +

    Search for any album, pick your tracks, and download in FLAC or MP3 with full metadata.

    + View Guide → +
    +
    +
    🔄
    +
    Sync a Spotify Playlist
    + 4 steps +

    Import your Spotify playlists and download every track to your local library.

    + View Guide → +
    +
    +
    🤖
    +
    Set Up Auto-Downloads
    + 4 steps +

    Follow your favorite artists and automatically download their new releases.

    + View Guide → +
    +
    +
    📥
    +
    Import Existing Music
    + 5 steps +

    Bring your existing music files into SoulSync with proper tags and organization.

    + View Guide → +
    +
    +
    📺
    +
    Connect Your Media Server
    + 3 steps +

    Link Plex, Jellyfin, or Navidrome so downloads appear in your library automatically.

    + View Guide → +
    +
    +
    🏁
    +
    First Things After Setup
    + 5 steps +

    Once connected, do these 5 things to get the most out of SoulSync right away.

    + See below ↓ +
    +
    +

    First Things After Setup

    +
      +
    1. Download one album — Verify your folder paths and post-processing work end-to-end
    2. +
    3. Run a Database Update — Dashboard → Database Updater → Full Refresh to import your existing media server library
    4. +
    5. Add 5–10 artists to your Watchlist — This seeds the discovery pool for recommendations
    6. +
    7. Check the Automations page — Enable the system automations you want (auto-process wishlist, auto-scan watchlist, auto-backup)
    8. +
    9. Explore the Discover page — Once your watchlist has artists, recommendations and playlists appear here
    10. +
    +
    +
    +

    How to: Download an Album

    +

    Goal: Find an album and download it to your library with full metadata, cover art, and proper file organization.

    +

    Prerequisites: At least one download source connected (Soulseek, YouTube, Tidal, or Qobuz). Download and Transfer paths configured.

    +
      +
    1. Open Search — Click the Search page in the sidebar (make sure Enhanced Search is active)
    2. +
    3. Type the album name — Results appear in a categorized dropdown: Artists, Albums, Singles & EPs, Tracks
    4. +
    5. Click the album result — The download modal opens showing cover art, tracklist, and album details
    6. +
    7. Select tracks — All tracks are selected by default. Uncheck any you don't want
    8. +
    9. Click Download — SoulSync searches for each track, downloads, tags, and organizes the files automatically
    10. +
    + ${docsImg('wf-download-album.gif', 'Downloading an album')} +

    Result: Tracks appear in your Transfer folder as Artist/Album/01 - Title.flac and your media server is notified to scan.

    +
    💡
    If a track fails to download, click the retry icon or use the candidate selector to pick an alternative source file from a different user.
    +
    +
    +

    How to: Sync a Spotify Playlist

    +

    Goal: Import a Spotify playlist and download all its tracks to your local library.

    +
      +
    1. Go to the Sync page — Click Sync in the sidebar
    2. +
    3. Click Refresh — Your Spotify playlists load automatically (or paste a playlist URL directly)
    4. +
    5. Click Sync on a playlist — This adds all missing tracks to your wishlist
    6. +
    7. Wait for auto-processing — The wishlist processor runs every 30 minutes and downloads queued tracks. Or click "Process Wishlist" in Automations to start immediately
    8. +
    + ${docsImg('wf-sync-playlist.gif', 'Syncing a Spotify playlist')} +
    💡
    Use the "Download Missing" button on any playlist to see exactly which tracks are missing and download them all at once.
    +
    +
    +

    How to: Set Up Auto-Downloads

    +

    Goal: Automatically download new releases from your favorite artists without manual intervention.

    +
      +
    1. Add artists to your Watchlist — Search for artists on the Artists page and click the Watch button on each one
    2. +
    3. Go to Automations — The built-in "Auto-Scan Watchlist" automation checks for new releases every 24 hours
    4. +
    5. Enable "Auto-Process Wishlist" — This automation picks up new releases found by the scan and downloads them every 30 minutes
    6. +
    7. Done! — New releases from watched artists are automatically found, queued, downloaded, tagged, and added to your library
    8. +
    + ${docsImg('wf-auto-downloads.gif', 'Setting up auto-downloads')} +
    💡
    Customize per-artist settings (click the gear icon on a watched artist) to control which release types are included: Albums, EPs, Singles, Live, Remixes, etc.
    +
    +
    +

    How to: Import Existing Music

    +

    Goal: Bring music files you already have into SoulSync with proper metadata and organization.

    +
      +
    1. Place files in your staging folder — Put album folders (e.g., Artist - Album/) in the Staging path configured in Settings
    2. +
    3. Go to the Import page — SoulSync detects the files and suggests album matches
    4. +
    5. Search for the correct album — If the auto-suggestion is wrong, search Spotify/iTunes for the right album
    6. +
    7. Match tracks — Drag-and-drop files onto the correct track slots, or click Auto-Match
    8. +
    9. Click Confirm — Files are tagged with official metadata, organized, and moved to your library
    10. +
    + ${docsImg('wf-import-music.gif', 'Importing music')} +
    💡
    For loose singles (not in album folders), use the Singles tab on the Import page.
    +
    +
    +

    How to: Connect Your Media Server

    +

    Goal: Link your media server so downloaded music automatically appears in your library and can be streamed via the built-in player.

    +
      +
    1. Go to Settings — Scroll to the Media Server section
    2. +
    3. Enter your server details — URL and credentials for Plex (URL + Token), Jellyfin (URL + API Key), or Navidrome (URL + Username + Password). Select your music library from the dropdown
    4. +
    5. Click Test Connection — Verify the connection is working. A green checkmark confirms success
    6. +
    + ${docsImg('wf-media-server.gif', 'Connecting media server')} +
    💡
    Make sure your Transfer Path points to the same folder your media server monitors. This is how new downloads automatically appear in your library.
    +
    + ` + }, { id: 'dashboard', title: 'Dashboard', @@ -301,6 +445,7 @@ const DOCS_SECTIONS = [

    Overview & Stats

    The dashboard is your command center. At the top you'll see service status indicators for Spotify, your media server, and Soulseek — showing connected/disconnected state at a glance. Below that, stat cards display your library totals: artists, albums, tracks, and total library size.

    Stats update in real-time via WebSocket — no page refresh needed.

    + ${docsImg('dash-overview.png', 'Dashboard overview')}

    Enrichment Workers

    @@ -317,9 +462,9 @@ const DOCS_SECTIONS = [

    Tidal

    Tidal IDs, artist images, album labels, explicit flags, ISRCs

    Qobuz

    Qobuz IDs, artist images, album labels, genres, explicit flags

    + ${docsImg('dash-workers.png', 'Enrichment workers status')}
    ℹ️
    Workers retry "not found" items every 30 days and errored items every 7 days. You can pause/resume any worker from the dashboard.
    -

    Rate Limit Protection: Workers include smart rate limiting for all APIs. If Spotify returns a rate limit with a Retry-After greater than 60 seconds, a global ban activates — all Spotify API calls are suppressed before they're made, and searches automatically fall back to iTunes/Apple Music. A countdown modal appears showing the ban duration, triggering endpoint, and remaining time. The enrichment worker auto-pauses during the ban and resumes when it expires.

    -

    The rate limit modal provides a Disconnect Spotify button that immediately clears the ban, pauses the Spotify enrichment worker, and deletes the local Spotify cache. This lets you switch to Apple Music as your primary metadata source without waiting for the ban to expire.

    +

    Rate Limit Protection: Workers include smart rate limiting for all APIs. If Spotify returns a rate limit with a Retry-After greater than 60 seconds, the app seamlessly switches to iTunes/Apple Music — an amber indicator appears in the sidebar, searches automatically use Apple Music, and the enrichment worker pauses. When the ban expires, everything recovers automatically. No action needed from the user.

    Tool Cards

    @@ -336,6 +481,7 @@ const DOCS_SECTIONS = [ Backup ManagerCreate, download, restore, and delete database backups. Rolling cleanup keeps the 5 most recent. + ${docsImg('dash-tools.png', 'Dashboard tool cards')}
    💡
    Each tool card has a help button (?) that opens detailed instructions for that specific tool.
    @@ -348,6 +494,7 @@ const DOCS_SECTIONS = [
  • Review the tag differences — mismatches are highlighted
  • Click Retag to write the corrected metadata to the audio files
  • + ${docsImg('dash-retag.png', 'Retag tool interface')}

    The retag operation writes title, artist, album artist, album, track number, disc number, year, and genre. Cover art can optionally be re-embedded.

    @@ -360,6 +507,7 @@ const DOCS_SECTIONS = [
  • Delete — Remove individual backups
  • Rolling Cleanup — Automatically keeps only the 5 most recent backups to save disk space
  • + ${docsImg('dash-backup.png', 'Backup manager')}

    The system automation Auto-Backup Database creates a backup every 3 days automatically. You can adjust the interval in Automations.

    @@ -376,6 +524,7 @@ const DOCS_SECTIONS = [

    Activity Feed

    The activity feed at the bottom of the dashboard shows recent system events: downloads completed, syncs started, settings changed, automation runs, and errors. Events appear in real-time via WebSocket.

    +

    Events include: downloads started/completed/failed, playlist syncs, watchlist scans, automation runs, enrichment worker progress, settings changes, and system errors. The feed shows the 10 most recent events and updates in real-time via WebSocket. Older events are available in the application logs.

    ` }, @@ -398,6 +547,7 @@ const DOCS_SECTIONS = [

    Overview

    The Sync page lets you import playlists from Spotify, YouTube, Tidal, and Beatport. Once imported, playlists are mirrored — they persist in your SoulSync instance and can be refreshed, discovered, and synced to your wishlist for downloading.

    + ${docsImg('sync-overview.png', 'Playlist sync page')}

    Spotify Playlists

    @@ -408,11 +558,13 @@ const DOCS_SECTIONS = [
  • Download Missing — Opens a modal showing tracks not in your library, with download controls
  • Sync Playlist — Adds tracks to your wishlist for automated downloading
  • + ${docsImg('sync-spotify.png', 'Spotify playlists loaded')}
    💡
    Spotify-sourced playlists are auto-discovered at confidence 1.0 during refresh — no separate discovery step needed.

    YouTube Playlists

    Paste a YouTube playlist URL into the input field and click Parse Playlist. SoulSync extracts the track list and attempts to match each track to official Spotify/iTunes metadata.

    + ${docsImg('sync-youtube.png', 'YouTube playlist import')}
    ⚠️
    YouTube tracks often have non-standard titles (e.g., "Artist - Song (Official Video)"). The discovery pipeline handles this, but some manual fixes may be needed for edge cases.
    @@ -443,6 +595,7 @@ const DOCS_SECTIONS = [

    Genre Browser — Browse 12+ electronic music genres (House, Techno, Drum & Bass, Trance, etc.) with per-genre views: Top 10 tracks, staff picks, hype rankings, latest releases, and new charts.

    Charts — Top 100 and Hype charts with full track listings. Each track can be manually matched against Spotify for metadata, then synced and downloaded.

    + ${docsImg('sync-beatport.png', 'Beatport genre browser')}
    ℹ️
    Beatport data is cached with a configurable TTL. The system automation Refresh Beatport Cache runs every 24 hours to keep content fresh.
    @@ -454,6 +607,7 @@ const DOCS_SECTIONS = [
  • Download progress survives page refresh
  • Each profile has its own mirrored playlists
  • + ${docsImg('sync-mirror.png', 'Mirrored playlist cards')}

    M3U Export

    @@ -497,11 +651,13 @@ const DOCS_SECTIONS = [
  • Click a track to search Soulseek for that specific song
  • Preview tracks — Play button on search result tracks lets you stream a preview directly from your download source before committing to a download
  • + ${docsImg('dl-enhanced-search.png', 'Enhanced search results')}

    Basic Search

    Toggle to Basic Search mode for direct Soulseek queries. This shows raw search results with detailed info: format, bitrate, quality score, file size, uploader name, upload speed, and availability.

    Filters let you narrow results by type (Albums/Singles), format (FLAC/MP3/OGG/AAC/WMA), and sort by relevance, quality, size, bitrate, duration, or uploader speed.

    + ${docsImg('dl-basic-search.png', 'Basic Soulseek search')}

    Download Sources

    @@ -529,6 +685,7 @@ const DOCS_SECTIONS = [

    Downloads can be started from multiple places: Enhanced Search results, artist discography, Download Missing modal, wishlist auto-processing, and playlist sync.

    Download Candidate Selection: If a download fails or no suitable source is found, you can view the cached search candidates and manually pick an alternative file from a different user. This lets you recover failed downloads without restarting the entire search.

    + ${docsImg('dl-candidates.png', 'Download candidate selection')}

    Post-Processing Pipeline

    @@ -542,6 +699,7 @@ const DOCS_SECTIONS = [
  • Lossy Copy — If enabled in settings, a lower-bitrate copy is created alongside the original (useful for mobile device syncing).
  • Media Server Scan — Your media server (Plex/Jellyfin) is notified to scan for the new file. Navidrome auto-detects changes.
  • + ${docsImg('dl-post-processing.png', 'Post-processing pipeline complete')}
    ℹ️
    Quarantine: Files that fail AcoustID verification are moved to a quarantine folder instead of your library. You can review quarantined files and manually approve or delete them. The automation engine can trigger notifications when files are quarantined.
    @@ -556,6 +714,7 @@ const DOCS_SECTIONS = [

    Each format has configurable bitrate ranges and a priority order. Enable Fallback to accept any quality when preferred formats aren't available.

    + ${docsImg('dl-quality-profiles.png', 'Quality profile settings')}

    Download Manager

    @@ -584,6 +743,7 @@ const DOCS_SECTIONS = [
  • Watch All — Add all featured artists to your watchlist at once
  • View Recommended — See 50+ similar artists with enrichment data
  • + ${docsImg('disc-hero.png', 'Featured artist hero slider')}

    Discovery & Personalized Playlists

    @@ -602,6 +762,7 @@ const DOCS_SECTIONS = [ Familiar FavoritesLibraryWell-known tracks from artists you follow + ${docsImg('disc-playlists.png', 'Discovery playlist cards')}

    Each playlist can be played in the media player, downloaded, or synced to your media server.

    Genre Browser — Filter discovery pool content by specific genres. Browse available genres and view top tracks within each genre category.

    ListenBrainz Playlists — If ListenBrainz is configured, the Discover page also shows personalized playlists generated from your listening history: Created For You, Your Playlists, and Collaborative playlists.

    @@ -609,6 +770,7 @@ const DOCS_SECTIONS = [

    Build Custom Playlist

    Search for 1–5 artists, select them, and click Generate to create a custom playlist from their catalogs. You can then download or sync the generated playlist.

    + ${docsImg('disc-build-playlist.png', 'Build custom playlist')}

    Seasonal & Curated Content

    @@ -622,6 +784,7 @@ const DOCS_SECTIONS = [

    Time Machine

    Browse discovery pool content by decade — tabs from the 1950s through the 2020s. Each decade pulls top tracks from pool artists active in that era.

    + ${docsImg('disc-time-machine.png', 'Time Machine decade browser')}
    ` }, @@ -641,6 +804,7 @@ const DOCS_SECTIONS = [

    Artist Detail & Discography

    @@ -653,6 +817,7 @@ const DOCS_SECTIONS = [

    At the top, View on buttons link to the artist on each matched external service (Spotify, Apple Music, MusicBrainz, Deezer, AudioDB, Last.fm, Genius, Tidal, Qobuz). Service badges on artist cards also indicate which services have matched this artist.

    Similar Artists appear as clickable bubbles below the discography for further exploration and discovery.

    + ${docsImg('art-detail.png', 'Artist detail page')}

    Watchlist

    @@ -664,6 +829,7 @@ const DOCS_SECTIONS = [
  • Use Watch All to add all recommended artists at once
  • Watch All Unwatched — Bulk-add every library artist that isn't already on your watchlist
  • + ${docsImg('art-watchlist.png', 'Watchlist page')}

    New Release Scanning

    @@ -674,6 +840,7 @@ const DOCS_SECTIONS = [
  • Recent wishlist additions feed
  • Stats: artists scanned, new tracks found, tracks added to wishlist
  • + ${docsImg('art-scan.png', 'New release scan panel')}

    Wishlist

    @@ -687,6 +854,7 @@ const DOCS_SECTIONS = [

    Manual Processing: Use the Process Wishlist automation action to trigger processing on demand. Options include processing all items, albums only, or singles only.

    Cleanup: The Cleanup Wishlist action removes duplicates (same track added multiple times) and items you already own in your library.

    ℹ️
    Each wishlist item tracks its source (watchlist scan, playlist sync, manual), number of retry attempts, last error message, and status (pending, downloading, failed, complete).
    + ${docsImg('art-wishlist.png', 'Wishlist queue')}

    Watchlist Settings

    @@ -713,6 +881,7 @@ const DOCS_SECTIONS = [

    Overview

    Automations let you schedule tasks and react to events with a visual WHEN → DO → THEN builder. Create custom workflows like "When a download completes, update the database, then notify me on Discord."

    Each automation card shows its trigger/action flow, last run time, next scheduled run (with countdown), and a Run Now button for instant execution.

    + ${docsImg('auto-overview.png', 'Automations page')}

    Builder

    @@ -723,6 +892,7 @@ const DOCS_SECTIONS = [
  • THEN (Post-Action) — Up to 3 notification or signal actions after the DO completes
  • Add Conditions to filter when the automation runs. Match modes: All (AND) or Any (OR). Operators: contains, equals, starts_with, not_contains.

    + ${docsImg('auto-builder.png', 'Automation builder')}

    All Triggers

    @@ -801,6 +971,7 @@ const DOCS_SECTIONS = [

    Use the Run Now button on any automation card to execute it immediately, regardless of its schedule. The result (success/failure) updates in real-time on the card. Running automations display a glow effect on their card.

    Stall detection: If an automation action runs for more than 2 hours without completing, it is automatically flagged as stalled and terminated to prevent resource leaks.

    The Dashboard activity feed also logs every automation execution with timestamps, so you can review the full history of what ran and when.

    + ${docsImg('auto-history.png', 'Automation execution history')}

    System Automations

    @@ -820,6 +991,7 @@ const DOCS_SECTIONS = [ Full CleanupEvery 12 hours + ${docsImg('auto-system.png', 'System automations')}
    ` }, @@ -841,6 +1013,7 @@ const DOCS_SECTIONS = [

    The Library page shows all artists in your collection as cards with images, album/track counts, and service badges (Spotify, MusicBrainz, Deezer, AudioDB, iTunes, Last.fm, Genius, Tidal, Qobuz) indicating which services have matched this artist.

    Use the search bar, alphabet navigation (A–Z, #), and watchlist filter (All/Watched/Unwatched) to browse. Click any artist card to view their discography.

    The artist detail page shows albums, EPs, and singles as cards with completion percentages. Filter by category, content type (live/compilations/featured), or status (owned/missing). At the top, View on buttons link to the artist on each matched external service.

    + ${docsImg('lib-standard.png', 'Library artist grid')}

    Enhanced Library Manager

    @@ -853,6 +1026,7 @@ const DOCS_SECTIONS = [
  • Play tracks — Queue button adds tracks to the media player
  • Delete — Remove tracks or albums from the database (files on disk are never touched)
  • + ${docsImg('lib-enhanced.png', 'Enhanced Library Manager')}

    Service Matching

    @@ -868,6 +1042,7 @@ const DOCS_SECTIONS = [
  • Optionally enable Embed cover art and Sync to server
  • Click Write Tags to apply changes to the file
  • + ${docsImg('lib-tags.png', 'Tag preview modal')}

    Supports MP3, FLAC, OGG, and M4A via Mutagen. After writing, optional server sync pushes metadata to Plex (per-track update), Jellyfin (library scan), or Navidrome (auto-detects).

    @@ -878,6 +1053,7 @@ const DOCS_SECTIONS = [
  • Write Tags — Batch write tags to all selected tracks with live progress
  • Clear Selection — Deselect all
  • + ${docsImg('lib-bulk.png', 'Bulk operations bar')}

    Download Missing Tracks

    @@ -901,7 +1077,10 @@ const DOCS_SECTIONS = [

    Staging Setup

    Set your staging folder path in Settings → Download Settings. Place audio files you want to import into this folder. SoulSync scans the folder and detects albums from the file structure.

    +

    Place albums in subfolders (e.g., Artist - Album/) and loose singles at the root level.

    The import page header shows the total files in staging and their combined size.

    + ${docsImg('imp-staging.png', 'Import staging page')} +
    💡
    Files not showing up? Check that your staging folder path is correct in Settings and that the folder has read permissions. Docker users: make sure the staging volume mount is configured in your docker-compose.yml.

    Import Workflow

    @@ -912,6 +1091,7 @@ const DOCS_SECTIONS = [
  • Match tracks — Drag-and-drop staged files onto album track slots, or let auto-match attempt it
  • Review the match and click Confirm to import — files are tagged, organized, and added to your library
  • + ${docsImg('imp-matching.png', 'Track matching interface')}

    Singles Import

    @@ -936,6 +1116,7 @@ const DOCS_SECTIONS = [
  • Map columns to the correct fields (Artist, Album, Track)
  • SoulSync searches for each track on Spotify/iTunes and adds matches to your wishlist for downloading
  • + ${docsImg('imp-textfile.png', 'Text file import')}
    ` }, @@ -954,6 +1135,7 @@ const DOCS_SECTIONS = [

    Playback Controls

    The sidebar media player is always visible when a track is loaded. It shows album art, track info, a seekable progress bar, and playback controls (play/pause, previous, next, volume, repeat, shuffle).

    Click the sidebar player to open the Now Playing modal — a full-screen experience with large album art, ambient glow (dominant color from cover art), a frequency-driven audio visualizer, and expanded controls.

    + ${docsImg('player-nowplaying.png', 'Now Playing modal')}

    Streaming & Sources

    @@ -970,6 +1152,7 @@ const DOCS_SECTIONS = [

    Add tracks to the queue from the Enhanced Library Manager or download results. Manage the queue in the Now Playing modal: reorder, remove individual tracks, or clear all.

    Smart Radio mode (toggle in queue header) automatically adds similar tracks when the queue runs out, based on genre, mood, style, and artist similarity. Playback continues seamlessly.

    Repeat modes: Off → Repeat All (loop queue) → Repeat One. Shuffle randomizes the next track from the remaining queue.

    + ${docsImg('player-queue.png', 'Queue panel')}

    Keyboard Shortcuts

    @@ -1015,6 +1198,7 @@ const DOCS_SECTIONS = [
  • AcoustID — API key from acoustid.org (enables fingerprint verification)
  • ListenBrainz — Base URL + token for listening history and playlist import
  • + ${docsImg('settings-credentials.png', 'Service credentials')}

    Media Server Setup

    @@ -1027,6 +1211,7 @@ const DOCS_SECTIONS = [ NavidromeURL + Username + PasswordSelect the Music Folder to monitor. Navidrome auto-detects new files, so SoulSync doesn't need to trigger scans — just place files in the right folder. + ${docsImg('settings-media-server.png', 'Media server setup')}

    The media player streams audio directly from your connected server — tracks play through your Plex, Jellyfin, or Navidrome instance without needing local file access.

    💡
    Navidrome users: If artist images are broken after upgrading, use the Fix Navidrome URLs tool in Settings to convert old image URL formats to the correct Subsonic API format.
    @@ -1041,6 +1226,7 @@ const DOCS_SECTIONS = [
  • Lossy Copy — When enabled, creates a lower-bitrate MP3 copy of every downloaded file. Configure the output bitrate (default 320kbps) and output folder. Optionally delete the original lossless file after creating the lossy copy. Useful for syncing to mobile devices or streaming servers with bandwidth constraints.
  • Content Filtering — Toggle explicit content filtering to control whether explicit tracks appear in search results and downloads.
  • + ${docsImg('settings-downloads.png', 'Download settings')}
    ⚠️
    Docker users: Always use container-side paths in these settings (e.g., /app/downloads, /app/Transfer). Never use host paths like /mnt/music — the container can't access those. Your docker-compose volumes section is where host paths are mapped to container paths. See Getting Started → Folder Setup for a complete walkthrough.
    @@ -1056,6 +1242,7 @@ const DOCS_SECTIONS = [
  • Soulseek Search Timeout — How long to wait for Soulseek search results before giving up (seconds).
  • Discovery Lookback Period — How many weeks back to check for new releases during watchlist scans.
  • + ${docsImg('settings-processing.png', 'Processing settings')}

    Quality Profiles

    @@ -1110,6 +1297,8 @@ const DOCS_SECTIONS = [
  • Set an Admin PIN when multiple profiles exist to protect the admin account
  • Profile 1 (admin) cannot be deleted
  • +

    PINs are 4-6 digits. If you forget your PIN, the admin can reset it from Manage Profiles. The admin PIN protects settings and destructive operations when multiple profiles exist.

    + ${docsImg('profiles-picker.png', 'Profile picker')}

    Permissions & Page Access

    @@ -1119,6 +1308,7 @@ const DOCS_SECTIONS = [
  • Can Download Music — Toggle whether the profile can initiate downloads. When disabled, all download buttons are hidden and the backend blocks download API calls with a 403 error.
  • Enhanced Library Manager — The Enhanced view toggle on artist detail pages is only available to admin profiles. Non-admin users see the Standard view only.
  • + ${docsImg('profiles-permissions.png', 'Profile permissions')}

    If the admin removes a page that was set as a user's home page, the home page automatically resets. Navigation guards prevent users from accessing restricted pages even via direct URL or browser history.

    ℹ️
    Existing profiles created before permissions were added have full access to all pages by default. The admin must explicitly restrict access per profile.
    @@ -1140,7 +1330,18 @@ const DOCS_SECTIONS = [ icon: '/static/settings.png', children: [ { id: 'api-auth', title: 'Authentication' }, - { id: 'api-endpoints', title: 'Key Endpoints' }, + { id: 'api-system', title: 'System & Status' }, + { id: 'api-search', title: 'Search' }, + { id: 'api-downloads', title: 'Downloads' }, + { id: 'api-library', title: 'Library' }, + { id: 'api-library-edit', title: 'Library Editing' }, + { id: 'api-playlists', title: 'Playlists' }, + { id: 'api-watchlist', title: 'Watchlist & Wishlist' }, + { id: 'api-automations', title: 'Automations' }, + { id: 'api-import', title: 'Import' }, + { id: 'api-settings', title: 'Settings' }, + { id: 'api-enrichment', title: 'Enrichment Workers' }, + { id: 'api-profiles', title: 'Profiles' }, { id: 'api-websocket', title: 'WebSocket Events' } ], content: () => ` @@ -1152,29 +1353,294 @@ const DOCS_SECTIONS = [
  • Query: ?api_key=sk_xxxxx
  • Keys use a sk_ prefix. The raw key is shown once at creation; only a SHA-256 hash is stored.

    +
    Example: cURL with API key
    +
    curl -H "Authorization: Bearer sk_abc123..." http://localhost:5000/api/system/status
    -
    -

    Key Endpoints

    +
    +

    System & Status

    +

    Endpoints for checking system health, service connectivity, and library statistics.

    - + - - - - - - - - - - + + + + +
    EndpointDescription
    MethodEndpointDescription
    GET /api/system/statusUptime and service connectivity
    GET /api/system/statsLibrary counts and sizes
    GET /api/library/artistsPaginated artist list with filters
    GET /api/artist-detail/{id}Full artist info and discography
    POST /api/downloadStart a download
    GET /api/downloads/statusActive download status
    POST /api/searchSearch Soulseek
    POST /api/enhanced-searchEnhanced metadata search
    GET /api/automationsList all automations
    POST /api/database/backupCreate a backup
    GET/api/system/statusUptime, version, and service connectivity
    GET/api/system/statsLibrary counts (artists, albums, tracks) and total size
    GET/api/system/activityRecent activity feed entries
    GET/api/debug-infoFull debug snapshot: services, paths, workers, recent logs
    GET/api/versionCurrent version and update availability
    -

    The full API has 200+ endpoints covering library, downloads, playlists, automations, profiles, settings, and more. Use a reverse proxy (Nginx, Caddy, Traefik) for external access with HTTPS.

    +
    Response: GET /api/system/stats
    +
    { + "artists": 342, + "albums": 1205, + "tracks": 14832, + "total_size": "128.4 GB", + "database_size": "45.2 MB" +}
    +
    + +
    +

    Downloads

    +

    Start, monitor, and manage music downloads.

    + + + + + + + + + +
    MethodEndpointDescription
    POST/api/downloadStart downloading a track
    GET/api/downloads/statusCurrent download queue and progress
    POST/api/download/cancelCancel an active download
    POST/api/download/albumStart downloading an entire album
    GET/api/downloads/historyCompleted and failed download history
    +
    Request: POST /api/download
    +
    Content-Type: application/json + +{ + "artist": "Radiohead", + "album": "OK Computer", + "title": "Karma Police", + "spotify_id": "3SVAN3BRByDmHOhKyIDxfC" +}
    +
    Response
    +
    { + "success": true, + "task_id": "abc123", + "message": "Download started" +}
    +
    +
    +

    Library

    +

    Browse and query your music library.

    + + + + + + + + + +
    MethodEndpointDescription
    GET/api/library/artistsPaginated artist list with search and filters
    GET/api/artist-detail/{id}Full artist info, discography, and match status
    GET/api/library/artist/{id}/enhancedEnhanced view: full tracks, tags, file paths
    GET/api/library/album/{id}/tracksTrack list for an album
    POST/api/database/updateTrigger library database refresh
    +
    Response: GET /api/library/artists?page=1&per_page=20
    +
    { + "artists": [ + { + "id": 1, + "name": "Radiohead", + "album_count": 9, + "track_count": 101, + "image_url": "https://..." + } + ], + "total": 342, + "page": 1, + "per_page": 20 +}
    +
    +
    +

    Library Editing

    +

    Edit metadata and write tags to files via the Enhanced Library Manager.

    + + + + + + + + + + + +
    MethodEndpointDescription
    PUT/api/library/artist/{id}Update artist fields (name, genres, label, etc.)
    PUT/api/library/album/{id}Update album fields
    PUT/api/library/track/{id}Update track fields (title, track_number, bpm)
    POST/api/library/tracks/batchBatch update multiple tracks at once
    GET/api/library/track/{id}/tag-previewPreview tag diff (current file tags vs database)
    POST/api/library/track/{id}/write-tagsWrite database metadata to audio file tags
    POST/api/library/tracks/write-tags-batchBatch write tags to multiple files
    +
    Request: PUT /api/library/track/42
    +
    Content-Type: application/json + +{ + "title": "Karma Police", + "track_number": 6, + "bpm": 75 +}
    +
    +
    +

    Playlists

    +

    Import, sync, and manage mirrored playlists from external sources.

    + + + + + + + + + +
    MethodEndpointDescription
    GET/api/playlists/spotifyList Spotify playlists
    POST/api/playlists/parseParse a YouTube/Tidal playlist URL
    GET/api/playlists/mirroredList all mirrored playlists
    POST/api/playlists/{id}/syncSync playlist tracks to wishlist
    POST/api/playlists/{id}/discoverDiscover official metadata for playlist tracks
    +
    Request: POST /api/playlists/parse
    +
    Content-Type: application/json + +{ + "url": "https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf" +}
    +
    +
    +

    Watchlist & Wishlist

    +

    Manage watched artists and the download wishlist queue.

    + + + + + + + + + +
    MethodEndpointDescription
    GET/api/watchlistList all watched artists
    POST/api/watchlist/addAdd an artist to the watchlist
    DELETE/api/watchlist/{id}Remove artist from watchlist
    GET/api/wishlistList all wishlist items with status
    POST/api/wishlist/processTrigger wishlist processing
    +
    Request: POST /api/watchlist/add
    +
    Content-Type: application/json + +{ + "artist_name": "Radiohead", + "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb" +}
    +
    +
    +

    Automations

    +

    Create, manage, and trigger automations.

    + + + + + + + + + +
    MethodEndpointDescription
    GET/api/automationsList all automations with status
    POST/api/automationsCreate a new automation
    PUT/api/automations/{id}Update an automation
    POST/api/automations/{id}/runRun an automation immediately
    DELETE/api/automations/{id}Delete a custom automation
    +
    Response: GET /api/automations
    +
    [ + { + "id": 1, + "name": "Auto-Process Wishlist", + "trigger_type": "schedule", + "action_type": "process_wishlist", + "enabled": true, + "last_run": "2026-03-12T10:30:00Z", + "run_count": 142, + "is_system": true + } +]
    +
    +
    +

    Import

    +

    Manage the staging folder and import music files.

    + + + + + + + + +
    MethodEndpointDescription
    GET/api/import/stagingList files and folders in the staging directory
    POST/api/import/matchAuto-match staged files to album tracks
    POST/api/import/confirmConfirm and execute an import
    POST/api/import/search/albumsSearch for album matches for staged files
    +
    Response: GET /api/import/staging
    +
    { + "folders": [ + { + "name": "Radiohead - OK Computer", + "files": 12, + "size": "485 MB" + } + ], + "singles": 3, + "total_size": "512 MB" +}
    +
    +
    +

    Settings

    +

    Read and update application settings. Admin-only endpoints.

    + + + + + + + + + +
    MethodEndpointDescription
    GET/api/settingsGet all current settings
    POST/api/settingsUpdate settings (partial update supported)
    POST/api/database/backupCreate a database backup
    GET/api/database/backupsList available backups
    POST/api/database/restoreRestore from a backup
    +
    Response: POST /api/database/backup
    +
    { + "success": true, + "filename": "backup_2026-03-12_103000.db", + "size": "45.2 MB" +}
    +
    +
    +

    Enrichment Workers

    +

    Monitor and control the background metadata enrichment workers.

    + + + + + + + + +
    MethodEndpointDescription
    GET/api/enrichment/statusStatus of all enrichment workers
    POST/api/enrichment/pausePause a specific worker
    POST/api/enrichment/resumeResume a paused worker
    POST/api/enrichment/resetReset worker progress and re-scan all items
    +
    Response: GET /api/enrichment/status
    +
    { + "workers": { + "spotify": { "status": "running", "matched": 142, "total": 342, "current": "Radiohead" }, + "musicbrainz": { "status": "paused", "matched": 98, "total": 342 }, + "lastfm": { "status": "running", "matched": 320, "total": 342 } + } +}
    +
    +
    +

    Profiles

    +

    Manage multi-profile support. Admin-only for create/edit/delete.

    + + + + + + + + + +
    MethodEndpointDescription
    GET/api/profilesList all profiles
    POST/api/profilesCreate a new profile
    PUT/api/profiles/{id}Update a profile (name, avatar, permissions)
    DELETE/api/profiles/{id}Delete a profile (admin only, cannot delete profile 1)
    POST/api/profiles/switchSwitch active profile (PIN required if set)
    +
    Request: POST /api/profiles
    +
    Content-Type: application/json + +{ + "name": "Family Room", + "is_admin": false, + "pin": "1234", + "allowed_pages": ["search", "discover", "library", "sync"], + "can_download": true +}

    WebSocket Events

    -

    SoulSync uses Socket.IO for real-time communication. The frontend connects automatically and receives live updates without polling:

    +

    SoulSync uses Socket.IO for real-time communication. The frontend connects automatically and receives live updates without polling. Connect to the same host/port as the web UI.

    @@ -1185,9 +1651,20 @@ const DOCS_SECTIONS = [ + +
    EventDescription
    scan_progressLibrary scan, quality scan, or duplicate scan progress
    system_statusService connectivity changes (Spotify rate limit, slskd disconnect)
    activitySystem activity feed entries
    wishlist_updateWishlist item added, status changed, or removed
    automation_runAutomation started, completed, or failed
    -

    All UI elements that show live progress (download bars, worker icons, scan counters) are driven by these WebSocket events.

    +

    All UI elements that show live progress (download bars, worker icons, scan counters) are driven by these WebSocket events. External clients can connect using any Socket.IO-compatible library and listen for these same events.

    +
    Example: Socket.IO client (JavaScript)
    +
    const socket = io('http://localhost:5000'); +socket.on('download_progress', (data) => { + console.log(data.title, data.percent + '%'); +}); +socket.on('activity', (data) => { + console.log(data.timestamp, data.message); +});
    +

    The full API has 200+ endpoints covering library, downloads, playlists, automations, profiles, settings, and more. Use a reverse proxy (Nginx, Caddy, Traefik) for external access with HTTPS.

    ` } @@ -1223,6 +1700,68 @@ function initializeDocsPage() { }); nav.innerHTML = navHTML; + // Add debug info button to sidebar header + const sidebarHeader = document.querySelector('.docs-sidebar-header'); + if (sidebarHeader) { + const debugBtn = document.createElement('button'); + debugBtn.className = 'docs-debug-button'; + debugBtn.innerHTML = '📋 Copy Debug Info'; + debugBtn.onclick = async () => { + try { + debugBtn.textContent = 'Collecting...'; + const resp = await fetch('/api/debug-info'); + const data = await resp.json(); + + let text = 'SoulSync Debug Info\n'; + text += '===================================\n\n'; + text += `Version: ${data.version}\n`; + text += `OS: ${data.os}${data.docker ? ' (Docker)' : ''}\n`; + text += `Python: ${data.python}\n\n`; + + text += '-- Services --\n'; + text += `Music Source: ${data.services?.music_source || 'unknown'}\n`; + text += `Spotify: ${data.services?.spotify_connected ? 'Connected' : 'Disconnected'}${data.services?.spotify_rate_limited ? ' (Rate Limited)' : ''}\n`; + text += `Media Server: ${data.services?.media_server_type || 'none'} (${data.services?.media_server_connected ? 'Connected' : 'Disconnected'})\n`; + text += `Soulseek: ${data.services?.soulseek_connected ? 'Connected' : 'Disconnected'}\n`; + text += `Download Source: ${data.services?.download_source || 'unknown'}\n\n`; + + text += '-- Paths --\n'; + text += `Download: ${data.paths?.download_path || '(not set)'} ${data.paths?.download_path_exists ? '\u2713 exists' : '\u2717 missing'}${data.paths?.download_path_writable ? ' \u2713 writable' : ' \u2717 not writable'}\n`; + text += `Transfer: ${data.paths?.transfer_folder || '(not set)'} ${data.paths?.transfer_folder_exists ? '\u2713 exists' : '\u2717 missing'}${data.paths?.transfer_folder_writable ? ' \u2713 writable' : ' \u2717 not writable'}\n`; + text += `Staging: ${data.paths?.staging_folder || '(not set)'} ${data.paths?.staging_folder_exists ? '\u2713 exists' : '\u2717 missing'}\n\n`; + + text += '-- Enrichment Workers --\n'; + if (data.enrichment_workers) { + Object.entries(data.enrichment_workers).forEach(([name, status]) => { + text += `${name}: ${status}\n`; + }); + } + text += '\n'; + + text += `Database: ${data.database_size || 'unknown'}\n`; + text += `Memory: ${data.memory_usage || 'unknown'}\n\n`; + + text += '-- Recent Logs (last 20 lines) --\n'; + if (data.recent_logs?.length) { + data.recent_logs.forEach(line => { text += line + '\n'; }); + } + + await navigator.clipboard.writeText(text); + debugBtn.innerHTML = '✅ Copied!'; + debugBtn.classList.add('copied'); + setTimeout(() => { + debugBtn.innerHTML = '📋 Copy Debug Info'; + debugBtn.classList.remove('copied'); + }, 2000); + } catch (err) { + debugBtn.innerHTML = '❌ Failed'; + console.error('Debug info error:', err); + setTimeout(() => { debugBtn.innerHTML = '📋 Copy Debug Info'; }, 2000); + } + }; + sidebarHeader.appendChild(debugBtn); + } + // Build content let contentHTML = ''; DOCS_SECTIONS.forEach(section => { @@ -1380,3 +1919,15 @@ function initializeDocsPage() { if (firstChildren) firstChildren.classList.add('expanded'); } } + +function navigateToDocsSection(sectionId) { + // Switch to help page + if (typeof showPage === 'function') showPage('help'); + // Wait for docs to initialize + setTimeout(() => { + const target = document.getElementById(sectionId); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, 300); +} diff --git a/webui/static/script.js b/webui/static/script.js index bf30842d..0e454070 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -5426,11 +5426,11 @@ async function saveSettings(quiet = false) { showToast('Settings saved, but quality profile failed to save', 'warning'); setTimeout(updateServiceStatus, 1000); } else { - showToast(`Failed to save settings: ${result.error}`, 'error'); + showToast(`Failed to save settings: ${result.error}`, 'error', 'set-services'); } } catch (error) { console.error('Error saving settings:', error); - showToast('Failed to save settings', 'error'); + showToast('Failed to save settings', 'error', 'set-services'); } finally { if (!quiet) hideLoadingOverlay(); } @@ -5461,11 +5461,11 @@ async function testConnection(service) { loadNavidromeMusicFolders(); } } else { - showToast(`${service} connection failed: ${result.error}`, 'error'); + showToast(`${service} connection failed: ${result.error}`, 'error', 'gs-connecting'); } } catch (error) { console.error(`Error testing ${service} connection:`, error); - showToast(`Failed to test ${service} connection`, 'error'); + showToast(`Failed to test ${service} connection`, 'error', 'gs-connecting'); } finally { hideLoadingOverlay(); } @@ -5765,7 +5765,7 @@ async function authenticateSpotify() { window.open('/auth/spotify', '_blank'); } catch (error) { console.error('Error authenticating Spotify:', error); - showToast('Failed to start Spotify authentication', 'error'); + showToast('Failed to start Spotify authentication', 'error', 'gs-connecting'); } finally { hideLoadingOverlay(); } @@ -13955,7 +13955,7 @@ function hideLoadingOverlay() { // Toast deduplication cache let recentToasts = new Map(); -function showToast(message, type = 'success') { +function showToast(message, type = 'success', helpSection = null) { const container = document.getElementById('toast-container'); // Create a unique key for this toast @@ -13985,14 +13985,28 @@ function showToast(message, type = 'success') { toast.className = `toast ${type}`; toast.textContent = message; + // Add contextual help link if a docs section is specified + if (helpSection) { + const helpLink = document.createElement('span'); + helpLink.className = 'toast-help-link'; + helpLink.textContent = 'Learn more'; + helpLink.onclick = (e) => { + e.stopPropagation(); + if (typeof navigateToDocsSection === 'function') { + navigateToDocsSection(helpSection); + } + }; + toast.appendChild(helpLink); + } + container.appendChild(toast); - // Auto-remove after 3 seconds + // Auto-remove after 3 seconds (5 seconds if has help link) setTimeout(() => { if (container.contains(toast)) { container.removeChild(toast); } - }, 3000); + }, helpSection ? 5000 : 3000); } function escapeHtml(text) { @@ -36115,6 +36129,11 @@ function _pollBatchWriteTagsStatus() { msg += ` — ${serverName} sync: ${state.sync_synced} synced, ${state.sync_failed} failed`; } } + // Surface the first error reason so users can diagnose (e.g. "File not found") + if (state.failed > 0 && state.errors && state.errors.length > 0) { + const firstErr = state.errors[0].error || 'Unknown error'; + msg += ` (${firstErr})`; + } showToast(msg, state.failed > 0 || state.sync_failed > 0 ? 'warning' : 'success'); _batchWriteTagsPollTimer = null; } @@ -40428,11 +40447,11 @@ async function selectNavidromeMusicFolder() { showToast(data.message, 'success'); } else { console.error('Failed to set music folder:', data.error); - showToast(`Failed to set music folder: ${data.error}`, 'error'); + showToast(`Failed to set music folder: ${data.error}`, 'error', 'set-media'); } } catch (error) { console.error('Error selecting Navidrome music folder:', error); - showToast('Error selecting music folder. Please try again.', 'error'); + showToast('Error selecting music folder. Please try again.', 'error', 'set-media'); } } diff --git a/webui/static/style.css b/webui/static/style.css index 564f4516..88a617aa 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -34551,6 +34551,182 @@ tr.tag-diff-same { display: flex; } +/* Workflow cards */ +.docs-workflow-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; + margin: 20px 0; +} + +.docs-workflow-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 12px; + padding: 20px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + gap: 10px; +} + +.docs-workflow-card:hover { + background: rgba(var(--accent-rgb), 0.06); + border-color: rgba(var(--accent-rgb), 0.2); + transform: translateY(-2px); +} + +.docs-workflow-card-header { + display: flex; + align-items: center; + gap: 12px; +} + +.docs-workflow-card-icon { + font-size: 24px; + flex-shrink: 0; +} + +.docs-workflow-card-title { + font-size: 15px; + font-weight: 600; + color: #fff; + flex: 1; +} + +.docs-workflow-card-badge { + padding: 3px 10px; + border-radius: 20px; + background: rgba(var(--accent-rgb), 0.15); + color: rgb(var(--accent-light-rgb)); + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.docs-workflow-card-desc { + font-size: 13px; + line-height: 1.5; + color: rgba(255, 255, 255, 0.55); + margin: 0; +} + +.docs-workflow-card-link { + font-size: 12.5px; + color: rgb(var(--accent-light-rgb)); + font-weight: 500; + align-self: flex-start; + opacity: 0.7; + transition: opacity 0.15s; +} + +.docs-workflow-card:hover .docs-workflow-card-link { + opacity: 1; +} + +/* Debug info button */ +.docs-debug-button { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + margin-top: 12px; + border-radius: 8px; + border: 1px solid rgba(var(--accent-rgb), 0.25); + background: rgba(var(--accent-rgb), 0.08); + color: rgb(var(--accent-light-rgb)); + font-size: 12.5px; + font-weight: 600; + font-family: 'SF Pro Text', -apple-system, sans-serif; + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.docs-debug-button:hover { + background: rgba(var(--accent-rgb), 0.15); + border-color: rgba(var(--accent-rgb), 0.4); +} + +.docs-debug-button.copied { + background: rgba(var(--accent-rgb), 0.2); + border-color: rgb(var(--accent-rgb)); +} + +/* Toast help link */ +.toast-help-link { + display: inline-block; + margin-left: 8px; + color: rgba(255, 255, 255, 0.5); + font-size: 12px; + text-decoration: underline; + text-decoration-style: dotted; + cursor: pointer; + transition: color 0.15s; +} + +.toast-help-link:hover { + color: rgba(255, 255, 255, 0.85); +} + +/* API code examples */ +.docs-code-block { + background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + padding: 16px; + margin: 12px 0; + overflow-x: auto; + font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; + font-size: 12.5px; + line-height: 1.6; + color: rgba(255, 255, 255, 0.8); + white-space: pre; +} + +.docs-code-block .code-key { + color: rgb(var(--accent-light-rgb)); +} + +.docs-code-block .code-string { + color: rgba(152, 195, 121, 0.9); +} + +.docs-code-block .code-number { + color: rgba(209, 154, 102, 0.9); +} + +.docs-code-block .code-comment { + color: rgba(255, 255, 255, 0.3); + font-style: italic; +} + +.docs-code-label { + font-size: 11.5px; + font-weight: 600; + color: rgba(255, 255, 255, 0.5); + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 16px 0 4px; +} + +/* Screenshot enhancements */ +.docs-screenshot { + display: block; + loading: lazy; +} + +.docs-screenshot-caption { + font-size: 12px; + color: rgba(255, 255, 255, 0.4); + text-align: center; + margin: -8px 0 16px; + font-style: italic; +} + /* Mobile */ @media (max-width: 900px) { .docs-layout {