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 `
`;
+}
+
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 = [
| ListenBrainz | Listening history and recommendations | URL + 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/Staging | For 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
+
+ - Download one album — Verify your folder paths and post-processing work end-to-end
+ - Run a Database Update — Dashboard → Database Updater → Full Refresh to import your existing media server library
+ - Add 5–10 artists to your Watchlist — This seeds the discovery pool for recommendations
+ - Check the Automations page — Enable the system automations you want (auto-process wishlist, auto-scan watchlist, auto-backup)
+ - Explore the Discover page — Once your watchlist has artists, recommendations and playlists appear here
+
+
+
+
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.
+
+ - Open Search — Click the Search page in the sidebar (make sure Enhanced Search is active)
+ - Type the album name — Results appear in a categorized dropdown: Artists, Albums, Singles & EPs, Tracks
+ - Click the album result — The download modal opens showing cover art, tracklist, and album details
+ - Select tracks — All tracks are selected by default. Uncheck any you don't want
+ - Click Download — SoulSync searches for each track, downloads, tags, and organizes the files automatically
+
+ ${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.
+
+ - Go to the Sync page — Click Sync in the sidebar
+ - Click Refresh — Your Spotify playlists load automatically (or paste a playlist URL directly)
+ - Click Sync on a playlist — This adds all missing tracks to your wishlist
+ - Wait for auto-processing — The wishlist processor runs every 30 minutes and downloads queued tracks. Or click "Process Wishlist" in Automations to start immediately
+
+ ${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.
+
+ - Add artists to your Watchlist — Search for artists on the Artists page and click the Watch button on each one
+ - Go to Automations — The built-in "Auto-Scan Watchlist" automation checks for new releases every 24 hours
+ - Enable "Auto-Process Wishlist" — This automation picks up new releases found by the scan and downloads them every 30 minutes
+ - Done! — New releases from watched artists are automatically found, queued, downloaded, tagged, and added to your library
+
+ ${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.
+
+ - Place files in your staging folder — Put album folders (e.g.,
Artist - Album/) in the Staging path configured in Settings
+ - Go to the Import page — SoulSync detects the files and suggests album matches
+ - Search for the correct album — If the auto-suggestion is wrong, search Spotify/iTunes for the right album
+ - Match tracks — Drag-and-drop files onto the correct track slots, or click Auto-Match
+ - Click Confirm — Files are tagged with official metadata, organized, and moved to your library
+
+ ${docsImg('wf-import-music.gif', 'Importing music')}
+
💡For loose singles (not in album folders), use the Singles tab on the Import page.
+
+
+ `
+ },
{
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.
@@ -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 Favorites | Library | Well-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 Search
Search for any artist by name. Results show artist cards with images and genres. Results come from Spotify (or iTunes as fallback). Click any card to open the artist detail view.
+ ${docsImg('art-search.png', 'Artist search results')}
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 Cleanup | Every 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')}
@@ -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.
- | Endpoint | Description |
+ | Method | Endpoint | Description |
- GET /api/system/status | Uptime and service connectivity |
- GET /api/system/stats | Library counts and sizes |
- GET /api/library/artists | Paginated artist list with filters |
- GET /api/artist-detail/{id} | Full artist info and discography |
- POST /api/download | Start a download |
- GET /api/downloads/status | Active download status |
- POST /api/search | Search Soulseek |
- POST /api/enhanced-search | Enhanced metadata search |
- GET /api/automations | List all automations |
- POST /api/database/backup | Create a backup |
+ | GET | /api/system/status | Uptime, version, and service connectivity |
+ | GET | /api/system/stats | Library counts (artists, albums, tracks) and total size |
+ | GET | /api/system/activity | Recent activity feed entries |
+ | GET | /api/debug-info | Full debug snapshot: services, paths, workers, recent logs |
+ | GET | /api/version | Current 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"
+}
+
+
+
Search
+
Search for music across metadata sources and Soulseek.
+
+ | Method | Endpoint | Description |
+
+ | POST | /api/enhanced-search | Search Spotify/iTunes for artists, albums, and tracks |
+ | POST | /api/search | Search Soulseek directly for files |
+ | POST | /api/spotify/search_tracks | Search Spotify for tracks by query |
+ | POST | /api/itunes/search_tracks | Search iTunes/Apple Music for tracks |
+
+
+
Request: POST /api/enhanced-search
+
Content-Type: application/json
+
+{
+ "query": "Radiohead OK Computer",
+ "type": "album"
+}
+
+
+
Downloads
+
Start, monitor, and manage music downloads.
+
+ | Method | Endpoint | Description |
+
+ | POST | /api/download | Start downloading a track |
+ | GET | /api/downloads/status | Current download queue and progress |
+ | POST | /api/download/cancel | Cancel an active download |
+ | POST | /api/download/album | Start downloading an entire album |
+ | GET | /api/downloads/history | Completed 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/library/artists | Paginated artist list with search and filters |
+ | GET | /api/artist-detail/{id} | Full artist info, discography, and match status |
+ | GET | /api/library/artist/{id}/enhanced | Enhanced view: full tracks, tags, file paths |
+ | GET | /api/library/album/{id}/tracks | Track list for an album |
+ | POST | /api/database/update | Trigger 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.
+
+ | Method | Endpoint | Description |
+
+ | 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/batch | Batch update multiple tracks at once |
+ | GET | /api/library/track/{id}/tag-preview | Preview tag diff (current file tags vs database) |
+ | POST | /api/library/track/{id}/write-tags | Write database metadata to audio file tags |
+ | POST | /api/library/tracks/write-tags-batch | Batch 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/playlists/spotify | List Spotify playlists |
+ | POST | /api/playlists/parse | Parse a YouTube/Tidal playlist URL |
+ | GET | /api/playlists/mirrored | List all mirrored playlists |
+ | POST | /api/playlists/{id}/sync | Sync playlist tracks to wishlist |
+ | POST | /api/playlists/{id}/discover | Discover 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/watchlist | List all watched artists |
+ | POST | /api/watchlist/add | Add an artist to the watchlist |
+ | DELETE | /api/watchlist/{id} | Remove artist from watchlist |
+ | GET | /api/wishlist | List all wishlist items with status |
+ | POST | /api/wishlist/process | Trigger wishlist processing |
+
+
+
Request: POST /api/watchlist/add
+
Content-Type: application/json
+
+{
+ "artist_name": "Radiohead",
+ "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb"
+}
+
+
+
Automations
+
Create, manage, and trigger automations.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/automations | List all automations with status |
+ | POST | /api/automations | Create a new automation |
+ | PUT | /api/automations/{id} | Update an automation |
+ | POST | /api/automations/{id}/run | Run 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/import/staging | List files and folders in the staging directory |
+ | POST | /api/import/match | Auto-match staged files to album tracks |
+ | POST | /api/import/confirm | Confirm and execute an import |
+ | POST | /api/import/search/albums | Search 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/settings | Get all current settings |
+ | POST | /api/settings | Update settings (partial update supported) |
+ | POST | /api/database/backup | Create a database backup |
+ | GET | /api/database/backups | List available backups |
+ | POST | /api/database/restore | Restore 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/enrichment/status | Status of all enrichment workers |
+ | POST | /api/enrichment/pause | Pause a specific worker |
+ | POST | /api/enrichment/resume | Resume a paused worker |
+ | POST | /api/enrichment/reset | Reset 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.
+
+ | Method | Endpoint | Description |
+
+ | GET | /api/profiles | List all profiles |
+ | POST | /api/profiles | Create 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/switch | Switch 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.
| Event | Description |
@@ -1185,9 +1651,20 @@ const DOCS_SECTIONS = [
scan_progress | Library scan, quality scan, or duplicate scan progress |
system_status | Service connectivity changes (Spotify rate limit, slskd disconnect) |
activity | System activity feed entries |
+ wishlist_update | Wishlist item added, status changed, or removed |
+ automation_run | Automation 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 {