You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/webui/index.html

8522 lines
668 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<title>SoulSync - Music Sync & Manager</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png', v=static_v) }}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.json', v=static_v) }}">
<meta name="theme-color" content="#1db954">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='pwa-icon-192.png', v=static_v) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=static_v) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='basic-search-v2.css', v=static_v) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='mobile.css', v=static_v) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='setup-wizard.css', v=static_v) }}">
{{ vite_assets('head')|safe }}
</head>
<body>
<!-- Setup Wizard Overlay -->
<div id="setup-wizard-overlay" class="setup-wizard-overlay" style="display: none;">
<div class="setup-wizard-container">
<div class="setup-stepper" id="setup-wizard-stepper"></div>
<div id="setup-wizard-content"></div>
</div>
</div>
<!-- Launch PIN Lock Screen -->
<div id="launch-pin-overlay" class="launch-pin-overlay" style="display: none;">
<div class="launch-pin-container" id="launch-pin-container">
<!-- PIN Entry (default view) -->
<div id="launch-pin-entry">
<div class="launch-pin-icon">🔒</div>
<h2 class="launch-pin-title">SoulSync is Locked</h2>
<p class="launch-pin-subtitle">Enter your PIN to continue</p>
<input type="password" id="launch-pin-input" class="launch-pin-input" maxlength="20" placeholder="PIN" autocomplete="off">
<button id="launch-pin-submit" class="launch-pin-submit">Unlock</button>
<p id="launch-pin-error" class="launch-pin-error" style="display: none;"></p>
<button class="launch-pin-forgot" onclick="showForgotPinView()">Forgot PIN?</button>
</div>
<!-- Forgot PIN (recovery view) -->
<div id="launch-pin-recovery" style="display: none;">
<div class="launch-pin-icon">🔑</div>
<h2 class="launch-pin-title">Verify Your Identity</h2>
<p class="launch-pin-subtitle">Enter any API key, token, or secret you configured in SoulSync settings</p>
<input type="password" id="launch-recovery-input" class="launch-pin-input" maxlength="200" placeholder="Paste any API credential..." autocomplete="off" style="letter-spacing: 1px;">
<button id="launch-recovery-submit" class="launch-pin-submit" onclick="submitRecoveryCredential()">Verify & Reset PIN</button>
<p id="launch-recovery-error" class="launch-pin-error" style="display: none;"></p>
<button class="launch-pin-forgot" onclick="showPinEntryView()">← Back to PIN</button>
</div>
</div>
</div>
<!-- Login Screen (username/password mode) -->
<div id="login-overlay" class="launch-pin-overlay" style="display: none;">
<div class="launch-pin-container">
<!-- Sign-in view -->
<div id="login-entry">
<div class="launch-pin-icon">🔐</div>
<h2 class="launch-pin-title">Sign in to SoulSync</h2>
<p class="launch-pin-subtitle">Enter your account name and password</p>
<input type="text" id="login-username" class="launch-pin-input" placeholder="Username" autocomplete="username" maxlength="40" style="margin-bottom: 8px; letter-spacing: normal;">
<input type="password" id="login-password" class="launch-pin-input" placeholder="Password" autocomplete="current-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitLogin()">
<button id="login-submit" class="launch-pin-submit" onclick="submitLogin()">Sign in</button>
<p id="login-error" class="launch-pin-error" style="display: none;"></p>
<button class="launch-pin-forgot" onclick="showLoginRecovery()">Forgot password?</button>
</div>
<!-- Recovery view -->
<div id="login-recovery" style="display: none;">
<div class="launch-pin-icon">🔑</div>
<h2 class="launch-pin-title">Reset your password</h2>
<p class="launch-pin-subtitle">Answer your recovery question</p>
<input type="text" id="recovery-username" class="launch-pin-input" placeholder="Username" autocomplete="username" maxlength="40" style="margin-bottom: 8px; letter-spacing: normal;">
<button id="recovery-fetch-btn" class="launch-pin-submit" onclick="fetchRecoveryQuestion()">Continue</button>
<div id="recovery-answer-section" style="display: none;">
<p id="recovery-question-text" class="launch-pin-subtitle" style="margin-top: 12px; font-weight: 600;"></p>
<input type="text" id="recovery-answer" class="launch-pin-input" placeholder="Your answer" autocomplete="off" maxlength="120" style="margin-bottom: 8px; letter-spacing: normal;">
<input type="password" id="recovery-new-password" class="launch-pin-input" placeholder="New password (min 6)" autocomplete="new-password" maxlength="200" style="margin-bottom: 8px; letter-spacing: normal;">
<input type="password" id="recovery-new-password-confirm" class="launch-pin-input" placeholder="Confirm new password" autocomplete="new-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitRecoveryReset()">
<button class="launch-pin-submit" onclick="submitRecoveryReset()">Reset password</button>
</div>
<p id="recovery-error" class="launch-pin-error" style="display: none;"></p>
<button class="launch-pin-forgot" onclick="showLoginEntry()">← Back to sign in</button>
</div>
</div>
</div>
<!-- Profile Picker Overlay -->
<div id="profile-picker-overlay" class="profile-picker-overlay" style="display: none;">
<div class="profile-picker-container">
<h2 class="profile-picker-title">Who's listening?</h2>
<div id="profile-picker-grid" class="profile-picker-grid"></div>
<div id="profile-picker-actions" class="profile-picker-actions" style="display: none;">
<button id="manage-profiles-btn" class="profile-manage-btn">Manage Profiles</button>
</div>
</div>
<!-- PIN Dialog -->
<div id="profile-pin-dialog" class="profile-pin-dialog" style="display: none;">
<div class="profile-pin-content">
<div id="profile-pin-avatar" class="profile-pin-avatar"></div>
<p id="profile-pin-name" class="profile-pin-name"></p>
<input type="password" id="profile-pin-input" class="profile-pin-input" maxlength="6" placeholder="Enter PIN" autocomplete="off">
<div class="profile-pin-buttons">
<button id="profile-pin-cancel" class="profile-pin-cancel">Cancel</button>
<button id="profile-pin-submit" class="profile-pin-submit">Submit</button>
</div>
<p id="profile-pin-error" class="profile-pin-error" style="display: none;"></p>
<a href="#" id="profile-pin-forgot" class="profile-pin-forgot" onclick="event.preventDefault(); showProfileForgotPin()">Forgot PIN?</a>
</div>
</div>
<!-- Profile Management Panel -->
<div id="profile-manage-panel" class="profile-manage-panel" style="display: none;">
<div class="profile-manage-content">
<div class="profile-manage-header">
<h3>Manage Profiles<small>Create, edit & control access for everyone on this instance</small></h3>
<button id="profile-manage-close" class="profile-manage-close-btn">&times;</button>
</div>
<div id="profile-manage-list" class="profile-manage-list"></div>
<div class="profile-manage-add">
<h4>Add Profile</h4>
<input type="text" id="new-profile-name" class="profile-input" placeholder="Profile name" maxlength="20">
<input type="url" id="new-profile-avatar-url" class="profile-input" placeholder="Avatar image URL (optional)">
<div class="profile-color-picker">
<span class="profile-color-swatch" data-color="#6366f1" style="background:#6366f1" title="Indigo"></span>
<span class="profile-color-swatch" data-color="#ec4899" style="background:#ec4899" title="Pink"></span>
<span class="profile-color-swatch" data-color="#10b981" style="background:#10b981" title="Green"></span>
<span class="profile-color-swatch" data-color="#f59e0b" style="background:#f59e0b" title="Amber"></span>
<span class="profile-color-swatch" data-color="#3b82f6" style="background:#3b82f6" title="Blue"></span>
<span class="profile-color-swatch" data-color="#ef4444" style="background:#ef4444" title="Red"></span>
<span class="profile-color-swatch" data-color="#8b5cf6" style="background:#8b5cf6" title="Purple"></span>
<span class="profile-color-swatch" data-color="#14b8a6" style="background:#14b8a6" title="Teal"></span>
</div>
<input type="password" id="new-profile-pin" class="profile-input" placeholder="PIN (optional)" maxlength="6">
<input type="password" id="new-profile-password" class="profile-input" placeholder="Login password (required when login mode is on)" autocomplete="new-password">
<div class="profile-settings-section">
<label class="profile-settings-label">Home Page</label>
<select id="new-profile-home-page" class="profile-input">
<option value="">Default (Discover)</option>
<option value="dashboard">Dashboard</option>
<option value="sync">Sync</option>
<option value="search">Search</option>
<option value="discover">Discover</option>
<option value="watchlist">Watchlist</option>
<option value="wishlist">Wishlist</option>
<option value="automations">Automations</option>
<option value="active-downloads">Downloads</option>
<option value="library">Library</option>
<option value="stats">Listening Stats</option>
<option value="playlist-explorer">Playlist Explorer</option>
<option value="import">Import</option>
<option value="help">Help & Docs</option>
</select>
<label class="profile-settings-label">Page Access</label>
<div id="new-profile-allowed-pages" class="profile-page-checkboxes">
<label><input type="checkbox" value="dashboard" checked> Dashboard</label>
<label><input type="checkbox" value="sync" checked> Sync</label>
<label><input type="checkbox" value="search" checked> Search</label>
<label><input type="checkbox" value="discover" checked> Discover</label>
<label><input type="checkbox" value="watchlist" checked> Watchlist</label>
<label><input type="checkbox" value="wishlist" checked> Wishlist</label>
<label><input type="checkbox" value="automations" checked> Automations</label>
<label><input type="checkbox" value="active-downloads" checked> Downloads</label>
<label><input type="checkbox" value="library" checked> Library</label>
<label><input type="checkbox" value="stats" checked> Listening Stats</label>
<label><input type="checkbox" value="playlist-explorer" checked> Playlist Explorer</label>
<label><input type="checkbox" value="import" checked> Import</label>
<label><input type="checkbox" value="help" checked disabled> Help & Docs</label>
<label><input type="checkbox" value="issues" checked disabled> Issues</label>
</div>
<label class="profile-checkbox-label">
<input type="checkbox" id="new-profile-can-download" checked> Can download music
</label>
</div>
<button id="create-profile-btn" class="btn btn--block btn--primary profile-create-btn">Create Profile</button>
</div>
<div id="admin-pin-section" class="admin-pin-section" style="display: none;">
<h4>Admin PIN</h4>
<p class="admin-pin-note">Required when multiple profiles exist</p>
<input type="password" id="admin-pin-input" class="profile-input" placeholder="Set admin PIN" maxlength="6">
<button id="set-admin-pin-btn" class="btn btn--block btn--primary profile-create-btn">Set Admin PIN</button>
</div>
</div>
</div>
</div>
<!-- Personal Settings Modal -->
<div id="personal-settings-overlay" class="personal-settings-overlay" style="display: none;" onclick="if(event.target===this)closePersonalSettings()">
<div class="personal-settings-container">
<div class="personal-settings-header">
<h3>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
My Settings
</h3>
<button class="personal-settings-close" onclick="closePersonalSettings()">&times;</button>
</div>
<div class="personal-settings-body" id="personal-settings-body">
<!-- Populated dynamically by JS -->
</div>
</div>
</div>
<div class="main-container">
<!-- Mobile Navigation -->
<button class="hamburger-btn" id="hamburger-btn" aria-label="Toggle navigation">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
<div class="mobile-overlay" id="mobile-overlay"></div>
<!-- Sidebar - Always Visible -->
<div class="sidebar">
<!-- Header Section -->
<div class="sidebar-header">
<div class="sidebar-brand">
<img src="/static/trans2.png" alt="SoulSync" class="sidebar-logo">
<div class="sidebar-brand-text">
<h1 class="app-name">SoulSync</h1>
<p class="app-subtitle">Music Sync & Manager</p>
</div>
</div>
<div id="profile-indicator" class="profile-indicator" style="display: none;" title="Switch profile">
<div id="profile-indicator-avatar" class="profile-indicator-avatar"></div>
<span id="profile-indicator-name" class="profile-indicator-name"></span>
<button id="my-accounts-btn" class="personal-settings-trigger" onclick="event.stopPropagation(); openMyAccountsModal()" title="My Accounts">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</button>
<button id="personal-settings-btn" class="personal-settings-trigger" onclick="event.stopPropagation(); openPersonalSettings()" title="My Settings">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
<button id="logout-btn" class="personal-settings-trigger" style="display:none" onclick="event.stopPropagation(); soulsyncLogout()" title="Sign out">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</div>
</div>
<!-- Navigation Section -->
<nav class="sidebar-nav">
<a class="nav-button" data-page="dashboard" href="/dashboard">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="4" rx="1"/><rect x="3" y="14" width="7" height="4" rx="1"/><rect x="14" y="11" width="7" height="7" rx="1"/><line x1="5" y1="20" x2="5" y2="22"/><line x1="8" y1="19" x2="8" y2="22"/></svg></span>
<span class="nav-text">Dashboard</span>
</a>
<a class="nav-button" data-page="sync" href="/sync">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg></span>
<span class="nav-text">Sync</span>
</a>
<a class="nav-button" data-page="search" href="/search">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><polyline points="8 11 11 14 14 11"/></svg></span>
<span class="nav-text">Search</span>
</a>
<a class="nav-button" data-page="discover" href="/discover">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="currentColor" opacity="0.2" stroke="currentColor"/></svg></span>
<span class="nav-text">Discover</span>
</a>
<a class="nav-button" data-page="playlist-explorer" href="/playlist-explorer">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="3"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="12" x2="5" y2="18"/><line x1="12" y1="12" x2="19" y2="18"/><circle cx="5" cy="19" r="2"/><circle cx="19" cy="19" r="2"/><line x1="12" y1="12" x2="12" y2="18"/><circle cx="12" cy="19" r="2"/></svg></span>
<span class="nav-text">Explorer</span>
</a>
<a class="nav-button" data-page="watchlist" href="/watchlist">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span>
<span class="nav-text">Watchlist</span>
<span class="dl-nav-badge hidden" id="watchlist-nav-badge">0</span>
</a>
<a class="nav-button" data-page="wishlist" href="/wishlist">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></span>
<span class="nav-text">Wishlist</span>
<span class="dl-nav-badge hidden" id="wishlist-nav-badge">0</span>
</a>
<a class="nav-button" data-page="active-downloads" href="/active-downloads">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></span>
<span class="nav-text">Downloads</span>
<span class="dl-nav-badge hidden" id="dl-nav-badge">0</span>
</a>
<a class="nav-button" data-page="automations" href="/automations">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>
<span class="nav-text">Automations</span>
</a>
<a class="nav-button" data-page="import" href="/import">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/><polyline points="7 9 12 4 17 9"/><line x1="12" y1="4" x2="12" y2="16"/></svg></span>
<span class="nav-text">Import</span>
</a>
<a class="nav-button" data-page="library" href="/library">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="14" y2="11"/></svg></span>
<span class="nav-text">Library</span>
</a>
<a class="nav-button" data-page="tools" href="/tools">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span>
<span class="nav-text">Tools</span>
</a>
<a class="nav-button" data-page="stats" href="/stats">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg></span>
<span class="nav-text">Stats</span>
</a>
<a class="nav-button" data-page="settings" href="/settings">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
<span class="nav-text">Settings</span>
</a>
<a class="nav-button" data-page="issues" href="/issues">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></span>
<span class="nav-text">Issues</span>
<span class="issues-nav-badge hidden" id="issues-nav-badge">0</span>
</a>
<a class="nav-button" data-page="help" href="/help">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<span class="nav-text">Help & Docs</span>
</a>
<a class="nav-button" data-page="hydrabase" id="hydrabase-nav" href="/hydrabase" style="display: none;">
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></span>
<span class="nav-text">Hydrabase</span>
</a>
</nav>
<!-- Spacer -->
<div class="sidebar-spacer"></div>
<!-- Support Button -->
<div class="support-section">
<button class="support-button" onclick="showSupportModal()">Support SoulSync</button>
</div>
<!-- Version Section -->
<div class="version-section">
<button class="version-button" onclick="showVersionInfo()">v{{ soulsync_base_version }}</button>
</div>
<!-- Status Section -->
<div class="status-section status-section--clickable" onclick="openServiceSwitchModal('metadata')" title="Change active sources for your profile" role="button" tabindex="0">
<h4 class="status-title">Service Status</h4>
<div class="status-indicator" id="metadata-source-indicator" data-status-ready="false" onclick="event.stopPropagation(); openServiceSwitchModal('metadata')">
<span class="status-dot disconnected"></span>
<span class="status-name" id="metadata-source-name">Metadata Source</span>
</div>
<div class="status-indicator" id="media-server-indicator" data-status-ready="false" onclick="event.stopPropagation(); openServiceSwitchModal('server')">
<span class="status-dot disconnected"></span>
<span class="status-name" id="media-server-name">Media Server</span>
</div>
<div class="status-indicator" id="soulseek-indicator" data-status-ready="false" onclick="event.stopPropagation(); openServiceSwitchModal('download')">
<span class="status-dot disconnected"></span>
<span class="status-name" id="download-source-name">Download Source</span>
</div>
</div>
</div>
<!-- Sidebar Audio Visualizer (right edge, outside sidebar to avoid overflow clip) -->
<div class="sidebar-visualizer" id="sidebar-visualizer"></div>
<!-- Main Content Area -->
<div class="main-content">
<!-- Global particle canvas for page background animations -->
<canvas id="page-particles-canvas"></canvas>
<div id="webui-react-root" class="page" data-react-app="router"></div>
<!-- Dashboard Page -->
<div class="page" id="dashboard-page">
<div class="page-shell dashboard-container">
<div class="dashboard-header"><div class="dashboard-header-sweep" aria-hidden="true"><span></span></div>
<div class="header-text">
<h2 class="header-title"><img src="/static/dashboard.png" class="page-header-icon" alt=""><span>System Dashboard</span></h2>
<p class="header-subtitle">Monitor your music system health and manage operations</p>
</div>
<div class="header-actions">
<!-- MusicBrainz Enrichment Status Icon -->
<div class="mb-button-container">
<button class="musicbrainz-button" id="musicbrainz-button"
title="MusicBrainz Library Enrichment">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/MusicBrainz_Logo_%282016%29.svg/500px-MusicBrainz_Logo_%282016%29.svg.png"
alt="MusicBrainz" class="mb-logo">
<div class="mb-spinner"></div>
</button>
<!-- MusicBrainz Hover Tooltip -->
<div class="musicbrainz-tooltip" id="musicbrainz-tooltip">
<div class="tooltip-content">
<div class="tooltip-header">🎵 MusicBrainz Enrichment</div>
<div class="tooltip-body" id="mb-tooltip-body">
<div class="tooltip-status">Status: <span id="mb-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="mb-tooltip-current">No active matches</div>
<div class="tooltip-progress" id="mb-tooltip-progress">Progress: 0 / 0</div>
</div>
</div>
</div>
</div>
<!-- AudioDB Enrichment Status Icon -->
<div class="audiodb-button-container">
<button class="audiodb-button" id="audiodb-button" title="AudioDB Artist Enrichment">
<img src='data:image/png;base64,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'
alt="AudioDB" class="audiodb-logo">
<div class="audiodb-spinner"></div>
</button>
<!-- AudioDB Hover Tooltip -->
<div class="audiodb-tooltip" id="audiodb-tooltip">
<div class="audiodb-tooltip-content">
<div class="audiodb-tooltip-header">🎶 AudioDB Enrichment</div>
<div class="audiodb-tooltip-body" id="audiodb-tooltip-body">
<div class="tooltip-status">Status: <span
id="audiodb-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="audiodb-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="audiodb-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Deezer Enrichment Status Icon -->
<div class="deezer-button-container">
<button class="deezer-button" id="deezer-button" title="Deezer Library Enrichment">
<img src="https://cdn.brandfetch.io/idEUKgCNtu/theme/dark/symbol.svg?c=1bxid64Mup7aczewSAYMX&t=1758260798610"
alt="Deezer" class="deezer-logo">
<div class="deezer-spinner"></div>
</button>
<!-- Deezer Hover Tooltip -->
<div class="deezer-tooltip" id="deezer-tooltip">
<div class="deezer-tooltip-content">
<div class="deezer-tooltip-header">🎧 Deezer Enrichment</div>
<div class="deezer-tooltip-body" id="deezer-tooltip-body">
<div class="tooltip-status">Status: <span
id="deezer-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="deezer-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="deezer-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Spotify Enrichment Status Icon -->
<div class="spotify-enrich-button-container">
<button class="spotify-enrich-button" id="spotify-enrich-button" title="Spotify Library Enrichment">
<img src="https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png"
alt="Spotify" class="spotify-enrich-logo">
<div class="spotify-enrich-spinner"></div>
</button>
<!-- Spotify Hover Tooltip -->
<div class="spotify-enrich-tooltip" id="spotify-enrich-tooltip">
<div class="spotify-enrich-tooltip-content">
<div class="spotify-enrich-tooltip-header">Spotify Enrichment</div>
<div class="spotify-enrich-tooltip-body" id="spotify-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="spotify-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="spotify-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="spotify-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- iTunes Enrichment Status Icon -->
<div class="itunes-enrich-button-container">
<button class="itunes-enrich-button" id="itunes-enrich-button" title="iTunes Library Enrichment">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/ITunes_logo.svg/960px-ITunes_logo.svg.png"
alt="iTunes" class="itunes-enrich-logo">
<div class="itunes-enrich-spinner"></div>
</button>
<!-- iTunes Hover Tooltip -->
<div class="itunes-enrich-tooltip" id="itunes-enrich-tooltip">
<div class="itunes-enrich-tooltip-content">
<div class="itunes-enrich-tooltip-header">iTunes Enrichment</div>
<div class="itunes-enrich-tooltip-body" id="itunes-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="itunes-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="itunes-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="itunes-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Last.fm Enrichment Status Icon -->
<div class="lastfm-enrich-button-container">
<button class="lastfm-enrich-button" id="lastfm-enrich-button" title="Last.fm Library Enrichment">
<img src="https://www.last.fm/static/images/lastfm_avatar_twitter.52a5d69a85ac.png"
alt="Last.fm" class="lastfm-enrich-logo">
<div class="lastfm-enrich-spinner"></div>
</button>
<div class="lastfm-enrich-tooltip" id="lastfm-enrich-tooltip">
<div class="lastfm-enrich-tooltip-content">
<div class="lastfm-enrich-tooltip-header">Last.fm Enrichment</div>
<div class="lastfm-enrich-tooltip-body" id="lastfm-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="lastfm-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="lastfm-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="lastfm-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Genius Enrichment Status Icon -->
<div class="genius-enrich-button-container">
<button class="genius-enrich-button" id="genius-enrich-button" title="Genius Library Enrichment">
<img src="https://images.genius.com/8ed669cadd956443e29c70361ec4f372.1000x1000x1.png"
alt="Genius" class="genius-enrich-logo">
<div class="genius-enrich-spinner"></div>
</button>
<div class="genius-enrich-tooltip" id="genius-enrich-tooltip">
<div class="genius-enrich-tooltip-content">
<div class="genius-enrich-tooltip-header">Genius Enrichment</div>
<div class="genius-enrich-tooltip-body" id="genius-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="genius-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="genius-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="genius-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Tidal Enrichment Status Icon -->
<div class="tidal-enrich-button-container">
<button class="tidal-enrich-button" id="tidal-enrich-button" title="Tidal Library Enrichment">
<img src="https://www.svgrepo.com/show/519734/tidal.svg"
alt="Tidal" class="tidal-enrich-logo">
<div class="tidal-enrich-spinner"></div>
</button>
<div class="tidal-enrich-tooltip" id="tidal-enrich-tooltip">
<div class="tidal-enrich-tooltip-content">
<div class="tidal-enrich-tooltip-header">Tidal Enrichment</div>
<div class="tidal-enrich-tooltip-body" id="tidal-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="tidal-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="tidal-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="tidal-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Qobuz Enrichment Status Icon -->
<div class="qobuz-enrich-button-container">
<button class="qobuz-enrich-button" id="qobuz-enrich-button" title="Qobuz Library Enrichment">
<img src="https://www.svgrepo.com/show/504778/qobuz.svg"
alt="Qobuz" class="qobuz-enrich-logo">
<div class="qobuz-enrich-spinner"></div>
</button>
<div class="qobuz-enrich-tooltip" id="qobuz-enrich-tooltip">
<div class="qobuz-enrich-tooltip-content">
<div class="qobuz-enrich-tooltip-header">Qobuz Enrichment</div>
<div class="qobuz-enrich-tooltip-body" id="qobuz-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="qobuz-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="qobuz-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="qobuz-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Discogs Enrichment Status Icon -->
<div class="discogs-button-container">
<button class="discogs-button" id="discogs-button" title="Discogs Library Enrichment"
onclick="toggleDiscogsEnrichment()">
<img src="https://www.svgrepo.com/show/305957/discogs.svg"
alt="Discogs" class="discogs-logo">
<div class="discogs-spinner"></div>
</button>
<div class="discogs-tooltip" id="discogs-tooltip">
<div class="discogs-tooltip-content">
<div class="discogs-tooltip-header">Discogs Enrichment</div>
<div class="discogs-tooltip-body" id="discogs-tooltip-body">
<div class="tooltip-status">Status: <span
id="discogs-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="discogs-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="discogs-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Amazon Music Enrichment Status Icon -->
<div class="amazon-enrich-button-container">
<button class="amazon-enrich-button" id="amazon-enrich-button" title="Amazon Music Library Enrichment"
onclick="toggleAmazonEnrichment()">
<img src="/static/amazon.svg"
alt="Amazon Music" class="amazon-enrich-logo">
<div class="amazon-enrich-spinner"></div>
</button>
<div class="amazon-enrich-tooltip" id="amazon-enrich-tooltip">
<div class="amazon-enrich-tooltip-content">
<div class="amazon-enrich-tooltip-header">Amazon Music Enrichment</div>
<div class="amazon-enrich-tooltip-body" id="amazon-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="amazon-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="amazon-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="amazon-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Similar Artists (MusicMap) Enrichment Status Icon -->
<div class="similar-artists-enrich-button-container">
<button class="similar-artists-enrich-button" id="similar-artists-enrich-button" title="Similar Artists (MusicMap) Enrichment">
<img src="https://www.music-map.com/elements/objects/og_logo.png"
alt="Similar Artists" class="similar-artists-enrich-logo">
<div class="similar-artists-enrich-spinner"></div>
</button>
<div class="similar-artists-enrich-tooltip" id="similar-artists-enrich-tooltip">
<div class="similar-artists-enrich-tooltip-content">
<div class="similar-artists-enrich-tooltip-header">Similar Artists Enrichment</div>
<div class="similar-artists-enrich-tooltip-body" id="similar-artists-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="similar-artists-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="similar-artists-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="similar-artists-enrich-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- Hydrabase P2P Mirror Status Icon -->
<div class="hydrabase-button-container" id="hydrabase-button-container" style="display: none;">
<button class="hydrabase-button" id="hydrabase-button" title="Hydrabase P2P Mirror">
<img src="/static/hydrabase.png" alt="Hydrabase" class="hydrabase-worker-logo">
<div class="hydrabase-spinner"></div>
</button>
<!-- Hydrabase Worker Tooltip -->
<div class="hydrabase-tooltip" id="hydrabase-tooltip">
<div class="hydrabase-tooltip-content">
<div class="hydrabase-tooltip-header">Hydrabase P2P Mirror</div>
<div class="hydrabase-tooltip-body" id="hydrabase-tooltip-body">
<div class="tooltip-status">Status: <span id="hydrabase-tooltip-status">Active</span></div>
</div>
</div>
</div>
</div>
<!-- Library Repair Worker Status Icon -->
<div class="repair-button-container">
<button class="repair-button" id="repair-button" title="Library Maintenance">
<img src="/static/whisoul.png" alt="Repair" class="repair-logo">
<div class="repair-spinner"></div>
<span class="repair-badge" id="repair-findings-badge" style="display:none">0</span>
</button>
<!-- Repair Worker Tooltip -->
<div class="repair-tooltip" id="repair-tooltip">
<div class="repair-tooltip-content">
<div class="repair-tooltip-header">🔧 Library Repair</div>
<div class="repair-tooltip-body" id="repair-tooltip-body">
<div class="tooltip-status">Status: <span
id="repair-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="repair-tooltip-current">No active repairs
</div>
<div class="tooltip-progress" id="repair-tooltip-progress">Progress: 0 / 0
</div>
</div>
</div>
</div>
</div>
<!-- SoulID Worker Status Icon -->
<div class="soulid-button-container">
<button class="soulid-button" id="soulid-button" title="SoulID Generator">
<img src="/static/trans2.png" alt="SoulID" class="soulid-logo">
<div class="soulid-spinner"></div>
</button>
<div class="soulid-tooltip" id="soulid-tooltip">
<div class="soulid-tooltip-content">
<div class="soulid-tooltip-header">SoulID Generator</div>
<div class="soulid-tooltip-body" id="soulid-tooltip-body">
<div class="tooltip-status">Status: <span
id="soulid-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="soulid-tooltip-current">No items processing
</div>
<div class="tooltip-progress" id="soulid-tooltip-progress">Progress: 0 pending
</div>
</div>
</div>
</div>
</div>
<!-- Manage Enrichment Workers — opens the full management modal -->
<button class="em-manage-btn" id="manage-enrichment-btn"
title="Manage enrichment workers — stats, unmatched items, manual matching"
onclick="openEnrichmentManager()">
<span class="em-manage-btn-icon"><img src="/static/trans2.png" alt="SoulSync" class="em-manage-btn-logo"></span>
<span class="em-manage-btn-label">Manage Workers</span>
</button>
</div>
<!-- Watchlist / Wishlist quick-nav (top-right corner) -->
<div class="header-quick-nav">
<button class="header-button watchlist-button" id="watchlist-button">
<span class="hero-btn-icon">👁️</span>
<span class="hero-btn-label">Watchlist</span>
<span class="hero-btn-badge" id="watchlist-badge">0</span>
<span class="hero-btn-shimmer"></span>
</button>
<button class="header-button wishlist-button" id="wishlist-button">
<span class="hero-btn-icon">🎵</span>
<span class="hero-btn-label">Wishlist</span>
<span class="hero-btn-badge" id="wishlist-badge">0</span>
<span class="hero-btn-shimmer"></span>
</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
Bento dashboard — bento-style mixed-size cards. Each card has a
bold title + short subtitle + body region + optional actions.
Layout flows in a 3-column grid; cards span 1/2/3 columns to
create visual hierarchy. All existing IDs + child classes
preserved so JS that updates the dashboard keeps working.
═══════════════════════════════════════════════════════════════ -->
<div class="dash-grid">
<!-- Card: Service Status (wide, top-left) -->
<article class="dash-card" data-card="services">
<header class="dash-card__head">
<h3 class="dash-card__title">Service Status</h3>
<p class="dash-card__sub">Connection health for every service SoulSync uses.</p>
</header>
<div class="dash-card__body">
<div class="service-status-grid">
<div class="service-card" id="metadata-source-service-card" data-status-ready="false">
<div class="service-card-header">
<span class="service-card-title" id="metadata-source-title">Metadata Source</span>
<span class="service-card-indicator disconnected" id="metadata-source-status-indicator"></span>
</div>
<p class="service-card-status-text" id="metadata-source-status-text">Disconnected</p>
<p class="service-card-response-time" id="metadata-source-response-time">Response: --</p>
<div class="service-card-footer">
<button class="service-card-button" onclick="testDashboardConnection(getActiveMetadataSource())">Test</button>
</div>
</div>
<div class="service-card" id="media-server-service-card" data-status-ready="false">
<div class="service-card-header">
<span class="service-card-title" id="media-server-service-name">Media Server</span>
<span class="service-card-indicator disconnected" id="media-server-status-indicator"></span>
</div>
<p class="service-card-status-text" id="media-server-status-text">Disconnected</p>
<p class="service-card-response-time" id="media-server-response-time">Response: --</p>
<div class="service-card-footer">
<button class="service-card-button" onclick="testDashboardConnection('server')">Test</button>
</div>
</div>
<div class="service-card" id="soulseek-service-card" data-status-ready="false">
<div class="service-card-header">
<span class="service-card-title" id="download-source-title">Download Source</span>
<span class="service-card-indicator disconnected" id="soulseek-status-indicator"></span>
</div>
<p class="service-card-status-text" id="soulseek-status-text">Disconnected</p>
<p class="service-card-response-time" id="soulseek-response-time">Response: --</p>
<div class="service-card-footer">
<button class="service-card-button" onclick="testDashboardConnection('soulseek')">Test</button>
</div>
</div>
</div>
<div class="enrichment-section" id="enrichment-pills-section" style="display:none">
<div class="enrichment-section-header">
<span class="enrichment-section-label">Enrichment Services</span>
</div>
<div class="enrichment-status-grid" id="enrichment-status-grid">
<!-- Legacy pills — hidden when rate monitor active -->
</div>
</div>
</div>
</article>
<!-- Card: System Stats (row 1, between Services and Library) -->
<article class="dash-card" data-card="stats">
<header class="dash-card__head">
<h3 class="dash-card__title">System Stats</h3>
<p class="dash-card__sub">Live performance metrics.</p>
</header>
<div class="dash-card__body">
<div class="stats-grid-dashboard">
<div class="stat-card-dashboard" id="active-downloads-card">
<p class="stat-card-title">Active Downloads</p>
<p class="stat-card-value">0</p>
<p class="stat-card-subtitle">Currently downloading</p>
</div>
<div class="stat-card-dashboard" id="finished-downloads-card">
<p class="stat-card-title">Finished Downloads</p>
<p class="stat-card-value">0</p>
<p class="stat-card-subtitle">Completed this session</p>
</div>
<div class="stat-card-dashboard" id="download-speed-card">
<p class="stat-card-title">Download Speed</p>
<p class="stat-card-value">0 KB/s</p>
<p class="stat-card-subtitle">Combined speed</p>
</div>
<div class="stat-card-dashboard" id="active-syncs-card">
<p class="stat-card-title">Active Syncs</p>
<p class="stat-card-value">0</p>
<p class="stat-card-subtitle">Playlists syncing</p>
</div>
<div class="stat-card-dashboard" id="uptime-card">
<p class="stat-card-title">System Uptime</p>
<p class="stat-card-value">0m</p>
<p class="stat-card-subtitle">Application runtime</p>
</div>
<div class="stat-card-dashboard" id="memory-card">
<p class="stat-card-title">Memory Usage</p>
<p class="stat-card-value">--</p>
<p class="stat-card-subtitle">Current usage</p>
</div>
</div>
</div>
</article>
<!-- Card: Library (row 1) -->
<article class="dash-card" data-card="library">
<header class="dash-card__head">
<h3 class="dash-card__title">Library</h3>
<p class="dash-card__sub">Your collection at a glance.</p>
</header>
<div class="dash-card__body">
<div class="library-status-card" id="library-status-card">
<div class="library-status-glow"></div>
<div class="library-status-header">
<div class="library-status-icon" id="library-status-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="14" y2="11"/></svg>
</div>
<div class="library-status-info">
<h4 class="library-status-title" id="library-status-title">Library</h4>
<p class="library-status-subtitle" id="library-status-subtitle">Checking status...</p>
</div>
<div class="library-status-actions" id="library-status-actions">
<button class="library-status-btn" id="library-status-scan-btn" style="display: none;" onclick="dashboardLibraryScan(false)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
<span id="library-status-scan-label">Refresh</span>
</button>
<button class="library-status-btn library-status-btn-secondary" id="library-status-deep-btn" style="display: none;" onclick="dashboardLibraryDeepScan()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
Deep Scan
</button>
</div>
</div>
<div class="library-status-stats" id="library-status-stats" style="display: none;">
<div class="library-status-stat">
<div class="library-status-stat-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="library-status-stat-text">
<span class="library-status-stat-value" id="library-status-artists">0</span>
<span class="library-status-stat-label">Artists</span>
</div>
</div>
<div class="library-status-stat">
<div class="library-status-stat-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</div>
<div class="library-status-stat-text">
<span class="library-status-stat-value" id="library-status-albums">0</span>
<span class="library-status-stat-label">Albums</span>
</div>
</div>
<div class="library-status-stat">
<div class="library-status-stat-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</div>
<div class="library-status-stat-text">
<span class="library-status-stat-value" id="library-status-tracks">0</span>
<span class="library-status-stat-label">Tracks</span>
</div>
</div>
<div class="library-status-stat">
<div class="library-status-stat-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
</div>
<div class="library-status-stat-text">
<span class="library-status-stat-value" id="library-status-size">--</span>
<span class="library-status-stat-label">DB Size</span>
</div>
</div>
</div>
<div class="library-status-progress" id="library-status-progress" style="display: none;">
<div class="library-status-phase" id="library-status-phase">Scanning...</div>
<div class="library-status-bar">
<div class="library-status-bar-fill" id="library-status-bar-fill" style="width: 0%;"></div>
</div>
<div class="library-status-progress-detail" id="library-status-progress-detail">0 / 0</div>
</div>
<div class="library-status-message" id="library-status-message" style="display: none;"></div>
</div>
</div>
</article>
<!-- Card: Recent Syncs (row 2) -->
<article class="dash-card" data-card="syncs">
<header class="dash-card__head">
<h3 class="dash-card__title">Recent Syncs</h3>
<p class="dash-card__sub">Playlists you've synced.</p>
</header>
<div class="dash-card__body">
<div class="sync-history-cards" id="sync-history-cards">
<!-- Dynamically populated by JS -->
</div>
</div>
</article>
<!-- Card: Quick Actions launcher — asymmetric bento.
Auto-Sync hero spans both rows on the left; Tools + Automations
stack on the right. Each tile has its own signature animation. -->
<article class="dash-card dash-card--quick-actions" data-card="tools">
<header class="dash-card__head">
<h3 class="dash-card__title">Quick Actions</h3>
<p class="dash-card__sub">Three control rooms inside SoulSync.</p>
</header>
<div class="dash-card__body qa-bento">
<button class="qa-tile qa-tile--hero qa-tile--sync" onclick="openAutoSyncScheduleModal()" aria-label="Open Auto-Sync">
<div class="qa-tile__bg" aria-hidden="true">
<div class="qa-tile__eq">
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<div class="qa-tile__topline">
<span class="qa-tile__pulse" aria-hidden="true"></span>
<span class="qa-tile__kicker">Playlist pipeline</span>
</div>
<div class="qa-tile__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15.5-6.2"/><path d="M18.5 3.5v5h-5"/><path d="M21 12a9 9 0 0 1-15.5 6.2"/><path d="M5.5 20.5v-5h5"/></svg>
</div>
<div class="qa-tile__heading">
<strong class="qa-tile__title">Auto-Sync</strong>
<p class="qa-tile__desc">Refresh, discover, sync, wishlist — running on a schedule you set.</p>
</div>
<div class="qa-tile__cta">
<span class="qa-tile__cta-label">Manage Schedule</span>
<span class="qa-tile__cta-arrow" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18"/></svg>
</span>
</div>
</button>
<button class="qa-tile qa-tile--minor qa-tile--tools" onclick="navigateToPage('tools')" aria-label="Open Tools">
<div class="qa-tile__bg" aria-hidden="true">
<div class="qa-tile__gear">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</div>
</div>
<div class="qa-tile__topline">
<span class="qa-tile__kicker">Maintenance</span>
</div>
<div class="qa-tile__heading">
<strong class="qa-tile__title">Tools</strong>
<p class="qa-tile__desc">Database, scanning, repair, backups.</p>
</div>
<div class="qa-tile__cta">
<span class="qa-tile__cta-label">Open</span>
<span class="qa-tile__cta-arrow" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18"/></svg>
</span>
</div>
</button>
<button class="qa-tile qa-tile--minor qa-tile--auto" onclick="navigateToPage('automations')" aria-label="Open Automations">
<div class="qa-tile__bg" aria-hidden="true">
<div class="qa-tile__flow">
<span class="qa-flow-node"></span>
<span class="qa-flow-line"></span>
<span class="qa-flow-node"></span>
<span class="qa-flow-line"></span>
<span class="qa-flow-node"></span>
</div>
</div>
<div class="qa-tile__topline">
<span class="qa-tile__kicker">Trigger → action</span>
</div>
<div class="qa-tile__heading">
<strong class="qa-tile__title">Automations</strong>
<p class="qa-tile__desc">Events, schedules, signals, then-actions.</p>
</div>
<div class="qa-tile__cta">
<span class="qa-tile__cta-label">Open</span>
<span class="qa-tile__cta-arrow" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18"/></svg>
</span>
</div>
</button>
</div>
</article>
<!-- Card: Recent Activity (row 2, after Tools) -->
<article class="dash-card" data-card="activity">
<header class="dash-card__head dash-card__head--withaction">
<div>
<h3 class="dash-card__title">Recent Activity</h3>
<p class="dash-card__sub">What just happened.</p>
</div>
<button class="dash-card__head-btn" onclick="openLibraryHistoryModal()" title="View full library history">Download History</button>
</header>
<div class="dash-card__body">
<div class="activity-feed-container" id="dashboard-activity-feed">
<div class="activity-item">
<span class="activity-icon">📊</span>
<div class="activity-text-content">
<p class="activity-title">System Started</p>
<p class="activity-subtitle">Dashboard initialized successfully</p>
</div>
<p class="activity-time">Now</p>
</div>
</div>
</div>
</article>
<!-- Card: Active Downloads (full-width, shows when downloads in flight) -->
<article class="dash-card dash-card--full" id="dashboard-active-downloads-section" style="display: none;" data-card="active-downloads">
<header class="dash-card__head">
<h3 class="dash-card__title">Active Downloads</h3>
<p class="dash-card__sub">In-flight transfers from your sources.</p>
</header>
<div class="dash-card__body">
<div id="dashboard-downloads-container"></div>
</div>
</article>
<!-- Card: Enrichment Detail (compact, paired with System Stats on the left) -->
<article class="dash-card dash-card--full" id="rate-monitor-section" data-card="enrichment">
<header class="dash-card__head">
<h3 class="dash-card__title">Enrichment Services</h3>
<p class="dash-card__sub">API rate monitoring across providers.</p>
</header>
<div class="dash-card__body">
<div class="rate-monitor-grid" id="rate-monitor-grid">
<!-- Populated dynamically by JS -->
</div>
</div>
</article>
<!-- Card: Recent Syncs (full-width) -->
</div>
</div>
</div>
<!-- Main container for the Sync page -->
<div class="page" id="sync-page">
<div class="page-shell">
<!-- Header -->
<div class="sync-header">
<div class="sync-header-row">
<div>
<h2 class="sync-title"><img src="/static/sync.png" class="page-header-icon" alt=""><span>Playlist Sync</span></h2>
<p class="sync-subtitle">Synchronize your Spotify, Tidal, and YouTube playlists with your media
server</p>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button class="btn btn--sm btn--secondary sync-history-btn auto-sync-manager-btn" onclick="openAutoSyncScheduleModal()" title="Schedule mirrored playlists to refresh, discover, sync, and queue missing tracks">Auto-Sync</button>
<button class="btn btn--sm btn--secondary sync-history-btn" onclick="openManualLibraryMatchTool()" title="Manually link source tracks to library tracks">Library Match</button>
<button class="btn btn--sm btn--secondary sync-history-btn" onclick="openSyncHistoryModal()" title="View sync history">Sync History</button>
<button class="btn btn--sm btn--secondary sync-history-btn" onclick="openDownloadOriginsModal('playlist')" title="See every track your playlist syncs downloaded">Download Origins</button>
</div>
</div>
</div>
<!-- Main two-column content area -->
<div class="sync-content-area">
<!-- Left Panel: Tabbed Playlist Section -->
<div class="sync-main-panel">
<div class="sync-tabs">
<button class="sync-tab-button sync-tab-server active" data-tab="server" title="Server Playlists">
<span class="tab-icon server-icon"></span><span class="sync-tab-label">Server Playlists</span>
</button>
<div class="sync-tab-divider"></div>
<button class="sync-tab-button" data-tab="spotify" title="Spotify">
<span class="tab-icon spotify-icon"></span><span class="sync-tab-label">Spotify</span>
</button>
<button class="sync-tab-button" data-tab="spotify-public" data-link="true" title="Spotify Link">
<span class="tab-icon spotify-icon"></span><span class="sync-tab-label">Spotify Link</span>
</button>
<button class="sync-tab-button" data-tab="itunes-link" data-link="true" title="iTunes Link">
<span class="tab-icon itunes-icon"></span><span class="sync-tab-label">iTunes Link</span>
</button>
<button class="sync-tab-button" data-tab="tidal" title="Tidal">
<span class="tab-icon tidal-icon"></span><span class="sync-tab-label">Tidal</span>
</button>
<button class="sync-tab-button" data-tab="qobuz" title="Qobuz">
<span class="tab-icon qobuz-icon"></span><span class="sync-tab-label">Qobuz</span>
</button>
<button class="sync-tab-button" data-tab="deezer" title="Deezer">
<span class="tab-icon deezer-icon"></span><span class="sync-tab-label">Deezer</span>
</button>
<button class="sync-tab-button" data-tab="deezer-link" data-link="true" title="Deezer Link">
<span class="tab-icon deezer-icon"></span><span class="sync-tab-label">Deezer Link</span>
</button>
<button class="sync-tab-button" data-tab="youtube" title="YouTube">
<span class="tab-icon youtube-icon"></span><span class="sync-tab-label">YouTube</span>
</button>
<button class="sync-tab-button" data-tab="beatport" title="Beatport">
<span class="tab-icon beatport-icon"></span><span class="sync-tab-label">Beatport</span>
</button>
<button class="sync-tab-button" data-tab="listenbrainz-sync" title="ListenBrainz">
<span class="tab-icon listenbrainz-icon"></span><span class="sync-tab-label">ListenBrainz</span>
</button>
<button class="sync-tab-button" data-tab="lastfm-sync" title="Last.fm">
<span class="tab-icon lastfm-icon"></span><span class="sync-tab-label">Last.fm</span>
</button>
<button class="sync-tab-button" data-tab="soulsync-discovery-sync" title="SoulSync Discovery">
<span class="tab-icon soulsync-discovery-icon"></span><span class="sync-tab-label">SoulSync Discovery</span>
</button>
<button class="sync-tab-button" data-tab="import-file" title="Import">
<span class="tab-icon import-file-icon"></span><span class="sync-tab-label">Import</span>
</button>
<button class="sync-tab-button" data-tab="mirrored" title="Mirrored">
<span class="tab-icon mirrored-icon"></span><span class="sync-tab-label">Mirrored</span>
</button>
</div>
<!-- Spotify Tab Content -->
<div class="sync-tab-content" id="spotify-tab-content">
<div class="playlist-header">
<h3>Your Spotify Playlists</h3>
<button class="refresh-button" id="spotify-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="spotify-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your Spotify playlists.</div>
</div>
</div>
<!-- Tidal Tab Content -->
<div class="sync-tab-content" id="tidal-tab-content">
<div class="playlist-header">
<h3>Your Tidal Playlists</h3>
<button class="refresh-button tidal" id="tidal-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="tidal-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your Tidal playlists.</div>
</div>
</div>
<!-- Deezer Tab Content (ARL User Playlists) -->
<div class="sync-tab-content" id="deezer-tab-content">
<div class="playlist-header">
<h3>Your Deezer Playlists</h3>
<button class="refresh-button deezer" id="deezer-arl-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="deezer-arl-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your Deezer playlists.</div>
</div>
</div>
<!-- Qobuz Tab Content -->
<div class="sync-tab-content" id="qobuz-tab-content">
<div class="playlist-header">
<h3>Your Qobuz Playlists</h3>
<button class="refresh-button qobuz" id="qobuz-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="qobuz-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your Qobuz playlists.</div>
</div>
</div>
<!-- Deezer Link Tab Content (URL Import) -->
<div class="sync-tab-content" id="deezer-link-tab-content">
<div class="youtube-input-section">
<input type="text" id="deezer-url-input"
placeholder="Paste Deezer Playlist URL...">
<button id="deezer-parse-btn">Load Playlist</button>
</div>
<div class="url-history-bar" id="deezer-url-history" style="display:none"></div>
<div class="playlist-scroll-container" id="deezer-playlist-container">
<div class="playlist-placeholder">Paste a Deezer playlist URL above to get started.</div>
</div>
</div>
<!-- YouTube Tab Content -->
<div class="sync-tab-content" id="youtube-tab-content">
<div class="youtube-input-section">
<input type="text" id="youtube-url-input"
placeholder="Paste YouTube Music Playlist URL...">
<button id="youtube-parse-btn">Parse Playlist</button>
</div>
<div class="url-history-bar" id="youtube-url-history" style="display:none"></div>
<div class="playlist-scroll-container" id="youtube-playlist-container">
<div class="playlist-placeholder">Parsed YouTube playlists will appear here.</div>
</div>
</div>
<!-- Spotify Public Link Tab Content -->
<div class="sync-tab-content" id="spotify-public-tab-content">
<div class="youtube-input-section">
<input type="text" id="spotify-public-url-input"
placeholder="Paste Spotify Playlist or Album URL...">
<button id="spotify-public-parse-btn">Load</button>
</div>
<div class="url-history-bar" id="spotify-public-url-history" style="display:none"></div>
<div class="playlist-scroll-container" id="spotify-public-playlist-container">
<div class="playlist-placeholder">Paste a Spotify playlist or album URL above to load tracks without needing Spotify API credentials.</div>
</div>
</div>
<!-- iTunes Link Tab Content -->
<div class="sync-tab-content" id="itunes-link-tab-content">
<div class="youtube-input-section">
<input type="text" id="itunes-link-url-input"
placeholder="Paste iTunes or Apple Music Album/Track URL...">
<button id="itunes-link-parse-btn">Load</button>
</div>
<div class="url-history-bar" id="itunes-link-url-history" style="display:none"></div>
<div class="playlist-scroll-container" id="itunes-link-playlist-container">
<div class="playlist-placeholder">Paste an iTunes or Apple Music album/track URL above to load tracks.</div>
</div>
</div>
<!-- Beatport Tab Content -->
<div class="sync-tab-content" id="beatport-tab-content">
<!-- Beatport Nested Tabs (hidden — Browse is the only view) -->
<div class="beatport-tabs" style="display: none;">
<button class="beatport-tab-button active" data-beatport-tab="rebuild">
<span class="tab-icon rebuild-icon"></span> Browse
</button>
<button class="beatport-tab-button" data-beatport-tab="browse">
<span class="tab-icon browse-icon"></span> Browse Charts
</button>
<button class="beatport-tab-button" data-beatport-tab="playlists">
<span class="tab-icon playlist-icon"></span> My Playlists
</button>
</div>
<!-- Browse Charts Tab Content -->
<div class="beatport-tab-content" id="beatport-browse-content">
<div class="beatport-navigation">
<!-- New Homepage Main View -->
<div class="beatport-main-view active" id="beatport-main-view">
<div class="beatport-hero">
<div class="beatport-hero-bg"></div>
<div class="beatport-hero-content">
<h2>Browse Beatport Charts</h2>
<p>Explore top electronic music charts and discover new tracks</p>
<div class="beatport-stats">
<span class="stat-item">39 Genres</span>
<span class="stat-divider"></span>
<span class="stat-item">Top 100</span>
<span class="stat-divider"></span>
<span class="stat-item">Daily Updates</span>
</div>
</div>
</div>
<!-- Genre Explorer Section -->
<div class="homepage-genre-section">
<h3 class="section-title">🎵 Genre Explorer</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-action="show-genres">
<div class="chart-type-icon">🎵</div>
<div class="chart-type-info">
<h3>Browse All Genres</h3>
<p>House, Techno, Trance, and 36 more genres</p>
<span class="track-count">39 Genres</span>
</div>
</div>
</div>
</div>
<!-- Main Charts Section -->
<div class="homepage-main-charts-section">
<h3 class="section-title">📊 Main Charts</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="top-10"
data-chart-endpoint="/api/beatport/top-100">
<div class="chart-type-icon">🔥</div>
<div class="chart-type-info">
<h3>Beatport Top 10</h3>
<p>Current hottest tracks</p>
<span class="track-count">10 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="top-100"
data-chart-endpoint="/api/beatport/top-100">
<div class="chart-type-icon">💯</div>
<div class="chart-type-info">
<h3>Beatport Top 100</h3>
<p>Complete chart rankings</p>
<span class="track-count">100 tracks</span>
</div>
</div>
</div>
</div>
<!-- Releases Section -->
<div class="homepage-releases-section">
<h3 class="section-title">🎵 Releases</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="releases-top-10"
data-chart-endpoint="/api/beatport/homepage/top-10-releases">
<div class="chart-type-icon">🆕</div>
<div class="chart-type-info">
<h3>Top 10 Releases</h3>
<p>Newest releases trending</p>
<span class="track-count">10 releases</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="releases-top-100"
data-chart-endpoint="/api/beatport/top-100-releases">
<div class="chart-type-icon">📊</div>
<div class="chart-type-info">
<h3>Top 100 Releases</h3>
<p>All trending releases</p>
<span class="track-count">100 releases</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="latest-releases"
data-chart-endpoint="/api/beatport/homepage/new-releases">
<div class="chart-type-icon">🕒</div>
<div class="chart-type-info">
<h3>Latest Releases</h3>
<p>Recently published</p>
<span class="track-count">50 releases</span>
</div>
</div>
</div>
</div>
<!-- Hype Section -->
<div class="homepage-hype-section">
<h3 class="section-title">🔥 Hype</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="hype-top-10"
data-chart-endpoint="/api/beatport/hype-top-100">
<div class="chart-type-icon">🚀</div>
<div class="chart-type-info">
<h3>Hype Top 10</h3>
<p>Hottest trending tracks</p>
<span class="track-count">10 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="hype-top-100"
data-chart-endpoint="/api/beatport/hype-top-100">
<div class="chart-type-icon">🔥</div>
<div class="chart-type-info">
<h3>Hype Top 100</h3>
<p>Complete hype chart rankings</p>
<span class="track-count">100 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="hype-picks"
data-chart-endpoint="/api/beatport/homepage/hype-picks">
<div class="chart-type-icon"></div>
<div class="chart-type-info">
<h3>Hype Picks</h3>
<p>Editor selected hype tracks</p>
<span class="track-count">50 tracks</span>
</div>
</div>
</div>
</div>
<!-- DJ Charts Section -->
<div class="homepage-dj-charts-section">
<h3 class="section-title">🎧 DJ Charts Collection</h3>
<p class="section-description">DJ curated chart collections</p>
<div class="charts-loading-inline" id="dj-charts-loading-inline">
<div class="loading-spinner-small"></div>
<p>Loading DJ chart collections...</p>
</div>
<div class="dj-charts-grid" id="dj-charts-grid">
<!-- Charts will be populated here -->
</div>
</div>
<!-- Featured Charts Section -->
<div class="homepage-featured-charts-section">
<h3 class="section-title">⭐ Featured Charts Collection</h3>
<p class="section-description">Editor curated chart collections</p>
<div class="charts-loading-inline" id="featured-charts-loading-inline">
<div class="loading-spinner-small"></div>
<p>Loading featured chart collections...</p>
</div>
<div class="featured-charts-grid" id="featured-charts-grid">
<!-- Charts will be populated here -->
</div>
</div>
</div>
<!-- Genre Explorer Sub-View -->
<div class="beatport-sub-view" id="beatport-genres-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back">← Back to Categories</button>
<span class="breadcrumb-path">Browse Charts > Genre Explorer</span>
</div>
<div class="beatport-genre-grid">
<div class="beatport-genre-item" data-genre-slug="house" data-genre-id="5">
<div class="genre-icon">🏠</div>
<h3>House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="tech-house"
data-genre-id="11">
<div class="genre-icon">🔧</div>
<h3>Tech House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="techno" data-genre-id="6">
<div class="genre-icon"></div>
<h3>Techno</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="deep-house"
data-genre-id="12">
<div class="genre-icon">🌊</div>
<h3>Deep House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="trance" data-genre-id="7">
<div class="genre-icon">🌀</div>
<h3>Trance</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="drum-and-bass"
data-genre-id="1">
<div class="genre-icon">🥁</div>
<h3>Drum & Bass</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="dubstep"
data-genre-id="18">
<div class="genre-icon">🎵</div>
<h3>Dubstep</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="progressive-house"
data-genre-id="15">
<div class="genre-icon">📈</div>
<h3>Progressive House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="melodic-house-and-techno"
data-genre-id="90">
<div class="genre-icon">🎼</div>
<h3>Melodic House & Techno</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="afro-house"
data-genre-id="89">
<div class="genre-icon">🌍</div>
<h3>Afro House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="minimal"
data-genre-id="14">
<div class="genre-icon"></div>
<h3>Minimal</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="nu-disco"
data-genre-id="50">
<div class="genre-icon"></div>
<h3>Nu Disco</h3>
<span class="genre-track-count">Top 100</span>
</div>
</div>
</div>
<!-- Genre Detail Sub-View -->
<div class="beatport-sub-view" id="beatport-genre-detail-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back" id="genre-detail-back">← Back to Genre
Explorer</button>
<span class="breadcrumb-path" id="genre-detail-breadcrumb">Browse Charts >
Genre Explorer > Loading...</span>
</div>
<div class="genre-detail-header">
<div class="genre-detail-info">
<h2 id="genre-detail-title">Loading Genre...</h2>
<p id="genre-detail-description">Explore all chart types for this genre
</p>
</div>
</div>
<!-- Main Chart Types Section -->
<div class="genre-main-charts-section">
<h3 class="section-title">📊 Main Charts</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="top-10">
<div class="chart-type-icon">🔥</div>
<div class="chart-type-info">
<h3 id="genre-top-10-title">Top 10</h3>
<p>Current hottest tracks</p>
<span class="track-count">10 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="top-100">
<div class="chart-type-icon">💯</div>
<div class="chart-type-info">
<h3 id="genre-top-100-title">Top 100</h3>
<p>Complete chart rankings</p>
<span class="track-count">100 tracks</span>
</div>
</div>
</div>
</div>
<!-- Releases Section -->
<div class="genre-releases-section">
<h3 class="section-title">🎵 Releases</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="releases-top-10">
<div class="chart-type-icon">🆕</div>
<div class="chart-type-info">
<h3 id="genre-releases-top-10-title">Top 10 Releases</h3>
<p>Newest releases trending</p>
<span class="track-count">10 releases</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="releases-top-100">
<div class="chart-type-icon">📊</div>
<div class="chart-type-info">
<h3 id="genre-releases-top-100-title">Top 100 Releases</h3>
<p>All trending releases</p>
<span class="track-count">100 releases</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="latest-releases">
<div class="chart-type-icon">🕒</div>
<div class="chart-type-info">
<h3 id="genre-latest-releases-title">Latest Releases</h3>
<p>Recently published</p>
<span class="track-count">50 releases</span>
</div>
</div>
</div>
</div>
<!-- Editorial Section -->
<div class="genre-editorial-section">
<h3 class="section-title">⭐ Editorial</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="staff-picks">
<div class="chart-type-icon"></div>
<div class="chart-type-info">
<h3 id="genre-staff-picks-title">Staff Picks</h3>
<p>Editor curated selection</p>
<span class="track-count">50 tracks</span>
</div>
</div>
</div>
</div>
<!-- Hype Section -->
<div class="genre-hype-section">
<h3 class="section-title">🔥 Hype</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="hype-top-10">
<div class="chart-type-icon">🚀</div>
<div class="chart-type-info">
<h3 id="genre-hype-top-10-title">Hype Top 10</h3>
<p>Hottest trending tracks</p>
<span class="track-count">10 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="hype-top-100">
<div class="chart-type-icon">🔥</div>
<div class="chart-type-info">
<h3 id="genre-hype-top-100-title">Hype Top 100</h3>
<p>Complete hype chart rankings</p>
<span class="track-count">100 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="hype-picks">
<div class="chart-type-icon"></div>
<div class="chart-type-info">
<h3 id="genre-hype-picks-title">Hype Picks</h3>
<p>Editor selected hype tracks</p>
<span class="track-count">50 tracks</span>
</div>
</div>
</div>
</div>
<!-- New Charts Section (Always Visible) -->
<div class="genre-new-charts-section">
<h3 class="section-title">📈 New Charts Collection</h3>
<p class="section-description">Artist and DJ curated chart collections</p>
<!-- Always Visible Charts List -->
<div class="new-charts-content" id="new-charts-content">
<div class="charts-loading-inline" id="charts-loading-inline">
<div class="loading-spinner-small"></div>
<p>Loading chart collections...</p>
</div>
<div class="new-charts-grid" id="new-charts-grid">
<!-- Charts will be populated here -->
</div>
</div>
</div>
</div>
<!-- Genre Charts List Sub-View -->
<div class="beatport-sub-view" id="beatport-genre-charts-list-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back" id="genre-charts-list-back">← Back to Genre
Charts</button>
<span class="breadcrumb-path" id="genre-charts-list-breadcrumb">Browse
Charts > Genre Explorer > Genre Charts > New Charts</span>
</div>
<div class="genre-charts-list-header">
<div class="genre-charts-list-info">
<h2 id="genre-charts-list-title">Loading Charts...</h2>
<p id="genre-charts-list-description">Browse all available chart
collections for this genre</p>
</div>
</div>
<div class="genre-charts-list-container">
<div class="charts-loading-placeholder" id="charts-loading-placeholder">
<div class="loading-spinner"></div>
<p>🔍 Loading chart collections...</p>
</div>
<div class="genre-charts-grid" id="genre-charts-grid">
<!-- Charts will be populated dynamically -->
</div>
</div>
</div>
</div>
</div>
<!-- My Playlists Tab Content -->
<div class="beatport-tab-content" id="beatport-playlists-content">
<div class="playlist-header">
<h3>My Beatport Playlists</h3>
<button class="refresh-button beatport" id="beatport-clear-btn">🗑️ Clear</button>
</div>
<div class="playlist-scroll-container" id="beatport-playlist-container">
<div class="playlist-placeholder">Your created Beatport playlists will appear here.
</div>
</div>
</div>
<!-- Rebuild Tab Content -->
<div class="beatport-tab-content active" id="beatport-rebuild-content">
<div class="beatport-rebuild-slider-container">
<div class="beatport-rebuild-slider" id="beatport-rebuild-slider">
<div class="beatport-rebuild-slider-track" id="beatport-rebuild-slider-track">
<!-- Loading placeholder -->
<div class="beatport-rebuild-loading">
<div class="beatport-rebuild-loading-content">
<h2>🎯 Loading Fresh Beatport Tracks...</h2>
<p>Fetching the latest music from Beatport</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-rebuild-slider-nav">
<button class="beatport-rebuild-nav-btn beatport-rebuild-prev-btn"
id="beatport-rebuild-prev-btn"></button>
<button class="beatport-rebuild-nav-btn beatport-rebuild-next-btn"
id="beatport-rebuild-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-rebuild-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
<!-- Navigation Buttons Section -->
<div class="beatport-nav-buttons-section">
<div class="beatport-nav-buttons-container">
<button class="beatport-nav-button" id="browse-by-genre-btn">
<span class="beatport-nav-icon genre-icon"></span>
<span class="beatport-nav-text">Browse by Genre</span>
</button>
<button class="beatport-nav-button" id="beatport-top100-btn">
<span class="beatport-nav-icon top100-icon"></span>
<span class="beatport-nav-text">Beatport Top 100</span>
</button>
<button class="beatport-nav-button" id="hype-top100-btn">
<span class="beatport-nav-icon hype-icon"></span>
<span class="beatport-nav-text">Hype Top 100</span>
</button>
</div>
</div>
<!-- Beatport Download Bubbles -->
<div id="beatport-downloads-section" class="artist-downloads-section" style="display: none;"></div>
<!-- Top 10 Lists Section -->
<div class="beatport-top10-section">
<div class="beatport-top10-header">
<h2 class="beatport-top10-title">🏆 Top 10 Lists</h2>
<p class="beatport-top10-subtitle">Current trending tracks from Beatport charts
</p>
</div>
<div class="beatport-top10-container">
<!-- Beatport Top 10 List -->
<div class="beatport-top10-list" id="beatport-top10-list">
<div class="beatport-top10-list-header">
<h3 class="beatport-top10-list-title">🎵 Beatport Top 10</h3>
<p class="beatport-top10-list-subtitle">Most popular tracks on Beatport
</p>
</div>
<div class="beatport-top10-tracks" id="beatport-top10-tracks">
<!-- Loading placeholder -->
<div class="beatport-top10-loading">
<div class="beatport-top10-loading-content">
<h4>🎵 Loading Beatport Top 10...</h4>
<p>Fetching trending tracks</p>
</div>
</div>
</div>
</div>
<!-- Hype Top 10 List -->
<div class="beatport-hype10-list" id="beatport-hype10-list">
<div class="beatport-hype10-list-header">
<h3 class="beatport-hype10-list-title">🔥 Hype Top 10</h3>
<p class="beatport-hype10-list-subtitle">Editor's hottest trending picks
</p>
</div>
<div class="beatport-hype10-tracks" id="beatport-hype10-tracks">
<!-- Loading placeholder -->
<div class="beatport-hype10-loading">
<div class="beatport-hype10-loading-content">
<h4>🔥 Loading Hype Top 10...</h4>
<p>Fetching editor's picks</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Top 10 Releases Section -->
<div class="beatport-releases-top10-section">
<div class="beatport-releases-top10-header">
<h2 class="beatport-releases-top10-title">💿 Top 10 Releases</h2>
<p class="beatport-releases-top10-subtitle">Most popular albums and EPs on
Beatport</p>
</div>
<div class="beatport-releases-top10-container">
<div class="beatport-releases-top10-list" id="beatport-releases-top10-list">
<!-- Loading placeholder -->
<div class="beatport-releases-top10-loading">
<div class="beatport-releases-top10-loading-content">
<h4>💿 Loading Top 10 Releases...</h4>
<p>Fetching trending albums and EPs</p>
</div>
</div>
</div>
</div>
</div>
<!-- New Releases Grid Slideshow Section -->
<div class="beatport-releases-section">
<div class="beatport-releases-header">
<h2 class="beatport-releases-title">🆕 New Releases</h2>
<p class="beatport-releases-subtitle">Latest albums and EPs from Beatport</p>
</div>
<div class="beatport-releases-slider-container">
<div class="beatport-releases-slider" id="beatport-releases-slider">
<div class="beatport-releases-slider-track"
id="beatport-releases-slider-track">
<!-- Loading placeholder -->
<div class="beatport-releases-loading">
<div class="beatport-releases-loading-content">
<h3>📀 Loading New Releases...</h3>
<p>Fetching the latest albums and EPs</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-releases-slider-nav">
<button class="beatport-releases-nav-btn beatport-releases-prev-btn"
id="beatport-releases-prev-btn"></button>
<button class="beatport-releases-nav-btn beatport-releases-next-btn"
id="beatport-releases-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-releases-slider-indicators"
id="beatport-releases-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
</div>
<!-- Hype Picks Grid Slideshow Section -->
<div class="beatport-hype-picks-section">
<div class="beatport-hype-picks-header">
<h2 class="beatport-hype-picks-title">🔥 Hype Picks</h2>
<p class="beatport-hype-picks-subtitle">Editor selected trending tracks from
Beatport</p>
</div>
<div class="beatport-hype-picks-slider-container">
<div class="beatport-hype-picks-slider" id="beatport-hype-picks-slider">
<div class="beatport-hype-picks-slider-track"
id="beatport-hype-picks-slider-track">
<!-- Loading placeholder -->
<div class="beatport-hype-picks-loading">
<div class="beatport-hype-picks-loading-content">
<h3>🔥 Loading Hype Picks...</h3>
<p>Fetching the hottest trending tracks</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-hype-picks-slider-nav">
<button class="beatport-hype-picks-nav-btn beatport-hype-picks-prev-btn"
id="beatport-hype-picks-prev-btn"></button>
<button class="beatport-hype-picks-nav-btn beatport-hype-picks-next-btn"
id="beatport-hype-picks-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-hype-picks-slider-indicators"
id="beatport-hype-picks-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
</div>
<!-- Featured Charts Grid Slideshow Section -->
<div class="beatport-charts-section">
<div class="beatport-charts-header">
<h2 class="beatport-charts-title">🔥 Featured Charts</h2>
<p class="beatport-charts-subtitle">Top chart collections from Beatport creators
</p>
</div>
<div class="beatport-charts-slider-container">
<div class="beatport-charts-slider" id="beatport-charts-slider">
<div class="beatport-charts-slider-track" id="beatport-charts-slider-track">
<!-- Loading placeholder -->
<div class="beatport-charts-loading">
<div class="beatport-charts-loading-content">
<h3>📊 Loading Featured Charts...</h3>
<p>Fetching top chart collections</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-charts-slider-nav">
<button class="beatport-charts-nav-btn beatport-charts-prev-btn"
id="beatport-charts-prev-btn"></button>
<button class="beatport-charts-nav-btn beatport-charts-next-btn"
id="beatport-charts-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-charts-slider-indicators"
id="beatport-charts-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
</div>
<!-- DJ Charts Carousel Section -->
<div class="beatport-dj-section">
<div class="beatport-dj-header">
<h2 class="beatport-dj-title">🎧 DJ Charts</h2>
<p class="beatport-dj-subtitle">Curated charts from top DJs and artists</p>
</div>
<div class="beatport-dj-slider-container">
<div class="beatport-dj-slider" id="beatport-dj-slider">
<div class="beatport-dj-slider-track" id="beatport-dj-slider-track">
<!-- Loading placeholder -->
<div class="beatport-dj-loading">
<div class="beatport-dj-loading-content">
<h3>🎧 Loading DJ Charts...</h3>
<p>Fetching curated DJ selections</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-dj-slider-nav">
<button class="beatport-dj-nav-btn beatport-dj-prev-btn"
id="beatport-dj-prev-btn"></button>
<button class="beatport-dj-nav-btn beatport-dj-next-btn"
id="beatport-dj-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-dj-slider-indicators"
id="beatport-dj-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Import File Tab Content -->
<div class="sync-tab-content" id="import-file-tab-content">
<div class="playlist-header">
<h3>Import Playlist from File</h3>
</div>
<!-- Step 1: Upload Zone -->
<div id="import-file-upload-zone" class="import-file-zone">
<div class="import-file-zone-inner" id="import-file-dropzone">
<div class="import-file-zone-icon">📄</div>
<div class="import-file-zone-title">Drop your file here</div>
<div class="import-file-zone-subtitle">or click to browse</div>
<div class="import-file-zone-formats">Supported: CSV, TSV, TXT</div>
<input type="file" id="import-file-input" accept=".csv,.tsv,.txt" style="display:none">
</div>
<div class="import-file-format-hints">
<div class="import-file-hint">
<span class="import-file-hint-label">CSV / TSV</span>
<span class="import-file-hint-text">First row as headers (e.g. Title, Artist, Album). Columns are auto-detected or can be mapped manually.</span>
</div>
<div class="import-file-hint">
<span class="import-file-hint-label">TXT</span>
<span class="import-file-hint-text">One track per line (e.g. Artist - Title). Format and separator can be adjusted after upload.</span>
</div>
</div>
</div>
<!-- Step 2: Column Mapping & Preview (hidden until file is parsed) -->
<div id="import-file-preview-section" style="display:none">
<!-- File info bar -->
<div class="import-file-info-bar">
<span id="import-file-name-label" class="import-file-info-filename"></span>
<span id="import-file-track-count" class="import-file-info-count"></span>
<button class="import-file-clear-btn" onclick="importFileClear()" title="Clear and start over"></button>
</div>
<!-- Format selector (for plain text files) -->
<div id="import-file-text-format" class="import-file-format-bar" style="display:none">
<span class="import-file-format-label">Line format:</span>
<select id="import-file-text-order" class="import-file-select" onchange="importFileReparse()">
<option value="artist-title">Artist - Title</option>
<option value="title-artist">Title - Artist</option>
</select>
<span class="import-file-format-label" style="margin-left:12px">Separator:</span>
<select id="import-file-text-separator" class="import-file-select" onchange="importFileReparse()">
<option value=" - "> - </option>
<option value=" — "></option>
<option value="|">|</option>
<option value="/"> / </option>
</select>
</div>
<!-- Column mapping (for CSV/TSV files) -->
<div id="import-file-column-mapping" class="import-file-mapping-bar" style="display:none">
<span class="import-file-format-label">Column mapping:</span>
<div id="import-file-mapping-selects" class="import-file-mapping-selects">
<!-- Dynamically populated with dropdowns per CSV column -->
</div>
</div>
<!-- Preview table -->
<div class="import-file-preview-table-wrap">
<table class="import-file-preview-table" id="import-file-preview-table">
<thead>
<tr>
<th>#</th>
<th>Track</th>
<th>Artist</th>
<th>Album</th>
</tr>
</thead>
<tbody id="import-file-preview-tbody">
</tbody>
</table>
</div>
<!-- Playlist name + import button -->
<div class="import-file-action-bar">
<input type="text" id="import-file-playlist-name"
class="import-file-name-input"
placeholder="Enter playlist name..." maxlength="200">
<button class="import-file-import-btn" id="import-file-import-btn"
onclick="importFileSubmit()" disabled>
Import as Mirrored Playlist
</button>
</div>
</div>
</div>
<!-- SoulSync Discovery Sync Tab Content -->
<div class="sync-tab-content" id="soulsync-discovery-sync-tab-content">
<div class="playlist-header">
<h3>SoulSync Discovery Playlists</h3>
<button class="refresh-button soulsync-discovery" id="soulsync-discovery-sync-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="soulsync-discovery-sync-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your personalized SoulSync Discovery playlists.</div>
</div>
</div>
<!-- Last.fm Radio Sync Tab Content -->
<div class="sync-tab-content" id="lastfm-sync-tab-content">
<div class="playlist-header">
<h3>Your Last.fm Radio Playlists</h3>
<button class="refresh-button lastfm" id="lastfm-sync-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="lastfm-sync-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your Last.fm Radio playlists. Generate new ones from the Discover page.</div>
</div>
</div>
<!-- ListenBrainz Sync Tab Content (separate ID from Discover-page LB UI) -->
<div class="sync-tab-content" id="listenbrainz-sync-tab-content">
<div class="playlist-header">
<h3>Your ListenBrainz Playlists</h3>
<div class="listenbrainz-sub-tabs">
<button class="listenbrainz-sub-tab-btn active" data-lb-type="created_for_user">For You</button>
<button class="listenbrainz-sub-tab-btn" data-lb-type="user_created">My Playlists</button>
<button class="listenbrainz-sub-tab-btn" data-lb-type="collaborative">Collaborative</button>
</div>
<button class="refresh-button listenbrainz" id="listenbrainz-sync-refresh-btn">🔄 Refresh</button>
</div>
<div class="playlist-scroll-container" id="listenbrainz-sync-playlist-container">
<div class="playlist-placeholder">Click 'Refresh' to load your ListenBrainz playlists.</div>
</div>
</div>
<!-- Mirrored Playlists Tab Content -->
<div class="sync-tab-content" id="mirrored-tab-content">
<div class="playlist-header">
<h3>Mirrored Playlists</h3>
<button class="pool-trigger-btn" onclick="openDiscoveryPoolModal()" title="View matched and failed discovery tracks">Discovery Pool</button>
<button class="refresh-button mirrored" id="mirrored-refresh-btn">Update list</button>
</div>
<div class="playlist-scroll-container" id="mirrored-playlist-container">
<div class="playlist-placeholder">Playlists you parse from any service will appear here as persistent backups.</div>
</div>
</div>
<!-- Server Playlist Manager Tab -->
<div class="sync-tab-content active" id="server-tab-content">
<div class="playlist-header">
<h3 id="server-tab-title">Server Playlists</h3>
<button class="refresh-button" id="server-refresh-btn" onclick="loadServerPlaylists()">🔄 Refresh</button>
</div>
<div id="server-playlist-view">
<!-- Playlist list (default view) -->
<div class="playlist-scroll-container" id="server-playlist-container">
<div class="playlist-placeholder">Playlists from your media server will load automatically.</div>
</div>
<!-- Disambiguation modal -->
<div id="server-disambig-overlay" class="server-disambig-overlay hidden">
<div class="server-disambig-modal">
<div class="server-disambig-header">
<div>
<h3 class="server-disambig-title">Multiple Sources Found</h3>
<p class="server-disambig-subtitle" id="server-disambig-subtitle"></p>
</div>
<button class="server-disambig-close" onclick="closeServerDisambig()">&times;</button>
</div>
<div class="server-disambig-list" id="server-disambig-list"></div>
</div>
</div>
<!-- Comparison editor -->
<div id="server-editor" style="display: none;">
<div class="server-editor-header">
<button class="server-editor-back" onclick="serverEditorBack()">← Back</button>
<div class="server-editor-info">
<h4 class="server-editor-name" id="server-editor-name"></h4>
<span class="server-editor-meta" id="server-editor-meta"></span>
</div>
<div class="server-editor-stats" id="server-editor-stats"></div>
<button class="server-editor-refresh" onclick="_serverEditorRefresh()" title="Re-fetch from server">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6M3 12a9 9 0 0115.356-6.356L21 8M3 22v-6h6M21 12a9 9 0 01-15.356 6.356L3 16"/></svg>
</button>
</div>
<div id="server-no-source-banner" class="server-no-source-banner" style="display:none">
No mirrored playlist found matching this name. Showing server tracks only.
</div>
<div class="server-editor-filters">
<button class="discog-filter active" data-filter="all" onclick="_serverEditorFilter(this, 'all')">All</button>
<button class="discog-filter" data-filter="matched" onclick="_serverEditorFilter(this, 'matched')">Matched</button>
<button class="discog-filter" data-filter="missing" onclick="_serverEditorFilter(this, 'missing')">Missing</button>
<button class="discog-filter" data-filter="extra" onclick="_serverEditorFilter(this, 'extra')">Extra</button>
<button class="discog-filter server-editor-export" id="server-editor-export-btn" style="margin-left:auto" onclick="exportServerPlaylistM3U()" title="Export this server playlist as an M3U file (for Music Assistant etc.)">📋 Export M3U</button>
</div>
<div class="server-compare-columns">
<div class="server-compare-col source" id="server-col-source">
<div class="server-col-header">
<span class="server-col-icon" id="server-col-source-icon"></span>
<span class="server-col-label" id="server-col-source-label">Source</span>
<span class="server-col-count" id="server-col-source-count"></span>
</div>
<div class="server-col-scroll" id="server-col-source-scroll"></div>
</div>
<div class="server-compare-col server" id="server-col-server">
<div class="server-col-header">
<span class="server-col-icon" id="server-col-server-icon"></span>
<span class="server-col-label" id="server-col-server-label">Server</span>
<span class="server-col-count" id="server-col-server-count"></span>
</div>
<div class="server-col-scroll" id="server-col-server-scroll"></div>
</div>
</div>
<div class="server-editor-footer" id="server-editor-footer"></div>
</div>
</div>
</div>
</div>
<!-- Right Panel: Sidebar with Options & Logging -->
<div class="sync-sidebar">
<div class="sidebar-section">
<h4>Sync Actions</h4>
<div id="selection-info">Select playlists to sync</div>
<button id="start-sync-btn" class="neo-button" disabled>Start Sync</button>
</div>
<div class="sidebar-section progress-section">
<h4>Sync Progress</h4>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="sync-progress-bar" style="width: 0%;"></div>
</div>
<div id="sync-progress-text">Ready to sync...</div>
<textarea id="sync-log-area" readonly>Waiting for sync to start...</textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Search Page -->
<div class="page" id="search-page">
<!--
This top-level container replicates the QSplitter from downloads.py,
creating the two-panel layout for the page.
-->
<div class="downloads-content">
<!-- ======================================================= -->
<!-- == Search page main panel == -->
<!-- ======================================================= -->
<div class="downloads-main-panel">
<!-- Header: Replicates create_elegant_header() -->
<div class="downloads-header">
<div class="downloads-header-content">
<div class="downloads-header-text">
<h2 class="downloads-title"><img src="/static/search.png" class="page-header-icon" alt=""><span>Search</span></h2>
<p class="downloads-subtitle">Find artists, albums, and tracks from any metadata source</p>
</div>
</div>
</div>
<!-- Search source picker — icon row populated by search.js.
Each icon triggers a single-source fetch (no fan-out);
results are cached per (query, source) pair. -->
<div id="enh-source-row" class="enh-source-row" role="tablist" aria-label="Search source"></div>
<!-- Basic Search Section -->
<div id="basic-search-section" class="search-section">
<!-- Source picker: chip row, one chip per active download source.
Populated by downloads.js:initBasicSearchSources().
Hidden until sources load; in single-source mode shows one
non-interactive chip so the user knows what they're searching. -->
<div class="bs-source-row" id="bs-source-row" aria-label="Search source" role="tablist"></div>
<!-- Search bar -->
<div class="bs-search-bar">
<div class="bs-search-input-wrap">
<svg class="bs-search-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="9" r="6"/><path d="M15 15l3 3"/></svg>
<input type="text" id="downloads-search-input"
placeholder="Search artists, albums, tracks…"
autocomplete="off" spellcheck="false">
<button id="downloads-cancel-btn" class="bs-cancel-btn hidden" aria-label="Cancel"></button>
</div>
<button id="downloads-search-btn" class="bs-search-btn">Search</button>
</div>
<!-- Status / loading bar -->
<div class="bs-status-bar">
<div class="spinner-animation hidden"></div>
<span id="search-status-text" class="bs-status-text">Enter an artist, album, or track name to search</span>
<div class="dots-animation hidden"></div>
</div>
<!-- Filters: always-visible compact pill row -->
<div id="filters-container" class="bs-filters hidden">
<div class="bs-filter-group">
<span class="bs-filter-label">Type</span>
<button class="filter-btn bs-filter-pill active" data-filter-type="type" data-value="all">All</button>
<button class="filter-btn bs-filter-pill" data-filter-type="type" data-value="album">Albums</button>
<button class="filter-btn bs-filter-pill" data-filter-type="type" data-value="track">Tracks</button>
</div>
<div class="bs-filter-group">
<span class="bs-filter-label">Format</span>
<button class="filter-btn bs-filter-pill active" data-filter-type="format" data-value="all">All</button>
<button class="filter-btn bs-filter-pill" data-filter-type="format" data-value="flac">FLAC</button>
<button class="filter-btn bs-filter-pill" data-filter-type="format" data-value="mp3">MP3</button>
<button class="filter-btn bs-filter-pill" data-filter-type="format" data-value="ogg">OGG</button>
<button class="filter-btn bs-filter-pill" data-filter-type="format" data-value="aac">AAC</button>
<button class="filter-btn bs-filter-pill" data-filter-type="format" data-value="wma">WMA</button>
</div>
<div class="bs-filter-group">
<span class="bs-filter-label">Sort</span>
<button id="sort-order-btn" class="filter-btn bs-filter-pill sort-order-btn" data-order="desc"></button>
<button class="filter-btn bs-filter-pill active" data-filter-type="sort" data-value="relevance">Relevance</button>
<button class="filter-btn bs-filter-pill" data-filter-type="sort" data-value="quality_score">Quality</button>
<button class="filter-btn bs-filter-pill" data-filter-type="sort" data-value="size">Size</button>
<button class="filter-btn bs-filter-pill" data-filter-type="sort" data-value="title">Name</button>
<button class="filter-btn bs-filter-pill" data-filter-type="sort" data-value="username">Uploader</button>
<button class="filter-btn bs-filter-pill" data-filter-type="sort" data-value="bitrate">Bitrate</button>
<button class="filter-btn bs-filter-pill" data-filter-type="sort" data-value="duration">Duration</button>
</div>
</div>
<!-- Results area -->
<div class="bs-results-wrap" id="search-results-area">
<div class="search-results-placeholder">
<p>Enter a search term to get started.</p>
</div>
</div>
</div>
<!-- End Basic Search Section -->
<!-- Enhanced Search Section (New) -->
<div id="enhanced-search-section" class="search-section active">
<!-- Enhanced Search Bar with Dropdown -->
<div class="enhanced-search-input-wrapper">
<div class="enhanced-search-bar-container">
<div class="enhanced-search-wrapper">
<div class="enhanced-search-icon"></div>
<input type="text" id="enhanced-search-input"
placeholder="Search for artists, albums, or tracks...">
<button id="enhanced-cancel-btn" class="enhanced-cancel-btn hidden"></button>
</div>
</div>
<!-- Link lookup (#775): paste a provider link and resolve
it directly on the owning source (Spotify / Apple Music
/ MusicBrainz / Deezer) — no fuzzy search. Results render
in the dropdown below and reuse the normal download/
import flow. Links only: a bare ID is ambiguous. -->
<div class="enh-id-lookup">
<span class="enh-id-lookup-icon">🔗</span>
<input type="text" id="enh-id-input" class="enh-id-input"
placeholder="…or paste a Spotify / Apple Music / MusicBrainz / Deezer link"
autocomplete="off" spellcheck="false">
<button id="enh-id-btn" class="enh-id-btn" type="button">Look up</button>
</div>
<!-- Enhanced Search Dropdown (Overlay Panel) -->
<div id="enhanced-dropdown" class="enhanced-dropdown hidden">
<div class="enhanced-dropdown-content">
<!-- Mobile close bar -->
<button id="enhanced-dropdown-close" class="enhanced-dropdown-close">
<span></span> Close Results
</button>
<!-- Loading State -->
<div id="enhanced-loading" class="enhanced-loading hidden">
<div class="spinner"></div>
<p id="enhanced-loading-text">Searching across Spotify and your library...
</p>
</div>
<!-- Empty State -->
<div id="enhanced-empty" class="enhanced-empty hidden">
<div class="empty-icon">🔍</div>
<p>No results found</p>
</div>
<!-- Results Container -->
<div id="enhanced-results-container" class="enhanced-results-container hidden">
<!-- Fallback banner — shown when user clicked Spotify but backend
served Deezer due to rate-limit, etc. Populated by search.js. -->
<div id="enh-fallback-banner" class="enh-fallback-banner hidden"></div>
<!-- Artists Container (Side by Side) -->
<div class="enh-artists-wrapper">
<!-- DB Artists -->
<div id="enh-db-artists-section"
class="enh-dropdown-section enh-artist-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">📚</span>
<h4 class="enh-section-title">In Your Library</h4>
<span class="enh-section-count"
id="enh-db-artists-count">0</span>
</div>
<div class="enh-compact-list" id="enh-db-artists-list"></div>
</div>
<!-- Spotify Artists -->
<div id="enh-spotify-artists-section"
class="enh-dropdown-section enh-artist-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">🎤</span>
<h4 class="enh-section-title">Artists</h4>
<span class="enh-section-count"
id="enh-spotify-artists-count">0</span>
</div>
<div class="enh-compact-list" id="enh-spotify-artists-list"></div>
</div>
</div>
<!-- Albums -->
<div id="enh-albums-section" class="enh-dropdown-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">💿</span>
<h4 class="enh-section-title">Albums</h4>
<span class="enh-section-count" id="enh-albums-count">0</span>
</div>
<div class="enh-compact-list" id="enh-albums-list"></div>
</div>
<!-- Singles & EPs -->
<div id="enh-singles-section" class="enh-dropdown-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">🎶</span>
<h4 class="enh-section-title">Singles & EPs</h4>
<span class="enh-section-count" id="enh-singles-count">0</span>
</div>
<div class="enh-compact-list" id="enh-singles-list"></div>
</div>
<!-- Tracks -->
<div id="enh-tracks-section" class="enh-dropdown-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">🎵</span>
<h4 class="enh-section-title">Tracks</h4>
<span class="enh-section-count" id="enh-tracks-count">0</span>
</div>
<div class="enh-compact-list" id="enh-tracks-list"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Search Results Area (for slskd results) -->
<div class="search-results-container">
<div class="search-results-header">
<h3>Search Results</h3>
</div>
<div class="search-results-scroll-area" id="enhanced-main-results-area">
<div class="search-results-placeholder">
<p>Search results will appear here when you select an album or track.</p>
</div>
</div>
</div>
</div>
<!-- End Enhanced Search Section -->
</div>
</div>
</div>
<!-- Automations Page -->
<div class="page" id="automations-page">
<!-- List View -->
<div class="automations-list-view" id="automations-list-view">
<div class="page-shell automations-container">
<div class="dashboard-header"><div class="dashboard-header-sweep" aria-hidden="true"><span></span></div>
<div class="header-text">
<h2 class="header-title"><img src="/static/automation.png" class="page-header-icon" alt=""><span>Automations</span></h2>
<p class="header-subtitle">Configure scheduled tasks and automated workflows</p>
</div>
<div class="header-spacer"></div>
<div class="header-actions">
<button class="auto-new-btn" onclick="showAutomationBuilder()">+ New Automation</button>
</div>
</div>
<div class="automations-stats" id="automations-stats"></div>
<div class="auto-filter-bar" id="auto-filter-bar" style="display:none;">
<input type="text" class="auto-filter-search" id="auto-filter-search" placeholder="Search automations...">
<select class="auto-filter-select" id="auto-filter-trigger"><option value="">All Triggers</option></select>
<select class="auto-filter-select" id="auto-filter-action"><option value="">All Actions</option></select>
<span class="auto-filter-count" id="auto-filter-count"></span>
</div>
<div class="automations-list" id="automations-list"></div>
<div class="automations-empty" id="automations-empty" style="display:none;">
<div class="automations-empty-icon">&#9889;</div>
<div class="automations-empty-title">No automations yet</div>
<div class="automations-empty-text">Create your first automation to schedule tasks and trigger actions automatically.</div>
<button class="auto-new-btn" onclick="showAutomationBuilder()">+ New Automation</button>
</div>
</div>
</div>
<!-- Builder View -->
<div class="automations-builder-view" id="automations-builder-view" style="display:none;">
<div class="builder-header">
<button class="builder-back-btn" onclick="hideAutomationBuilder()" title="Back to list">&#8592;</button>
<input type="text" id="builder-name" class="builder-name-input" placeholder="Automation Name">
<input type="text" id="builder-group-name" class="builder-group-input" placeholder="Group (optional)" list="builder-group-list">
<datalist id="builder-group-list"></datalist>
<div class="builder-header-actions">
<button class="btn-cancel" onclick="hideAutomationBuilder()">Cancel</button>
<button class="btn-save" onclick="saveAutomation()">Save</button>
</div>
</div>
<div class="builder-content">
<div class="builder-sidebar" id="builder-sidebar"></div>
<div class="builder-canvas" id="builder-canvas"></div>
</div>
</div>
</div>
<!-- Active Downloads Page -->
<div class="page" id="active-downloads-page">
<div class="adl-layout">
<!-- Left: download list -->
<div class="adl-main">
<div class="adl-container">
<div class="adl-header">
<h2 class="adl-title"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Downloads</h2>
<div class="adl-controls">
<div class="adl-filter-pills" id="adl-filter-pills">
<button class="adl-pill active" data-filter="all" onclick="adlSetFilter('all')">All</button>
<button class="adl-pill" data-filter="active" onclick="adlSetFilter('active')">Active</button>
<button class="adl-pill" data-filter="queued" onclick="adlSetFilter('queued')">Queued</button>
<button class="adl-pill" data-filter="completed" onclick="adlSetFilter('completed')">Completed</button>
<button class="adl-pill" data-filter="failed" onclick="adlSetFilter('failed')">Failed</button>
<button class="adl-pill" data-filter="unverified" onclick="adlSetFilter('unverified')" title="Review queue: imported-but-unconfirmed downloads (unverified / force-imported) and quarantined files that were never imported.">⚠ Unverified/Quarantine</button>
</div>
<div style="display:flex;align-items:center;gap:10px;">
<span class="adl-count" id="adl-count"></span>
<button class="adl-cancel-all-btn" id="adl-cancel-all-btn" onclick="adlCancelAll()" style="display:none" title="Cancel all active and queued downloads">Cancel All</button>
<button class="adl-clear-btn" id="adl-clear-btn" onclick="adlClearCompleted()" style="display:none">Clear Completed</button>
</div>
</div>
</div>
<div class="adl-list" id="adl-list">
<div class="adl-empty" id="adl-empty">No downloads yet. Start one from Search, Sync, Discover, or Library.</div>
</div>
</div>
</div>
<!-- Right: batch context panel -->
<div class="adl-batch-panel" id="adl-batch-panel">
<div class="adl-batch-panel-header">
<h3 class="adl-batch-panel-title">Batches</h3>
<button class="adl-batch-panel-collapse" id="adl-batch-collapse" onclick="adlToggleBatchPanel()" title="Toggle batch panel">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
<div class="adl-batch-summary" id="adl-batch-summary" style="display:none">
<!-- Live aggregate (batches · downloading · speed · ETA) rendered by JS -->
</div>
<div class="adl-batch-active" id="adl-batch-active">
<!-- Active batch cards rendered by JS -->
</div>
<div class="adl-batch-history-section" id="adl-batch-history-section" style="display:none">
<div class="adl-batch-history-header" onclick="adlToggleBatchHistory()">
<span>Recent History</span>
<div class="adl-batch-history-header-actions">
<button class="library-history-btn" onclick="event.stopPropagation();openLibraryHistoryModal()" title="View full download + import history">Download History</button>
<svg class="adl-batch-history-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
<div class="adl-batch-history-list" id="adl-batch-history-list">
<!-- Completed batch history rendered by JS -->
</div>
</div>
</div>
</div>
</div>
<!-- Library Page -->
<div class="page" id="library-page">
<div class="library-container">
<!-- Header -->
<div class="library-header">
<div class="library-header-content">
<h2 class="library-title"><img src="/static/library.png" class="page-header-icon" alt=""><span>Music Library</span></h2>
<p class="library-subtitle">Browse your complete music collection</p>
</div>
<div class="library-stats" id="library-stats">
<span class="library-stat">
<span class="stat-number" id="library-artist-count">0</span>
<span class="stat-label">Artists</span>
</span>
</div>
<button class="library-watchlist-all-btn library-export-btn" id="library-export-btn" onclick="openArtistExportModal()" title="Export artists — pick watchlist or whole library, as JSON / CSV / text">
<span class="watchlist-all-icon"></span>
<span class="watchlist-all-text">Export</span>
</button>
</div>
<!-- Search and Filters -->
<div class="library-controls">
<div class="library-search-container">
<input type="text" id="library-search-input" class="library-search-input"
placeholder="Search artists...">
<div class="library-search-icon">🔍</div>
</div>
<!-- Watchlist Filter -->
<div class="watchlist-filter" id="watchlist-filter">
<button class="watchlist-filter-btn active" data-filter="all">All</button>
<button class="watchlist-filter-btn" data-filter="watched">Watched</button>
<button class="watchlist-filter-btn" data-filter="unwatched">Unwatched</button>
<button class="library-watchlist-all-btn hidden" id="library-watchlist-all-btn" onclick="openWatchAllUnwatchedModal()">
<span class="watchlist-all-icon">👁️</span>
<span class="watchlist-all-text">Watch All Unwatched</span>
</button>
</div>
<!-- Metadata Source Filter -->
<div class="library-source-filter">
<select id="library-source-filter" class="library-source-filter-select">
<option value="">All Sources</option>
<optgroup label="Unmatched to">
<option value="!spotify">No Spotify</option>
<option value="!musicbrainz">No MusicBrainz</option>
<option value="!deezer">No Deezer</option>
<option value="!discogs">No Discogs</option>
<option value="!audiodb">No AudioDB</option>
<option value="!itunes">No iTunes</option>
<option value="!lastfm">No Last.fm</option>
<option value="!genius">No Genius</option>
<option value="!tidal">No Tidal</option>
<option value="!qobuz">No Qobuz</option>
</optgroup>
<optgroup label="Matched to">
<option value="spotify">Has Spotify</option>
<option value="musicbrainz">Has MusicBrainz</option>
<option value="deezer">Has Deezer</option>
<option value="discogs">Has Discogs</option>
<option value="audiodb">Has AudioDB</option>
<option value="itunes">Has iTunes</option>
<option value="lastfm">Has Last.fm</option>
<option value="genius">Has Genius</option>
<option value="tidal">Has Tidal</option>
<option value="qobuz">Has Qobuz</option>
</optgroup>
</select>
</div>
<!-- Alphabet Selector -->
<div class="alphabet-selector" id="alphabet-selector">
<div class="alphabet-selector-inner">
<button class="alphabet-btn active" data-letter="all">All</button>
<button class="alphabet-btn" data-letter="a">A</button>
<button class="alphabet-btn" data-letter="b">B</button>
<button class="alphabet-btn" data-letter="c">C</button>
<button class="alphabet-btn" data-letter="d">D</button>
<button class="alphabet-btn" data-letter="e">E</button>
<button class="alphabet-btn" data-letter="f">F</button>
<button class="alphabet-btn" data-letter="g">G</button>
<button class="alphabet-btn" data-letter="h">H</button>
<button class="alphabet-btn" data-letter="i">I</button>
<button class="alphabet-btn" data-letter="j">J</button>
<button class="alphabet-btn" data-letter="k">K</button>
<button class="alphabet-btn" data-letter="l">L</button>
<button class="alphabet-btn" data-letter="m">M</button>
<button class="alphabet-btn" data-letter="n">N</button>
<button class="alphabet-btn" data-letter="o">O</button>
<button class="alphabet-btn" data-letter="p">P</button>
<button class="alphabet-btn" data-letter="q">Q</button>
<button class="alphabet-btn" data-letter="r">R</button>
<button class="alphabet-btn" data-letter="s">S</button>
<button class="alphabet-btn" data-letter="t">T</button>
<button class="alphabet-btn" data-letter="u">U</button>
<button class="alphabet-btn" data-letter="v">V</button>
<button class="alphabet-btn" data-letter="w">W</button>
<button class="alphabet-btn" data-letter="x">X</button>
<button class="alphabet-btn" data-letter="y">Y</button>
<button class="alphabet-btn" data-letter="z">Z</button>
<button class="alphabet-btn" data-letter="#">#</button>
</div>
</div>
</div>
<!-- Content Area -->
<div class="library-content">
<!-- Loading State -->
<div class="library-loading hidden" id="library-loading">
<div class="loading-spinner"></div>
<div class="loading-text">Loading artists...</div>
</div>
<!-- Artist Grid -->
<div class="library-artists-grid" id="library-artists-grid">
<!-- Artist cards will be populated here -->
</div>
<!-- Empty State — populated by showLibraryEmpty() in library.js.
Shows a generic "no artists" message by default; when the
user's search has no library matches, it switches to a
search-online CTA that hands off to the /search page. -->
<div class="library-empty hidden" id="library-empty">
<div class="empty-icon" id="library-empty-icon">🎵</div>
<div class="empty-title" id="library-empty-title">No artists found</div>
<div class="empty-subtitle" id="library-empty-subtitle">Try adjusting your search or filters</div>
<button class="library-empty-search-cta hidden" id="library-empty-search-cta">
<span class="library-empty-search-cta-icon">🔍</span>
<span class="library-empty-search-cta-text">Search online for <span id="library-empty-search-cta-query"></span></span>
<span class="library-empty-search-cta-arrow"></span>
</button>
</div>
<!-- Pagination -->
<div class="library-pagination hidden" id="library-pagination">
<button class="pagination-btn" id="prev-page-btn" disabled>
<span>← Previous</span>
</button>
<div class="pagination-info">
<span id="page-info">Page 1 of 1</span>
</div>
<button class="pagination-btn" id="next-page-btn" disabled>
<span>Next →</span>
</button>
</div>
</div>
</div>
</div>
<!-- Artist Detail Page -->
<div class="page" id="artist-detail-page">
<div class="page-header">
<button class="back-btn" id="artist-detail-back-btn">
<span>← Back</span>
</button>
</div>
<!-- Artist Hero Section -->
<div class="artist-hero-section" id="artist-hero-section">
<!-- Blurred background image + dark overlay (inline-Artists hero treatment) -->
<div class="artist-detail-hero-bg" id="artist-detail-hero-bg"></div>
<div class="artist-detail-hero-overlay"></div>
<div class="artist-hero-content">
<!-- Left: Image -->
<div class="artist-image-container">
<img class="artist-image" id="artist-detail-image" src="" alt="Artist Image" />
<div class="artist-image-fallback" id="artist-detail-image-fallback">🎵</div>
</div>
<!-- Center: Identity + Meta -->
<div class="artist-info">
<div class="artist-hero-identity">
<h1 class="artist-name" id="artist-detail-name">Artist Name</h1>
<div class="artist-hero-badges" id="artist-hero-badges"></div>
</div>
<div class="artist-hero-actions">
<button class="library-artist-radio-btn" id="library-artist-radio-btn"
onclick="playArtistRadio()">
<span class="radio-icon">📻</span>
<span class="radio-text">Artist Radio</span>
</button>
<button class="library-artist-watchlist-btn" id="library-artist-watchlist-btn">
<span class="watchlist-icon">👁️</span>
<span class="watchlist-text">Add to Watchlist</span>
</button>
<div class="discog-download-wrap" id="discog-download-wrap" style="display:none;">
<button class="discog-download-btn discog-btn-compact" id="discog-download-btn" onclick="openDiscographyModal()">
<span class="discog-btn-icon"></span>
<span class="discog-btn-text">Download Discography</span>
<span class="discog-btn-shimmer"></span>
</button>
</div>
<button class="library-artist-enhance-btn hidden" id="library-artist-enhance-btn"
onclick="openEnhanceQualityModal()">
<span class="enhance-icon"></span>
<span class="enhance-text">Enhance Quality</span>
</button>
</div>
<div class="artist-genres-container" id="artist-genres"></div>
<div class="artist-hero-bio" id="artist-hero-bio" style="display:none;"></div>
<div class="artist-hero-numbers">
<div class="artist-hero-stat" id="artist-hero-listeners" style="display:none;">
<span class="hero-stat-value">0</span>
<span class="hero-stat-label">listeners</span>
</div>
<div class="artist-hero-stat" id="artist-hero-playcount" style="display:none;">
<span class="hero-stat-value">0</span>
<span class="hero-stat-label">plays</span>
</div>
</div>
<div class="collection-overview">
<div class="collection-category">
<span class="category-label">Albums</span>
<div class="completion-bar"><div class="completion-fill" id="albums-completion-fill" style="width: 0%"></div></div>
<span class="category-stats" id="albums-stats">0/0</span>
</div>
<div class="collection-category">
<span class="category-label">EPs</span>
<div class="completion-bar"><div class="completion-fill" id="eps-completion-fill" style="width: 0%"></div></div>
<span class="category-stats" id="eps-stats">0/0</span>
</div>
<div class="collection-category">
<span class="category-label">Singles</span>
<div class="completion-bar"><div class="completion-fill" id="singles-completion-fill" style="width: 0%"></div></div>
<span class="category-stats" id="singles-stats">0/0</span>
</div>
</div>
<!-- Per-artist enrichment coverage -->
<div class="artist-enrichment-coverage" id="artist-enrichment-coverage" style="display:none;"></div>
</div>
<!-- Right: Top Tracks only -->
<div class="artist-hero-right" id="artist-hero-sidebar" style="display:none;">
<div class="hero-sidebar-title" id="hero-sidebar-title">Popular on Last.fm</div>
<div class="hero-top-tracks" id="hero-top-tracks"></div>
<button class="hero-top-tracks-download-all" id="hero-top-tracks-download-all" style="display:none;">Download All</button>
</div>
</div>
</div>
<div class="artist-detail-content">
<!-- Loading State -->
<div class="artist-detail-loading hidden" id="artist-detail-loading">
<div class="loading-spinner"></div>
<p>Loading artist discography...</p>
</div>
<!-- Error State -->
<div class="artist-detail-error hidden" id="artist-detail-error">
<div class="error-icon">⚠️</div>
<h3>Failed to load artist details</h3>
<p id="artist-detail-error-message">An error occurred while loading the artist's discography.
</p>
<button class="retry-btn" id="artist-detail-retry-btn">Retry</button>
</div>
<!-- Main Content -->
<div class="artist-detail-main" id="artist-detail-main">
<!-- Discography Filters -->
<div class="discography-filters" id="discography-filters">
<div class="filter-group">
<span class="filter-label">Show</span>
<button class="discography-filter-btn active" data-filter="category" data-value="albums">Albums</button>
<button class="discography-filter-btn active" data-filter="category" data-value="eps">EPs</button>
<button class="discography-filter-btn active" data-filter="category" data-value="singles">Singles</button>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<span class="filter-label">Include</span>
<button class="discography-filter-btn active" data-filter="content" data-value="live">Live</button>
<button class="discography-filter-btn active" data-filter="content" data-value="compilations">Compilations</button>
<button class="discography-filter-btn active" data-filter="content" data-value="featured">Featured</button>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<span class="filter-label">Status</span>
<button class="discography-filter-btn active" data-filter="ownership" data-value="all">All</button>
<button class="discography-filter-btn" data-filter="ownership" data-value="owned">Owned</button>
<button class="discography-filter-btn" data-filter="ownership" data-value="missing">Missing</button>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<span class="filter-label">View</span>
<button class="enhanced-view-toggle-btn active" data-view="standard" onclick="toggleEnhancedView(false)">Standard</button>
<button class="enhanced-view-toggle-btn" data-view="enhanced" onclick="toggleEnhancedView(true)">Enhanced</button>
</div>
</div>
<!-- Discography Sections -->
<div class="discography-sections">
<!-- Albums Section -->
<div class="discography-section" id="albums-section">
<div class="section-header">
<h3>Albums</h3>
<div class="section-stats">
<span id="albums-owned-count">0 owned</span>
<span id="albums-missing-count">0 missing</span>
</div>
</div>
<div class="releases-grid" id="albums-grid">
<!-- Album cards will be populated here -->
</div>
</div>
<!-- EPs Section -->
<div class="discography-section" id="eps-section">
<div class="section-header">
<h3>EPs</h3>
<div class="section-stats">
<span id="eps-owned-count">0 owned</span>
<span id="eps-missing-count">0 missing</span>
</div>
</div>
<div class="releases-grid" id="eps-grid">
<!-- EP cards will be populated here -->
</div>
</div>
<!-- Singles Section -->
<div class="discography-section" id="singles-section">
<div class="section-header">
<h3>Singles</h3>
<div class="section-stats">
<span id="singles-owned-count">0 owned</span>
<span id="singles-missing-count">0 missing</span>
</div>
</div>
<div class="releases-grid" id="singles-grid">
<!-- Single cards will be populated here -->
</div>
</div>
</div>
<!-- Similar Artists Section (works for both library and source artists).
Uses its own scoped IDs because the artist detail view has a section
with the same base IDs and both elements live in the DOM at once. -->
<div class="similar-artists-section" id="ad-similar-artists-section">
<div class="similar-artists-header">
<h3 class="similar-artists-title">Similar Artists</h3>
<p class="similar-artists-subtitle">Discover artists with a similar sound</p>
</div>
<div class="similar-artists-loading hidden" id="ad-similar-artists-loading">
<div class="loading-spinner-small"></div>
<span>Finding similar artists...</span>
</div>
<div class="similar-artists-error hidden" id="ad-similar-artists-error">
<span class="error-icon">⚠️</span>
<span class="error-text">Unable to load similar artists</span>
</div>
<div class="similar-artists-bubbles-container" id="ad-similar-artists-bubbles-container">
<!-- Artist bubble cards will be populated here -->
</div>
</div>
<!-- Enhanced Library Management View -->
<div class="enhanced-view-container hidden" id="enhanced-view-container">
<!-- Populated dynamically by renderEnhancedView() -->
</div>
</div>
</div>
</div>
<!-- Enhanced Bulk Edit Modal -->
<div class="modal-overlay hidden" id="enhanced-bulk-edit-overlay">
<div class="enhanced-bulk-modal">
<div class="enhanced-bulk-modal-header">
<h3 id="enhanced-bulk-modal-title">Batch Edit Tracks</h3>
<button class="enhanced-bulk-modal-close" onclick="closeBulkEditModal()">&times;</button>
</div>
<div class="enhanced-bulk-modal-body" id="enhanced-bulk-modal-body">
<!-- Populated dynamically -->
</div>
<div class="enhanced-bulk-modal-footer">
<button class="btn btn--sm btn--secondary enhanced-bulk-btn" onclick="closeBulkEditModal()">Cancel</button>
<button class="btn btn--sm btn--primary enhanced-bulk-btn" onclick="executeBulkEdit()">Apply Changes</button>
</div>
</div>
</div>
<!-- Enhanced Bulk Actions Bar -->
<div class="enhanced-bulk-bar" id="enhanced-bulk-bar">
<div class="enhanced-bulk-bar-info">
<span class="enhanced-bulk-bar-count" id="enhanced-bulk-count">0</span>
<span class="enhanced-bulk-bar-label">tracks selected</span>
</div>
<div class="enhanced-bulk-bar-actions">
<button class="btn btn--sm btn--secondary enhanced-bulk-btn" onclick="showBulkEditModal()">Edit Selected</button>
<button class="btn btn--sm btn--secondary enhanced-bulk-btn tag-write" onclick="batchWriteTagsSelected()">Write Tags</button>
<button class="btn btn--sm btn--secondary enhanced-bulk-btn rg-analyze" onclick="batchAnalyzeReplayGainSelected()">ReplayGain</button>
<button class="btn btn--sm btn--danger enhanced-bulk-btn" onclick="clearTrackSelection()">Clear Selection</button>
</div>
</div>
<!-- Tag Preview Modal -->
<div class="modal-overlay hidden" id="tag-preview-overlay">
<div class="enhanced-bulk-modal tag-preview-modal">
<div class="enhanced-bulk-modal-header">
<h3 id="tag-preview-title">Write Tags to File</h3>
<button class="enhanced-bulk-modal-close" onclick="closeTagPreviewModal()">&times;</button>
</div>
<div class="enhanced-bulk-modal-body" id="tag-preview-body">
<!-- Populated dynamically -->
</div>
<div class="enhanced-bulk-modal-footer">
<label class="tag-preview-cover-label">
<input type="checkbox" id="tag-preview-embed-cover" checked>
Embed cover art
</label>
<label class="tag-preview-cover-label hidden" id="tag-preview-sync-label">
<input type="checkbox" id="tag-preview-sync-server" checked>
<span id="tag-preview-sync-text">Sync to server</span>
</label>
<div class="tag-preview-footer-actions">
<button class="btn btn--sm btn--secondary enhanced-bulk-btn" onclick="closeTagPreviewModal()">Cancel</button>
<button class="btn btn--sm btn--primary enhanced-bulk-btn" id="tag-preview-write-btn" onclick="executeWriteTags()">Write Tags</button>
</div>
</div>
</div>
</div>
<!-- Batch Tag Preview Modal -->
<div class="modal-overlay hidden" id="batch-tag-preview-overlay">
<div class="enhanced-bulk-modal batch-tag-preview-modal">
<div class="enhanced-bulk-modal-header">
<h3 id="batch-tag-preview-title">Write Tags</h3>
<button class="enhanced-bulk-modal-close" onclick="closeBatchTagPreviewModal()">&times;</button>
</div>
<div id="batch-tag-preview-summary"></div>
<div class="enhanced-bulk-modal-body batch-tag-preview-body" id="batch-tag-preview-body">
<!-- Populated dynamically -->
</div>
<div class="enhanced-bulk-modal-footer">
<label class="tag-preview-cover-label">
<input type="checkbox" id="batch-tag-preview-embed-cover" checked>
Embed cover art
</label>
<label class="tag-preview-cover-label hidden" id="batch-tag-preview-sync-label">
<input type="checkbox" id="batch-tag-preview-sync-server" checked>
<span id="batch-tag-preview-sync-text">Sync to server</span>
</label>
<div class="tag-preview-footer-actions">
<button class="btn btn--sm btn--secondary enhanced-bulk-btn" onclick="closeBatchTagPreviewModal()">Cancel</button>
<button class="btn btn--sm btn--primary enhanced-bulk-btn" id="batch-tag-preview-write-btn" onclick="executeBatchWriteTags()">Write Tags</button>
</div>
</div>
</div>
</div>
<!-- Reorganize Album Modal -->
<div class="modal-overlay hidden" id="reorganize-overlay">
<div class="enhanced-bulk-modal reorganize-modal">
<div class="enhanced-bulk-modal-header">
<h3 id="reorganize-modal-title">Reorganize Album</h3>
<button class="enhanced-bulk-modal-close" onclick="closeReorganizeModal()">&times;</button>
</div>
<div class="enhanced-bulk-modal-body" id="reorganize-modal-body">
<!-- Populated dynamically -->
</div>
<div class="enhanced-bulk-modal-footer" id="reorganize-modal-footer">
<button class="btn btn--sm btn--secondary enhanced-bulk-btn" onclick="closeReorganizeModal()">Cancel</button>
<button class="btn btn--sm btn--primary enhanced-bulk-btn" id="reorganize-apply-btn" onclick="executeReorganize()" disabled>Apply</button>
</div>
</div>
</div>
<!-- Discover Page -->
<div class="page" id="discover-page">
<div class="discover-container">
<!-- Hero Section -->
<div class="discover-hero">
<div class="discover-hero-background" id="discover-hero-bg"></div>
<div class="discover-hero-overlay"></div>
<!-- Navigation Arrows -->
<button class="discover-hero-nav discover-hero-nav-prev" onclick="navigateDiscoverHero(-1)"
aria-label="Previous artist">
<span></span>
</button>
<button class="discover-hero-nav discover-hero-nav-next" onclick="navigateDiscoverHero(1)"
aria-label="Next artist">
<span></span>
</button>
<!-- Discover Page Help Button -->
<button class="tool-help-button discover-page-help-button" data-tool="discover-page"
title="Learn about the Discover page">?</button>
<button class="discover-blacklist-btn" onclick="openDiscoveryBlacklistModal()" title="Blocked Artists">🚫</button>
<div class="discover-hero-content">
<div class="discover-hero-info">
<div class="discover-hero-label">FEATURED ARTIST</div>
<h1 class="discover-hero-title" id="discover-hero-title">Loading...</h1>
<p class="discover-hero-subtitle" id="discover-hero-subtitle">Discover new music
tailored to your taste</p>
<div class="discover-hero-meta" id="discover-hero-meta">
<!-- Popularity and genres will be populated here -->
</div>
<div class="discover-hero-actions">
<a class="discover-hero-button secondary" id="discover-hero-discography" href="#"
style="text-decoration:none;color:inherit;">
<span class="button-icon">📀</span>
<span class="button-text">View Discography</span>
</a>
<button class="discover-hero-button primary watchlist-toggle-btn"
id="discover-hero-add" onclick="toggleDiscoverHeroWatchlist(event)">
<span class="watchlist-icon">👁️</span>
<span class="watchlist-text">Add to Watchlist</span>
</button>
</div>
</div>
<div class="discover-hero-image" id="discover-hero-image">
<div class="hero-image-placeholder">🎧</div>
</div>
</div>
<!-- Slideshow Indicators -->
<div class="discover-hero-indicators" id="discover-hero-indicators"></div>
<!-- Hero Bottom Buttons -->
<div class="discover-hero-bottom-actions">
<button class="discover-hero-watch-all" id="discover-hero-watch-all" onclick="watchAllHeroArtists(this)">
<span class="watch-all-icon">👁️</span>
<span class="watch-all-text">Watch All</span>
</button>
<button class="discover-hero-view-all" id="discover-hero-view-all" onclick="openRecommendedArtistsModal()">
View Recommended
</button>
</div>
</div>
<!-- Artist Map Hub -->
<div class="artmap-hub">
<div class="artmap-hub-bg"></div>
<div class="artmap-hub-content">
<div class="artmap-hub-header">
<svg class="artmap-hub-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="3"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/>
<circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/>
<line x1="6" y1="7" x2="10" y2="10"/><line x1="14" y1="10" x2="18" y2="7"/>
<line x1="6" y1="17" x2="10" y2="14"/><line x1="14" y1="14" x2="18" y2="17"/>
</svg>
<div>
<h2 class="artmap-hub-title">Artist Map</h2>
<p class="artmap-hub-subtitle">Explore the connections between your artists</p>
</div>
</div>
<div class="artmap-hub-cards">
<div class="artmap-hub-card" onclick="openArtistMap()">
<div class="artmap-hub-card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</div>
<div class="artmap-hub-card-text">
<h3>Watchlist</h3>
<p>Your watched artists and their similar connections</p>
</div>
<svg class="artmap-hub-card-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</div>
<div class="artmap-hub-card" onclick="openArtistMapGenre()">
<div class="artmap-hub-card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/><line x1="2" y1="12" x2="22" y2="12"/>
</svg>
</div>
<div class="artmap-hub-card-text">
<h3>Genres</h3>
<p>Artists clustered by genre across your library and cache</p>
</div>
<svg class="artmap-hub-card-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</div>
<div class="artmap-hub-card" onclick="openArtistMapExplorer()">
<div class="artmap-hub-card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
<line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/>
</svg>
</div>
<div class="artmap-hub-card-text">
<h3>Explorer</h3>
<p>Pick any artist and explore outward through their connections</p>
</div>
<svg class="artmap-hub-card-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</div>
</div>
</div>
</div>
<!-- Artist Map (fullscreen canvas, hidden by default) -->
<div class="artist-map-container" id="artist-map-container" style="display:none;">
<div class="artist-map-toolbar">
<!-- Left: back + title -->
<div class="artmap-nav-left">
<button class="artmap-back" onclick="closeArtistMap()" title="Back to Discover (Esc)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="artmap-brand">
<svg class="artmap-brand-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/><circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/><line x1="6" y1="7" x2="10" y2="10"/><line x1="14" y1="10" x2="18" y2="7"/><line x1="6" y1="17" x2="10" y2="14"/><line x1="14" y1="14" x2="18" y2="17"/></svg>
<span class="artmap-brand-text">Artist Map</span>
</div>
<div class="artmap-stats" id="artist-map-stats"></div>
</div>
<!-- Center: search -->
<div class="artmap-nav-center">
<div class="artmap-search-wrap">
<svg class="artmap-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="artist-map-search" placeholder="Search artists... (S)" oninput="artMapSearch(this.value)">
</div>
</div>
<!-- Right: tools -->
<div class="artmap-nav-right">
<button class="artmap-tool-btn" id="artmap-toggle-similar" onclick="artMapToggleSimilar()" title="Toggle similar artists (H)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/></svg>
<span>Filter</span>
</button>
<div class="artmap-zoom-group">
<button class="artmap-tool-btn artmap-zoom" onclick="artMapZoom(1.3)" title="Zoom in (+)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<button class="artmap-tool-btn artmap-zoom" onclick="artMapZoom(0.7)" title="Zoom out (-)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<button class="artmap-tool-btn artmap-zoom" onclick="artMapFitToView()" title="Fit to view (F)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
</button>
</div>
<button class="artmap-tool-btn" onclick="artMapShowShortcuts()" title="Keyboard shortcuts">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="10.01"/><line x1="10" y1="10" x2="10" y2="10.01"/><line x1="14" y1="10" x2="14" y2="10.01"/><line x1="18" y1="10" x2="18" y2="10.01"/><line x1="8" y1="14" x2="16" y2="14"/></svg>
</button>
</div>
</div>
<div class="artmap-content-row">
<div class="artmap-genre-sidebar" id="artmap-genre-sidebar" style="display:none;">
<div class="artmap-genre-sidebar-header">
<span>Genres</span>
<input type="text" class="artmap-genre-sidebar-search" placeholder="Filter..." oninput="_filterGenreSidebar(this.value)">
</div>
<div class="artmap-genre-sidebar-list" id="artmap-genre-sidebar-list"></div>
</div>
<canvas id="artist-map-canvas"></canvas>
</div>
<div class="artist-map-tooltip" id="artist-map-tooltip"></div>
<div class="artist-map-search-results" id="artist-map-search-results"></div>
</div>
<!-- Recommended For You Section (similar-artists graph) -->
<div class="discover-section" id="recommended-artists-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">Recommended For You</h2>
<p class="discover-section-subtitle">Artists similar to ones across your library — not yet on your watchlist</p>
</div>
<div class="discover-section-actions">
<button class="btn btn--sm btn--secondary ya-header-btn ya-viewall-btn" onclick="openRecommendedArtistsModal()">
<span>View All</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</button>
</div>
</div>
<div class="discover-carousel" id="recommended-artists-carousel">
<!-- Populated by JS -->
</div>
</div>
<!-- Your Artists Section -->
<div class="discover-section" id="your-artists-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">Your Artists</h2>
<p class="discover-section-subtitle" id="your-artists-subtitle">Artists you follow across your music services</p>
</div>
<div class="discover-section-actions">
<button class="btn btn--sm btn--secondary ya-header-btn ya-refresh-btn" id="your-artists-refresh-btn" onclick="refreshYourArtists()" title="Refresh from services">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
</button>
<button class="btn btn--sm btn--secondary ya-header-btn ya-settings-btn" onclick="openYourArtistsSourcesModal()" title="Configure sources">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
<button class="btn btn--sm btn--secondary ya-header-btn ya-viewall-btn" onclick="openYourArtistsModal()">
<span>View All</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</button>
</div>
</div>
<div class="discover-carousel" id="your-artists-carousel">
<!-- Populated by JS -->
</div>
</div>
<!-- Your Albums Section -->
<div class="discover-section" id="your-albums-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">Your Albums</h2>
<p class="discover-section-subtitle" id="your-albums-subtitle">Albums you've saved across your music services</p>
</div>
<div class="discover-section-actions">
<button class="btn btn--sm btn--secondary ya-header-btn ya-refresh-btn" id="your-albums-refresh-btn" onclick="refreshYourAlbums()" title="Refresh from services">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
</button>
<button class="btn btn--sm btn--secondary ya-header-btn ya-settings-btn" onclick="openYourAlbumsSourcesModal()" title="Configure sources">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
<button class="btn btn--sm btn--secondary ya-header-btn" id="your-albums-download-btn" onclick="downloadMissingYourAlbums()" style="display:none;" title="Download missing albums">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
</button>
</div>
</div>
<div class="spotify-library-filters" id="your-albums-filters" style="display: none;">
<input type="text" class="spotify-library-search" id="your-albums-search"
placeholder="Search by artist or album..." oninput="debouncedYourAlbumsSearch()">
<select id="your-albums-status-filter" class="spotify-library-select" onchange="loadYourAlbumsGrid()">
<option value="all">All Albums</option>
<option value="missing">Missing</option>
<option value="owned">Owned</option>
</select>
<select id="your-albums-sort" class="spotify-library-select" onchange="loadYourAlbumsGrid()">
<option value="artist_name">Artist</option>
<option value="album_name">Album</option>
<option value="release_date">Release Date</option>
<option value="recent">Date Added</option>
</select>
</div>
<div class="spotify-library-grid" id="your-albums-grid">
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading your albums...</p>
</div>
</div>
<div class="spotify-library-pagination" id="your-albums-pagination" style="display: none;"></div>
</div>
<!-- Recent Releases Section -->
<div class="discover-section">
<div class="discover-section-header">
<h2 class="discover-section-title">Recent Releases</h2>
<p class="discover-section-subtitle">New music from artists you follow</p>
</div>
<div class="discover-carousel" id="recent-releases-carousel">
<!-- Content will be populated dynamically -->
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading recent releases...</p>
</div>
</div>
</div>
<!-- Seasonal Albums Section (Auto-shows based on current season) -->
<div class="discover-section" id="seasonal-albums-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title" id="seasonal-albums-title">Seasonal</h2>
<p class="discover-section-subtitle" id="seasonal-albums-subtitle">Seasonal music</p>
</div>
<div class="discover-carousel" id="seasonal-albums-carousel">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Seasonal Playlist Section (Auto-shows based on current season) -->
<div class="discover-section" id="seasonal-playlist-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title" id="seasonal-playlist-title">Seasonal Mix</h2>
<p class="discover-section-subtitle" id="seasonal-playlist-subtitle">Curated seasonal
playlist</p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForDiscoverPlaylist('seasonal_playlist', 'Seasonal Mix')"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="seasonal-playlist-sync-btn"
onclick="startDiscoverPlaylistSync('seasonal_playlist', 'Seasonal Mix')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="seasonal-playlist-sync-status" style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span
id="seasonal-playlist-sync-completed">0</span></span>
<span class="sync-stat"><span id="seasonal-playlist-sync-pending">0</span></span>
<span class="sync-stat"><span id="seasonal-playlist-sync-failed">0</span></span>
<span class="sync-stat">(<span
id="seasonal-playlist-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<div class="discover-playlist-container compact" id="seasonal-playlist">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Daily Mixes Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">🎵 Daily Mixes</h2>
<p class="discover-section-subtitle">Personalized mixes based on your taste</p>
</div>
<div class="discover-more-playlists-grid" id="daily-mixes-grid">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Fresh Tape Section -->
<div class="discover-section">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">Fresh Tape</h2>
<p class="discover-section-subtitle">New drops from recent releases</p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForDiscoverPlaylist('release_radar', 'Fresh Tape')"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="release-radar-sync-btn"
onclick="startDiscoverPlaylistSync('release_radar', 'Fresh Tape')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="release-radar-sync-status" style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span id="release-radar-sync-total">0</span></span>
<span class="sync-separator">/</span>
<span class="sync-stat"><span id="release-radar-sync-matched">0</span></span>
<span class="sync-separator">/</span>
<span class="sync-stat"><span id="release-radar-sync-failed">0</span></span>
<span class="sync-stat">(<span id="release-radar-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<div class="discover-playlist-container compact" id="release-radar-playlist">
<!-- Content will be populated dynamically -->
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading fresh tape...</p>
</div>
</div>
</div>
<!-- The Archives Section -->
<div class="discover-section">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">The Archives</h2>
<p class="discover-section-subtitle">Curated from your collection</p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForDiscoverPlaylist('discovery_weekly', 'The Archives')"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="discovery-weekly-sync-btn"
onclick="startDiscoverPlaylistSync('discovery_weekly', 'The Archives')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="discovery-weekly-sync-status" style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span id="discovery-weekly-sync-total">0</span></span>
<span class="sync-separator">/</span>
<span class="sync-stat"><span id="discovery-weekly-sync-matched">0</span></span>
<span class="sync-separator">/</span>
<span class="sync-stat"><span id="discovery-weekly-sync-failed">0</span></span>
<span class="sync-stat">(<span
id="discovery-weekly-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<div class="discover-playlist-container compact" id="discovery-weekly-playlist">
<!-- Content will be populated dynamically -->
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading the archives...</p>
</div>
</div>
</div>
<!-- Popular Picks Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">🔥 Popular Picks</h2>
<p class="discover-section-subtitle">Trending tracks from new discoveries</p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForDiscoverPlaylist('popular_picks', 'Popular Picks')"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="popular-picks-sync-btn"
onclick="startDiscoverPlaylistSync('popular_picks', 'Popular Picks')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="popular-picks-sync-status" style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span id="popular-picks-sync-completed">0</span></span>
<span class="sync-stat"><span id="popular-picks-sync-pending">0</span></span>
<span class="sync-stat"><span id="popular-picks-sync-failed">0</span></span>
<span class="sync-stat">(<span id="popular-picks-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<div class="discover-playlist-container compact" id="personalized-popular-picks">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Hidden Gems Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">🌟 Hidden Gems</h2>
<p class="discover-section-subtitle">Underground discoveries waiting for you</p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForDiscoverPlaylist('hidden_gems', 'Hidden Gems')"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="hidden-gems-sync-btn"
onclick="startDiscoverPlaylistSync('hidden_gems', 'Hidden Gems')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="hidden-gems-sync-status" style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span id="hidden-gems-sync-completed">0</span></span>
<span class="sync-stat"><span id="hidden-gems-sync-pending">0</span></span>
<span class="sync-stat"><span id="hidden-gems-sync-failed">0</span></span>
<span class="sync-stat">(<span id="hidden-gems-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<div class="discover-playlist-container compact" id="personalized-hidden-gems">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Discovery Shuffle Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">🔀 Discovery Shuffle</h2>
<p class="discover-section-subtitle">Random tracks from your discovery pool - different
every time</p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForDiscoverPlaylist('discovery_shuffle', 'Discovery Shuffle')"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="discovery-shuffle-sync-btn"
onclick="startDiscoverPlaylistSync('discovery_shuffle', 'Discovery Shuffle')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="discovery-shuffle-sync-status" style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span
id="discovery-shuffle-sync-completed">0</span></span>
<span class="sync-stat"><span id="discovery-shuffle-sync-pending">0</span></span>
<span class="sync-stat"><span id="discovery-shuffle-sync-failed">0</span></span>
<span class="sync-stat">(<span
id="discovery-shuffle-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<div class="discover-playlist-container compact" id="personalized-discovery-shuffle">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Build a Playlist Section -->
<div class="discover-section">
<div class="discover-section-header">
<h2 class="discover-section-title">
Build a Playlist
<span class="bp-info-toggle" onclick="document.getElementById('bp-info-panel').classList.toggle('visible')" title="How it works">?</span>
</h2>
<p class="discover-section-subtitle">Create a custom playlist from your favorite artists</p>
</div>
<div class="bp-info-panel" id="bp-info-panel">
<div class="bp-info-content">
<p><strong>How it works:</strong></p>
<ol>
<li>Search and select 1-5 seed artists you like</li>
<li>Hit Generate &mdash; the app finds similar artists, pulls their albums, and picks tracks at random</li>
<li>You get a fresh 50-track playlist mixing your picks with new discoveries</li>
</ol>
<p class="bp-info-note">Tip: The more seed artists you add, the more varied the playlist will be.</p>
</div>
</div>
<div class="build-playlist-container">
<!-- Artist Search -->
<div class="build-playlist-search-section">
<div class="bp-search-input-wrapper">
<svg class="bp-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input type="text" id="build-playlist-search"
placeholder="Search for an artist..."
oninput="searchBuildPlaylistArtists()"
autocomplete="off" />
<div class="bp-search-spinner" id="bp-search-spinner" style="display: none;">
<div class="loading-spinner" style="width: 18px; height: 18px; border-width: 2px;"></div>
</div>
</div>
<div id="build-playlist-search-results" class="build-playlist-search-results"></div>
</div>
<!-- Selected Artists -->
<div class="build-playlist-selected-section">
<div class="bp-selected-header">
<h3>Seed Artists</h3>
<span class="bp-selected-counter" id="bp-selected-counter">0 / 5</span>
</div>
<div id="build-playlist-selected-artists" class="build-playlist-selected-artists">
<div class="build-playlist-no-selection">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width: 32px; height: 32px; opacity: 0.4; margin-bottom: 8px;"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
<span>Search above to add seed artists</span>
</div>
</div>
</div>
<!-- Generate Button -->
<div class="build-playlist-actions">
<button id="build-playlist-generate-btn" class="build-playlist-generate-btn"
onclick="generateBuildPlaylist()" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Generate Playlist
</button>
<div id="build-playlist-loading" class="build-playlist-loading" style="display: none;">
<div class="loading-spinner"></div>
<span>Finding similar artists and building your playlist...</span>
</div>
</div>
<!-- Generated Playlist Results -->
<div id="build-playlist-results-wrapper" style="display: none;">
<!-- Playlist Header with Actions -->
<div class="discover-section-header" style="margin-top: 20px;">
<div>
<h3 id="build-playlist-results-title"
style="margin: 0; color: #fff; font-size: 18px;">Generated Playlist</h3>
<p id="build-playlist-results-subtitle"
style="margin: 4px 0 0 0; color: #999; font-size: 13px;"></p>
</div>
<div class="discover-section-actions">
<button class="action-button secondary"
onclick="openDownloadModalForBuildPlaylist()"
title="Download missing tracks">
<span class="button-icon"></span>
<span class="button-text">Download</span>
</button>
<button class="action-button primary" id="build-playlist-sync-btn"
onclick="startDiscoverPlaylistSync('build_playlist', 'Custom Playlist')"
title="Sync to media server">
<span class="button-icon"></span>
<span class="button-text">Sync</span>
</button>
</div>
</div>
<!-- Sync Status Display -->
<div class="discover-sync-status" id="build-playlist-sync-status"
style="display: none;">
<div class="sync-status-content">
<div class="sync-status-label">
<span class="sync-icon"></span>
<span>Syncing to media server...</span>
</div>
<div class="sync-status-stats">
<span class="sync-stat"><span
id="build-playlist-sync-completed">0</span></span>
<span class="sync-stat"><span
id="build-playlist-sync-pending">0</span></span>
<span class="sync-stat"><span
id="build-playlist-sync-failed">0</span></span>
<span class="sync-stat">(<span
id="build-playlist-sync-percentage">0</span>%)</span>
</div>
</div>
</div>
<!-- Metadata Display -->
<div id="build-playlist-metadata-display"></div>
<!-- Track List -->
<div id="build-playlist-results" class="discover-playlist-container compact">
<!-- Generated playlist will appear here -->
</div>
</div>
</div>
</div>
<!-- Last.fm Track Radio (hidden until Last.fm API key configured) -->
<div class="discover-section" id="lastfm-radio-section" style="display:none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">📻 Last.fm Radio</h2>
<p class="discover-section-subtitle">Search a track to generate a similar-tracks playlist</p>
</div>
</div>
<!-- Search bar -->
<div class="lastfm-radio-search" id="lastfm-radio-search-section">
<div class="lastfm-radio-search-row">
<div class="lastfm-radio-input-wrap">
<input type="text" id="lastfm-radio-input"
placeholder="Search a track to generate a radio..."
autocomplete="off"
oninput="debouncedLastfmTrackSearch(this.value)"
onkeydown="if(event.key==='Escape')clearLastfmRadioSelection()">
<div id="lastfm-radio-dropdown" class="lastfm-radio-dropdown" style="display:none;"></div>
</div>
</div>
</div>
<!-- Generated radio playlist cards -->
<div id="lastfm-radio-playlists">
<!-- Populated dynamically -->
</div>
</div>
<!-- ListenBrainz Playlists (Tabbed) -->
<div class="discover-section">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">🧠 ListenBrainz Playlists</h2>
<p class="discover-section-subtitle" id="listenbrainz-section-subtitle">Playlists from ListenBrainz</p>
</div>
<div class="discover-section-actions">
<button class="action-button primary" id="listenbrainz-refresh-btn"
onclick="refreshListenBrainzPlaylists()"
title="Refresh playlists from ListenBrainz">
<span class="button-icon">🔄</span>
<span class="button-text">Refresh</span>
</button>
</div>
</div>
<!-- ListenBrainz Tabs -->
<div class="listenbrainz-tabs" id="listenbrainz-tabs">
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading playlists...</p>
</div>
</div>
<!-- ListenBrainz Tab Content -->
<div class="listenbrainz-tab-content" id="listenbrainz-tab-content">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Time Machine (Tabbed by Decade) -->
<div class="discover-section">
<div class="discover-section-header">
<h2 class="discover-section-title">⏰ Time Machine</h2>
<p class="discover-section-subtitle">Explore music from different decades</p>
</div>
<!-- Decade Tabs (will be populated dynamically) -->
<div class="decade-tabs" id="decade-tabs">
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading decades...</p>
</div>
</div>
<!-- Decade Tab Contents (will be populated dynamically) -->
<div id="decade-tab-contents"></div>
</div>
<!-- Browse by Genre (Tabbed by Genre) -->
<div class="discover-section">
<div class="discover-section-header">
<h2 class="discover-section-title">🎵 Browse by Genre</h2>
<p class="discover-section-subtitle">Discover music by your favorite genres</p>
</div>
<!-- Genre Tabs (will be populated dynamically) -->
<div class="genre-tabs" id="genre-tabs">
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading genres...</p>
</div>
</div>
<!-- Genre Tab Contents (will be populated dynamically) -->
<div id="genre-tab-contents"></div>
</div>
</div>
</div>
<!-- Playlist Explorer Page -->
<div class="page" id="playlist-explorer-page">
<div class="page-shell explorer-container">
<!-- Header (compact) -->
<div class="dashboard-header" style="margin-bottom: 12px;"><div class="dashboard-header-sweep" aria-hidden="true"><span></span></div>
<div class="header-text">
<h2 class="header-title"><img src="/static/explorer.png" class="page-header-icon" alt=""><span>Playlist Explorer</span></h2>
</div>
</div>
<!-- Playlist picker + controls -->
<div class="explorer-playlist-picker" id="explorer-playlist-picker">
<div class="explorer-picker-top">
<div class="explorer-picker-tabs" id="explorer-picker-tabs"></div>
<div class="explorer-controls">
<div class="explorer-mode-toggle">
<button class="explorer-mode-btn active" data-mode="albums"
onclick="explorerSetMode('albums')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Albums
</button>
<button class="explorer-mode-btn" data-mode="discographies"
onclick="explorerSetMode('discographies')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
Full Discog
</button>
</div>
</div>
</div>
<div class="explorer-picker-scroll" id="explorer-picker-scroll">
<!-- Populated by JS -->
</div>
<div class="explorer-build-row">
<div class="explorer-build-hint" id="explorer-build-hint">Select a playlist above, then explore</div>
<button class="explorer-build-btn" id="explorer-build-btn"
onclick="explorerBuildTree()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Explore Selected Playlist
</button>
</div>
</div>
<!-- Action bar (sticky, appears after tree built) -->
<div class="explorer-action-bar" id="explorer-action-bar" style="display: none;">
<div class="explorer-action-left">
<span class="explorer-selection-count" id="explorer-selection-count">0 albums selected</span>
</div>
<div class="explorer-action-buttons">
<button class="btn btn--sm btn--secondary" onclick="explorerSelectAll()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
Select All
</button>
<button class="btn btn--sm btn--secondary" onclick="explorerDeselectAll()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
Deselect
</button>
<button class="btn btn--sm btn--primary" onclick="explorerAddToWishlist()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
Add to Wishlist
</button>
</div>
<span class="explorer-nav-hint">Scroll to zoom &middot; Right-drag to pan &middot; Double-click album for tracks</span>
</div>
<!-- Tree viewport -->
<div class="explorer-viewport" id="explorer-viewport">
<div class="explorer-zoom-controls">
<button class="explorer-zoom-btn" onclick="explorerZoom(0.15)" title="Zoom in">+</button>
<button class="explorer-zoom-btn" onclick="explorerZoom(-0.15)" title="Zoom out">&minus;</button>
<button class="explorer-zoom-btn" onclick="explorerFitToView()" title="Fit to view">&#11036;</button>
<button class="explorer-zoom-btn" onclick="_explorer._zoom=1; explorerZoom(0)" title="Reset zoom">1:1</button>
</div>
<div class="explorer-tree" id="explorer-tree">
<svg class="explorer-svg" id="explorer-svg"></svg>
<!-- Empty state -->
<div class="explorer-empty" id="explorer-empty">
<div class="explorer-empty-icon">
<svg viewBox="0 0 80 80" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="40" cy="12" r="8"/>
<line x1="40" y1="20" x2="40" y2="35"/>
<line x1="40" y1="35" x2="18" y2="55"/>
<line x1="40" y1="35" x2="62" y2="55"/>
<circle cx="18" cy="60" r="6"/>
<circle cx="40" cy="60" r="6"/>
<circle cx="62" cy="60" r="6"/>
<line x1="40" y1="35" x2="40" y2="54"/>
</svg>
</div>
<p class="explorer-empty-title">Select a playlist to explore</p>
<p class="explorer-empty-desc">Choose a mirrored playlist and mode above, then click Explore to build the discovery tree</p>
</div>
</div>
</div>
<!-- Building progress -->
<div class="explorer-progress" id="explorer-progress" style="display: none;">
<div class="explorer-progress-bar">
<div class="explorer-progress-fill" id="explorer-progress-fill"></div>
</div>
<span class="explorer-progress-text" id="explorer-progress-text">Building tree...</span>
</div>
</div>
</div>
<!-- Settings Page -->
<div class="page" id="settings-page">
<div class="page-shell">
<div class="dashboard-header"><div class="dashboard-header-sweep" aria-hidden="true"><span></span></div>
<div class="header-text">
<h2 class="header-title"><img src="/static/settings.png" class="page-header-icon" alt=""><span>Settings</span></h2>
<p class="header-subtitle">Configure services, downloads, and preferences</p>
</div>
<div class="header-spacer"></div>
<div class="header-actions">
<button class="save-button" onclick="document.getElementById('save-settings').click()">Save Settings</button>
</div>
</div>
<div class="settings-content">
<!-- Category Tab Bar -->
<div class="stg-tabbar">
<button class="stg-tab active" data-tab="connections" onclick="switchSettingsTab('connections')">Connections</button>
<button class="stg-tab" data-tab="downloads" onclick="switchSettingsTab('downloads')">Downloads</button>
<button class="stg-tab" data-tab="indexers" onclick="switchSettingsTab('indexers')">Indexers &amp; Downloaders</button>
<button class="stg-tab" data-tab="library" onclick="switchSettingsTab('library')">Library</button>
<button class="stg-tab" data-tab="appearance" onclick="switchSettingsTab('appearance')">Appearance</button>
<button class="stg-tab" data-tab="advanced" onclick="switchSettingsTab('advanced')">Advanced</button>
<button class="stg-tab" data-tab="logs" onclick="switchSettingsTab('logs')">Logs</button>
</div>
<!-- Settings Panels -->
<div class="settings-columns">
<!-- Left Column - API Configuration -->
<div class="settings-left-column">
<!-- API Configuration -->
<div class="settings-group" data-stg="connections">
<h3>API Configuration <button class="stg-accordion-toggle" onclick="toggleAllServiceAccordions(this)">Expand All</button></h3>
<!-- Metadata Source Selection (first — tells user what they're configuring for) -->
<div class="api-service-frame">
<h4 class="service-title" style="color: #e8e8e8;">Metadata Source</h4>
<div class="form-group">
<label>Primary metadata source:</label>
<select id="metadata-fallback-source">
<option value="spotify">Spotify</option>
<option value="spotify_free">Spotify (no auth)</option>
<option value="itunes">iTunes / Apple Music</option>
<option value="deezer">Deezer</option>
<option value="discogs">Discogs</option>
<option value="musicbrainz">MusicBrainz</option>
</select>
</div>
<div class="callback-info">
<div class="callback-help">Where artist, album, and track metadata comes from:<br>
<strong>Spotify</strong> — official Spotify; connect your account in the Spotify section below.<br>
<strong>Spotify (no auth)</strong> — the same Spotify catalog with no account needed. It's unofficial and best-effort (may break if Spotify changes its site) and can't read your personal library or playlists.<br>
<strong>Deezer</strong> / <strong>iTunes</strong> — free, no account.<br>
<strong>MusicBrainz</strong> — free, but limited to 1 request/sec.<br>
<strong>Discogs</strong> — needs a personal access token.<br><br>
Tip: if you pick <strong>Spotify (no auth)</strong> and later connect a real Spotify account, it uses the official account normally and only falls back to no-auth while Spotify is rate-limited — then switches back automatically.</div>
</div>
</div>
<!-- Spotify Settings -->
<div class="api-service-frame stg-service" data-service="spotify">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #1DB954;"></span>
<h4 class="service-title spotify-title">Spotify</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>Client ID:</label>
<input type="text" id="spotify-client-id" placeholder="Spotify Client ID">
</div>
<div class="form-group">
<label>Client Secret:</label>
<input type="password" id="spotify-client-secret"
placeholder="Spotify Client Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="spotify-redirect-uri"
placeholder="http://127.0.0.1:8888/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="spotify-callback-display">
http://127.0.0.1:8888/callback</div>
<div class="callback-help">Add this URL to your Spotify app's 'Redirect URIs' in
the Spotify Developer Dashboard</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateSpotify()">🔐
Authenticate</button>
<button class="auth-button disconnect-button" id="spotify-disconnect-btn"
onclick="disconnectSpotify()" style="display: none;">🔌
Disconnect</button>
</div>
<div class="form-group" style="margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: normal;">
<input type="checkbox" id="metadata-spotify-free-enrichment" style="width: auto; margin: 0;">
<span>Use Spotify (no auth) for background enrichment</span>
</label>
<div class="callback-info">
<div class="callback-help">Runs the background metadata enrichment worker on the no-auth Spotify source instead of this connected account — keeping bulk enrichment off your official API quota (and dodging rate-limit bans), so your account is reserved for interactive search and playlist sync. <strong>On by default.</strong> Turn off to enrich through your connected account instead. Works even with no account connected. Note: the no-auth source can't supply artist genres.</div>
</div>
</div>
</div>
</div>
<!-- iTunes Settings -->
<div class="api-service-frame stg-service" data-service="itunes">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #fc3c44;"></span>
<h4 class="service-title itunes-title">iTunes / Apple Music</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>Storefront Country:</label>
<select id="itunes-country">
<option value="US">United States (US)</option>
<option value="GB">United Kingdom (GB)</option>
<option value="CA">Canada (CA)</option>
<option value="AU">Australia (AU)</option>
<option value="DE">Germany (DE)</option>
<option value="FR">France (FR)</option>
<option value="JP">Japan (JP)</option>
<option value="KR">South Korea (KR)</option>
<option value="BR">Brazil (BR)</option>
<option value="SE">Sweden (SE)</option>
<option value="NL">Netherlands (NL)</option>
<option value="IT">Italy (IT)</option>
<option value="ES">Spain (ES)</option>
<option value="MX">Mexico (MX)</option>
<option value="IN">India (IN)</option>
<option value="RU">Russia (RU)</option>
</select>
</div>
<div class="callback-info">
<div class="callback-help">Sets the primary Apple Music storefront. Region-specific albums are auto-searched across other storefronts as fallback.</div>
</div>
</div>
</div>
<!-- Deezer OAuth Auth -->
<div class="api-service-frame stg-service" data-service="deezer">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #A238FF;"></span>
<h4 class="service-title deezer-title">Deezer (Favorites & Playlists)</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>App ID:</label>
<input type="text" id="deezer-app-id" placeholder="Deezer App ID">
</div>
<div class="form-group">
<label>App Secret:</label>
<input type="password" id="deezer-app-secret" placeholder="Deezer App Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="deezer-redirect-uri"
placeholder="http://127.0.0.1:8008/deezer/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="deezer-callback-display">
http://127.0.0.1:8008/deezer/callback</div>
<div class="callback-help">Add this URL to your Deezer app at developers.deezer.com</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateDeezer()">🔐
Authenticate</button>
</div>
<div class="callback-info" style="margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<div class="callback-help" style="margin-bottom: 8px;">Or use an <strong>ARL token</strong> for playlist access and downloads (no OAuth app needed):</div>
</div>
<div class="form-group">
<label>ARL Token:</label>
<input type="password" id="deezer-connection-arl"
placeholder="Paste your ARL cookie token here">
</div>
<div class="callback-info">
<div class="callback-help">Log into <a href="https://www.deezer.com" target="_blank" style="color: #A238FF;">deezer.com</a>
→ DevTools (F12) → Application → Cookies → copy the <code>arl</code> value. Provides playlist access + downloads.</div>
</div>
</div>
</div>
<!-- Discogs Settings -->
<div class="api-service-frame stg-service" data-service="discogs">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #e0d4b8;"></span>
<h4 class="service-title">Discogs</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>Personal Access Token:</label>
<input type="password" id="discogs-token"
placeholder="Discogs Personal Access Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your free token from <a
href="https://www.discogs.com/settings/developers" target="_blank"
style="color: #ffff64;">Discogs Developer Settings</a></div>
<div class="callback-help">Click "Generate new token" — no app registration needed. Provides 60 req/min and cover art in search results.</div>
</div>
</div>
</div>
<!-- Tidal Playlist/Metadata Auth -->
<div class="api-service-frame stg-service" data-service="tidal">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #ff6600;"></span>
<h4 class="service-title tidal-title">Tidal (Playlists & Metadata)</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>Client ID:</label>
<input type="text" id="tidal-client-id" placeholder="Tidal Client ID">
</div>
<div class="form-group">
<label>Client Secret:</label>
<input type="password" id="tidal-client-secret"
placeholder="Tidal Client Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="tidal-redirect-uri"
placeholder="http://127.0.0.1:8889/tidal/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="tidal-callback-display">
http://127.0.0.1:8889/tidal/callback</div>
<div class="callback-help">Add this URL to your Tidal app configuration</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateTidal()">🔐
Authenticate</button>
<button class="auth-button" onclick="disconnectTidal()" style="margin-left: 8px;">
Disconnect</button>
</div>
</div>
</div>
<!-- Qobuz Metadata/Enrichment Auth -->
<div class="api-service-frame stg-service" data-service="qobuz">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #4285f4;"></span>
<h4 class="service-title qobuz-title">Qobuz (Metadata & Enrichment)</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div id="qobuz-connection-logged-in" style="display: none; margin-bottom: 8px;">
<span id="qobuz-connection-user-info" class="setting-help-text" style="color: #4caf50;"></span>
<button class="auth-button" onclick="logoutQobuz()" style="margin-left: 8px;">
Disconnect
</button>
</div>
<div id="qobuz-connection-form">
<div class="form-group">
<label>Email:</label>
<input type="email" id="qobuz-connection-email" class="form-input"
placeholder="Qobuz email" autocomplete="email">
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="qobuz-connection-password" class="form-input"
placeholder="Qobuz password" autocomplete="current-password">
</div>
<div class="form-actions">
<button class="auth-button" id="qobuz-connection-login-btn" onclick="loginQobuzFromConnections()">
Connect Qobuz
</button>
<span id="qobuz-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
</div>
<div class="callback-info" style="margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<div class="callback-help" style="margin-bottom: 8px;"><strong>Alternative: Auth Token</strong> — If email/password login fails (CAPTCHA), paste your token directly:</div>
</div>
<div class="form-group">
<label>Auth Token:</label>
<input type="password" id="qobuz-connection-token" class="form-input"
placeholder="Paste your Qobuz auth token here">
</div>
<div class="form-actions">
<button class="auth-button" id="qobuz-token-login-btn" onclick="loginQobuzWithToken()">
Connect with Token
</button>
<span id="qobuz-token-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="callback-info">
<div class="callback-help">To get your token: log into <a href="https://play.qobuz.com" target="_blank" style="color: #4285f4;">play.qobuz.com</a>
→ DevTools (F12) → Network tab → find any request to <code>www.qobuz.com/api.json</code>
→ look in the request headers for <code>X-User-Auth-Token</code> → copy that value.</div>
</div>
</div>
</div>
<!-- Last.fm Settings -->
<div class="api-service-frame stg-service" data-service="lastfm">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #d51007;"></span>
<h4 class="service-title lastfm-title">Last.fm</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>API Key:</label>
<input type="password" id="lastfm-api-key"
placeholder="Last.fm API Key">
</div>
<div class="form-group">
<label>API Secret:</label>
<input type="password" id="lastfm-api-secret"
placeholder="Last.fm API Secret (required for scrobbling)">
</div>
<div class="callback-info">
<div class="callback-help">Get your API key and secret from <a
href="https://www.last.fm/api/account/create" target="_blank"
style="color: #d51007;">Last.fm API Account</a></div>
<div class="callback-help">API key: used for metadata enrichment. API secret: required for scrobbling.</div>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="lastfm-scrobble-enabled">
Scrobble plays to Last.fm
</label>
</div>
<div class="form-actions" id="lastfm-scrobble-actions">
<button class="test-button" onclick="authorizeLastfmScrobbling()">Authorize Scrobbling</button>
<span class="setting-help-text" id="lastfm-scrobble-status"></span>
</div>
</div>
</div>
<!-- Genius Settings -->
<div class="api-service-frame stg-service" data-service="genius">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #ffff64;"></span>
<h4 class="service-title genius-title">Genius</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>Client Access Token:</label>
<input type="password" id="genius-access-token"
placeholder="Genius Client Access Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your token from <a
href="https://genius.com/api-clients" target="_blank"
style="color: #ffff64;">Genius API Clients</a></div>
<div class="callback-help">Generate a "Client Access Token" — no OAuth flow needed.</div>
</div>
</div>
</div>
<!-- AcoustID Settings -->
<div class="api-service-frame stg-service" data-service="acoustid">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #ba55d3;"></span>
<h4 class="service-title acoustid-title">AcoustID Verification</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group" style="margin-bottom: 12px;">
<label class="checkbox-label"
style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="acoustid-enabled"
style="width: 16px; height: 16px;">
<span>Enable Download Verification</span>
</label>
<div style="color: #888; font-size: 0.8em; margin-top: 4px; margin-left: 24px;">
Verifies downloaded audio matches expected track using fingerprints
</div>
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="acoustid-api-key" placeholder="AcoustID API Key">
</div>
<div class="callback-info">
<div class="callback-help">Get your free API key from <a
href="https://acoustid.org/new-application" target="_blank"
style="color: #ba55d3;">AcoustID Applications</a></div>
<div class="callback-help"
style="opacity: 0.7; font-size: 0.85em; margin-top: 4px;">
The fpcalc fingerprint tool is automatically downloaded if needed.
Failed verifications move files to Quarantine folder.
</div>
</div>
<button class="test-button" onclick="clearQuarantine()" style="margin-top: 10px; background: rgba(255,82,82,0.15); border-color: rgba(255,82,82,0.3); color: #ff5252;">Clear Quarantine</button>
</div>
</div>
<!-- ListenBrainz Settings -->
<div class="api-service-frame stg-service" data-service="listenbrainz">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #eb743b;"></span>
<h4 class="service-title listenbrainz-title">ListenBrainz</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group">
<label>API Base URL:</label>
<input type="text" id="listenbrainz-base-url"
placeholder="Leave empty for official (api.listenbrainz.org)">
</div>
<div class="form-group">
<label>User Token:</label>
<input type="password" id="listenbrainz-token"
placeholder="ListenBrainz User Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your token from <a
href="https://listenbrainz.org/profile/" target="_blank"
style="color: #eb743b;">ListenBrainz Settings</a></div>
<div class="callback-help">Self-hosted? Enter your server URL (e.g. http://localhost:8093)</div>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="listenbrainz-scrobble-enabled">
Scrobble plays to ListenBrainz
</label>
</div>
</div>
</div>
<!-- Hydrabase P2P Metadata -->
<div class="api-service-frame stg-service" data-stg="connections" data-service="hydrabase">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #00b4d8;"></span>
<h4 class="service-title">Hydrabase</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<input type="hidden" id="hydrabase-enabled" value="false">
<div class="form-group">
<label>WebSocket URL:</label>
<input type="text" id="hydrabase-url" placeholder="ws://localhost:4545">
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="hydrabase-api-key" placeholder="Hydrabase API Key">
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label class="checkbox-label" style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 0;">
<input type="checkbox" id="hydrabase-auto-connect" style="width: 16px; height: 16px;">
<span>Auto-connect on startup</span>
</label>
</div>
<div class="callback-info">
<div class="callback-help">P2P metadata network. When enabled and connected, replaces Spotify/iTunes as the primary metadata source for searches.</div>
</div>
<div class="form-actions">
<button class="test-button" id="hydrabase-connect-btn" onclick="toggleHydrabaseFromSettings()">Connect</button>
<span id="hydrabase-settings-status" style="font-size: 0.82em; color: rgba(255,255,255,0.4); margin-left: 8px;"></span>
</div>
</div>
</div>
<!-- Test Connection Buttons -->
<div class="api-test-buttons">
<button class="test-button" onclick="testConnection('spotify')">Test
Spotify</button>
<button class="test-button" onclick="testConnection('tidal')">Test Tidal</button>
<button class="test-button" onclick="testConnection('listenbrainz')">Test
ListenBrainz</button>
<button class="test-button" onclick="testConnection('acoustid')">Test
AcoustID</button>
<button class="test-button" onclick="testConnection('lastfm')">Test
Last.fm</button>
<button class="test-button" onclick="testConnection('genius')">Test
Genius</button>
</div>
</div>
<!-- Server Connections -->
<div class="settings-group" data-stg="connections">
<h3>Server Connections</h3>
<!-- Server Toggle Buttons -->
<div class="server-toggle-container">
<button class="server-toggle-btn active" id="plex-toggle"
onclick="toggleServer('plex')">
<img src="https://www.plex.tv/wp-content/themes/plex/assets/img/plex-logo.svg"
alt="Plex" class="server-logo">
</button>
<button class="server-toggle-btn" id="jellyfin-toggle"
onclick="toggleServer('jellyfin')">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/jellyfin.png" alt="Jellyfin"
class="server-logo">
</button>
<button class="server-toggle-btn" id="navidrome-toggle"
onclick="toggleServer('navidrome')">
<img src="https://tweakers.net/ext/i/2007323764.png" alt="Navidrome"
class="server-logo">
Navidrome
</button>
<button class="server-toggle-btn" id="soulsync-toggle"
onclick="toggleServer('soulsync')">
<img src="/static/trans2.png" alt="SoulSync"
class="server-logo">
Standalone
</button>
</div>
<!-- Plex Settings -->
<div class="server-config-container" id="plex-container">
<div id="plex-configuration">
<div class="form-group">
<label>Plex Server URL:</label>
<input type="url" id="plex-url" placeholder="http://localhost:32400">
<div class="form-actions" id="plex-url-actions" style="justify-content:flex-end; width: 100%; flex-basis: 100%; margin-top: 8px;">
<button id="plex-autodetect-button" class="detect-button" onclick="autoDetectPlex()">Auto-detect</button>
</div>
</div>
<div class="form-group">
<label>Plex Token:</label>
<input type="password" id="plex-token" placeholder="X-Plex-Token">
<div class="form-actions" id="plex-token-actions" style="justify-content:flex-end; width: 100%; flex-basis: 100%; margin-top: 8px;">
<button id="plex-connect-button" class="test-button" onclick="testConnection('plex')">Connect</button>
</div>
</div>
<div class="form-group" id="plex-library-selector-container" style="display: none;">
<label>Music Library:</label>
<select id="plex-music-library" onchange="selectPlexLibrary()">
<option value="">Loading...</option>
</select>
<small style="color: #999; font-size: 0.9em; display: block; margin-top: 5px;">
Select which music library to use (doesn't affect config file)
</small>
</div>
<div class="form-actions">
<button id="plex-config-action-button" class="detect-button" onclick="clearPlexConfiguration()">Clear Configuration</button>
</div>
</div>
<div id="plex-setup">
<div id="plex-setup-buttons" class="form-actions">
<button id="plex-link-to-plex-button" class="detect-button" onclick="startPlexPinAuth()">Link to Plex (OAuth)</button>
<button id="plex-manual-config-button" class="detect-button" onclick="showPlexConfiguration(false, true)">Manually Configure Plex</button>
<button id="plex-view-config-button" class="detect-button view-config-button" onclick="showPlexConfiguration()">View Configuration</button>
</div>
<div id="plex-pin-auth-flow" style="display: none; padding: 16px; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; margin-top: 12px;">
<div class="form-group">
<label>Plex PIN Code</label>
<div id="plex-pin-code" style="font-size: 1.4em; font-weight: 700; margin-top: 8px;"></div>
</div>
<div class="form-group">
<div id="plex-pin-instructions" class="setting-help-text" style="margin-bottom: 8px;">
Go to <strong>https://plex.tv/link</strong> and enter the code shown above.
</div>
<div id="plex-pin-status" class="setting-help-text" style="color: #ccc;"></div>
</div>
<div class="form-actions" style="gap: 10px;">
<button class="detect-button" id="plex-pin-refresh-button" onclick="restartPlexPinAuth()">Generate New Code</button>
<button class="test-button" onclick="cancelPlexPinAuth()">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Jellyfin Settings -->
<div class="server-config-container hidden" id="jellyfin-container">
<div class="form-group">
<label>Jellyfin Server URL:</label>
<input type="url" id="jellyfin-url" placeholder="http://localhost:8096">
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="jellyfin-api-key" placeholder="Jellyfin API Key">
</div>
<div class="form-actions">
<button class="detect-button"
onclick="autoDetectJellyfin()">Auto-detect</button>
<button class="test-button" onclick="testConnection('jellyfin')">Test</button>
</div>
<div class="form-group">
<label>API Timeout (seconds):</label>
<input type="number" id="jellyfin-timeout" placeholder="120" min="15"
max="300" value="120">
<small class="settings-hint">Timeout for
bulk API requests during database sync (15-300s). Increase if your
Jellyfin server is slow to respond.</small>
</div>
<div class="form-group" id="jellyfin-user-selector-container"
style="display: none;">
<label>Jellyfin User</label>
<select id="jellyfin-user" onchange="selectJellyfinUser()">
<option value="">Select User</option>
</select>
<small>Select which user's music library to scan</small>
</div>
<div class="form-group" id="jellyfin-library-selector-container"
style="display: none;">
<label>Music Library</label>
<select id="jellyfin-music-library" onchange="selectJellyfinLibrary()">
<option value="">Select Library</option>
</select>
<small>Select which music library to use (doesn't affect config file)</small>
</div>
</div>
<!-- Navidrome Settings -->
<div class="server-config-container hidden" id="navidrome-container">
<div class="setting-help-text">
Navidrome users should enable <strong>Real Path</strong> in the Navidrome player/settings so SoulSync can resolve imported and synced files correctly.
</div>
<div class="form-group">
<label>Navidrome Server URL:</label>
<input type="url" id="navidrome-url" placeholder="http://localhost:4533">
</div>
<div class="form-group">
<label>Username:</label>
<input type="text" id="navidrome-username" placeholder="Username">
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="navidrome-password" placeholder="Password">
</div>
<div class="form-group" id="navidrome-folder-selector-container"
style="display: none;">
<label>Music Library</label>
<select id="navidrome-music-folder" onchange="selectNavidromeMusicFolder()">
<option value="">All Libraries</option>
</select>
<small>Select which music library to import from</small>
</div>
<div class="form-actions">
<button class="detect-button"
onclick="autoDetectNavidrome()">Auto-detect</button>
<button class="test-button" onclick="testConnection('navidrome')">Test</button>
</div>
</div>
<div class="server-config-container hidden" id="soulsync-container">
<div class="soulsync-standalone-info">
<img src="/static/trans2.png" alt="SoulSync" class="soulsync-standalone-logo">
<div class="soulsync-standalone-text">
<h4>Standalone Mode</h4>
<p>SoulSync manages your library directly without an external media server. Import your existing music first: move files into the import folder, then use a separate music/output folder for the processed library SoulSync creates.</p>
</div>
</div>
<div class="form-actions">
<button class="test-button" onclick="testConnection('soulsync')">Verify Output Folder</button>
</div>
</div>
<!-- Server Test Button -->
<div class="server-test-section">
<button class="test-button server-test-btn" onclick="testConnection('server')">Test
Server</button>
</div>
</div>
</div>
<!-- Right Column - Download Settings, Database, Metadata, Logging -->
<div class="settings-right-column">
<!-- Download Settings -->
<div class="settings-group" data-stg="downloads">
<h3>Download Settings</h3>
<div class="setting-help-text">
These are container-internal paths. Only modify them if you know what you're doing — incorrect values will break downloads.
</div>
<div class="form-group">
<label>Input Folder (Download Dir):</label>
<div class="path-input-group">
<input type="text" id="download-path" placeholder="./downloads" readonly>
<button class="browse-button locked" onclick="togglePathLock('download', this)">Unlock</button>
</div>
</div>
<div class="form-group">
<label>Output Folder (Music Library):</label>
<div class="path-input-group">
<input type="text" id="transfer-path" placeholder="./Transfer" readonly>
<button class="browse-button locked" onclick="togglePathLock('transfer', this)">Unlock</button>
</div>
</div>
<div class="form-group">
<label>Import Folder:</label>
<div class="path-input-group">
<input type="text" id="staging-path" placeholder="./Staging" readonly>
<button class="browse-button locked" onclick="togglePathLock('staging', this)">Unlock</button>
</div>
</div>
<div class="form-group">
<label>Music Videos Dir:</label>
<div class="path-input-group">
<input type="text" id="music-videos-path" placeholder="./MusicVideos" readonly>
<button class="browse-button locked" onclick="togglePathLock('music-videos', this)">Unlock</button>
</div>
</div>
<div class="form-group">
<label>Playlists Folder:</label>
<div class="path-input-group">
<input type="text" id="playlists-materialize-path" placeholder="./Playlists" readonly>
<button class="browse-button locked" onclick="togglePathLock('playlists-materialize', this)">Unlock</button>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Where <strong>Organize by playlist</strong> builds playlist folders — as shortcuts into your library, so songs are never stored twice. Keep this <strong>outside</strong> your music library folder so your media server doesn't scan the same tracks twice.
</div>
</div>
<div class="form-group">
<label>Playlist Folder Style:</label>
<select id="playlists-materialize-mode">
<option value="symlink">Symlinks — no extra disk, point to your library</option>
<option value="copy">Copies — real duplicates (USB sticks / players that can't follow links)</option>
</select>
<div class="setting-help-text" style="margin-top: 6px;">
Symlinks use almost no space. Copies are self-contained. Symlinks fall back to copies automatically where the filesystem can't link (Windows without privileges, some network shares, FAT/exFAT drives).
</div>
<button class="test-button" id="playlists-rebuild-btn" onclick="rebuildPlaylistFolders()" style="margin-top: 10px;">
Rebuild playlist folders now
</button>
<div class="setting-help-text" id="playlists-rebuild-status" style="margin-top: 6px;"></div>
</div>
<div class="form-group">
<label>M3U Entry Base Path:</label>
<div class="path-input-group">
<input type="text" id="m3u-entry-base-path" placeholder="e.g. /mnt/music (leave empty for relative paths)" readonly>
<button class="browse-button locked" onclick="togglePathLock('m3u-entry-base', this)">Unlock</button>
</div>
<div class="setting-help-text">
Optional prefix added to every track path in exported M3U files. Use this when your media server needs absolute paths (e.g. <code>/mnt/music</code>).
</div>
</div>
<div class="form-group">
<label>Download Source:</label>
<select id="download-source-mode" class="form-select"
onchange="updateDownloadSourceUI()">
<option value="soulseek">Soulseek Only</option>
<option value="youtube">YouTube Only</option>
<option value="tidal">Tidal Only</option>
<option value="qobuz">Qobuz Only</option>
<option value="hifi">HiFi Only (Free Lossless)</option>
<option value="deezer_dl">Deezer Only</option>
<option value="amazon">Amazon Music Only</option>
<option value="lidarr">Lidarr Only</option>
<option value="soundcloud">SoundCloud Only</option>
<option value="torrent">Torrent Only (via Prowlarr)</option>
<option value="usenet">Usenet Only (via Prowlarr)</option>
<option value="hybrid">Hybrid (Primary + Fallback)</option>
</select>
<div class="setting-help-text">
Choose where to download music from. Hybrid mode tries primary source first,
then falls back to secondary if it fails.
</div>
</div>
<div class="form-group">
<label>Stream / Preview Source:</label>
<select id="stream-source" class="form-select">
<option value="youtube">YouTube (Instant)</option>
<option value="active">Active Download Source</option>
</select>
<div class="setting-help-text">
Where to stream audio when previewing tracks. YouTube is instant and requires no setup.
"Active Download Source" uses your configured source (Tidal, Qobuz, etc.) — if that's Soulseek, YouTube is used automatically.
</div>
</div>
<div class="form-group">
<label>Concurrent Downloads:</label>
<select id="max-concurrent-downloads" class="form-select">
<option value="1">1</option>
<option value="2">2</option>
<option value="3" selected>3 (Default)</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="8">8</option>
<option value="10">10</option>
</select>
<div class="setting-help-text">
Maximum simultaneous downloads per batch. Soulseek album downloads always use 1 worker
due to per-user upload limits. Higher values speed up large playlists and wishlists.
</div>
</div>
<!-- Hybrid Mode Settings (shown only when hybrid is selected) -->
<div id="hybrid-settings-container" style="display: none;">
<div class="setting-help-text">
Drag to reorder source priority. Enable sources you want to use. Downloads try each enabled source in order.
The first source can use album-level downloads when supported; later sources stay track-level fallback.
</div>
<div class="hybrid-source-list" id="hybrid-source-list">
<!-- Populated by JS -->
</div>
<!-- Hidden selects for backward compatibility with saveSettings -->
<select id="hybrid-primary-source" style="display:none;"><option value="soulseek">Soulseek</option><option value="youtube">YouTube</option><option value="tidal">Tidal</option><option value="qobuz">Qobuz</option><option value="hifi">HiFi</option><option value="deezer_dl">Deezer</option><option value="soundcloud">SoundCloud</option></select>
<select id="hybrid-secondary-source" style="display:none;"><option value="soulseek">Soulseek</option><option value="youtube">YouTube</option><option value="tidal">Tidal</option><option value="qobuz">Qobuz</option><option value="hifi">HiFi</option><option value="deezer_dl">Deezer</option><option value="soundcloud">SoundCloud</option></select>
</div>
<!-- Soulseek Settings (shown when soulseek is active source) -->
<div id="soulseek-settings-container" style="display: none;">
<div class="form-group">
<label>slskd URL:</label>
<div class="path-input-group">
<input type="url" id="soulseek-url" placeholder="http://localhost:5030">
<button class="detect-button"
onclick="autoDetectSlskd()">Auto-detect</button>
</div>
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="soulseek-api-key" placeholder="Slskd API Key">
</div>
<div class="form-group">
<label>Search Timeout (seconds):</label>
<input type="number" id="soulseek-search-timeout" placeholder="60" min="15"
max="300" value="60">
<small class="settings-hint">How long to search
for tracks (15-300 seconds)</small>
</div>
<div class="form-group">
<label>Search Timeout Buffer (seconds):</label>
<input type="number" id="soulseek-search-timeout-buffer" placeholder="15"
min="5" max="60" value="15">
<small class="settings-hint">Extra time to wait
for late results (5-60 seconds)</small>
</div>
<div class="form-group">
<label>Minimum Delay Between Searches (seconds):</label>
<input type="number" id="soulseek-search-min-delay-seconds" placeholder="0"
min="0" max="60" value="0">
<small class="settings-hint">Forces a gap between
consecutive searches. Smooths burst patterns
that trip ISP anti-abuse (e.g. Bell Canada
cuts the WAN after rapid peer-connection
spikes). 0 disables.</small>
</div>
<div class="form-group">
<label>Minimum Peer Upload Speed:</label>
<select id="soulseek-min-peer-speed">
<option value="0">Any speed</option>
<option value="1">1 Mbps</option>
<option value="2">2 Mbps</option>
<option value="3">3 Mbps</option>
<option value="4">4 Mbps</option>
<option value="5">5 Mbps</option>
<option value="10">10 Mbps</option>
</select>
<small class="settings-hint">Ignore search results from peers with upload speed below this threshold</small>
</div>
<div class="form-group">
<label>Max Peer Queue Length:</label>
<select id="soulseek-max-peer-queue">
<option value="0">No limit</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<small class="settings-hint">Skip peers with a queue longer than this (prefers peers with shorter queues regardless)</small>
</div>
<div class="form-group">
<label>Download Timeout (minutes):</label>
<input type="number" id="soulseek-download-timeout" placeholder="10" min="2"
max="60" value="10">
<small class="settings-hint">Abandon stuck downloads after this many minutes</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="soulseek-auto-clear-searches" checked>
Auto-clear slskd search history
</label>
<small class="settings-hint">Automatically cleans old searches from slskd when over 200 entries.
Disable if you want to keep manual searches in slskd's queue.</small>
</div>
<div class="form-actions" style="margin-top: 8px;">
<button class="test-button" onclick="testConnection('soulseek')">Test Soulseek Connection</button>
</div>
</div>
<!-- Tidal Download Settings (shown only when tidal mode is selected) -->
<div id="tidal-download-settings-container" style="display: none;">
<div class="form-group">
<label>Tidal Download Quality:</label>
<select id="tidal-download-quality" class="form-select">
<option value="low">Low (AAC 96kbps)</option>
<option value="high">High (AAC 320kbps)</option>
<option value="lossless">Lossless (FLAC 16-bit/44.1kHz)</option>
<option value="hires">HiRes (FLAC 24-bit/96kHz)</option>
</select>
<div class="setting-help-text">
Audio quality for Tidal downloads. HiRes requires a Tidal HiFi Plus subscription.
</div>
<label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="tidal-allow-fallback" checked>
Allow quality fallback
</label>
<div class="setting-help-text">
When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source.
</div>
</div>
<div class="form-group">
<label>Tidal Download Auth:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="auth-button" id="tidal-download-auth-btn" onclick="startTidalDownloadAuth()">
Link Tidal Account
</button>
<span id="tidal-download-auth-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div id="tidal-download-auth-code" style="display: none; margin-top: 8px;" class="setting-help-text">
</div>
</div>
</div>
<!-- Qobuz Settings (shown only when qobuz mode is selected) -->
<div id="qobuz-settings-container" style="display: none;">
<div class="form-group">
<label>Qobuz Download Quality:</label>
<select id="qobuz-quality" class="form-select">
<option value="mp3">MP3 320kbps</option>
<option value="lossless">Lossless (FLAC 16-bit/44.1kHz)</option>
<option value="hires">Hi-Res (FLAC 24-bit/96kHz)</option>
<option value="hires_max">Hi-Res Max (FLAC 24-bit/192kHz)</option>
</select>
<div class="setting-help-text">
Audio quality for Qobuz downloads. Hi-Res requires a Qobuz Studio or Sublime subscription.
</div>
<label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="qobuz-allow-fallback" checked>
Allow quality fallback
</label>
<div class="setting-help-text">
When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source.
</div>
</div>
<div class="form-group">
<label>Qobuz Account:</label>
<div id="qobuz-auth-logged-in" style="display: none; margin-top: 4px;">
<span id="qobuz-auth-user-info" class="setting-help-text" style="color: #4caf50;"></span>
<button class="auth-button" onclick="logoutQobuz()" style="margin-left: 8px;">
Disconnect
</button>
</div>
<div id="qobuz-auth-form" style="margin-top: 4px;">
<input type="email" id="qobuz-email" class="form-input"
placeholder="Qobuz email" autocomplete="email"
style="margin-bottom: 8px;">
<input type="password" id="qobuz-password" class="form-input"
placeholder="Qobuz password" autocomplete="current-password"
style="margin-bottom: 8px;">
<div class="form-actions">
<button class="auth-button" id="qobuz-login-btn" onclick="loginQobuz()">
Connect Qobuz
</button>
<span id="qobuz-auth-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Requires a paid Qobuz subscription. Streams are DRM-free.
</div>
<div class="callback-info" style="margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<div class="callback-help" style="margin-bottom: 8px;"><strong>Alternative: Auth Token</strong> — If email/password login fails (CAPTCHA), paste your token directly:</div>
</div>
<input type="password" id="qobuz-download-token" class="form-input"
placeholder="Paste your Qobuz auth token here"
style="margin-bottom: 8px;">
<div class="form-actions">
<button class="auth-button" id="qobuz-download-token-btn" onclick="loginQobuzWithTokenFromDownloads()">
Connect with Token
</button>
<span id="qobuz-download-token-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="callback-info">
<div class="callback-help">To get your token: log into <a href="https://play.qobuz.com" target="_blank" style="color: #4285f4;">play.qobuz.com</a>
→ DevTools (F12) → Network tab → find any request to <code>www.qobuz.com/api.json</code>
→ look for <code>X-User-Auth-Token</code> in the request headers → copy that value.</div>
</div>
</div>
</div>
<!-- HiFi Download Settings (shown only when hifi mode is selected) -->
<div id="hifi-download-settings-container" style="display: none;">
<div class="form-group">
<label>HiFi Download Quality:</label>
<select id="hifi-download-quality" class="form-select">
<option value="low">Low (AAC 96kbps)</option>
<option value="high">High (AAC 320kbps)</option>
<option value="lossless">Lossless (FLAC 16-bit/44.1kHz)</option>
<option value="hires">HiRes (FLAC 24-bit/96kHz)</option>
</select>
<div class="setting-help-text">
Audio quality for HiFi downloads. Uses public API instances — no account required.
</div>
<label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="hifi-allow-fallback" checked>
Allow quality fallback
</label>
<div class="setting-help-text">
When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source.
</div>
</div>
<div class="form-group">
<label>HiFi Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="hifi-test-btn" onclick="testHiFiConnection()">
Test Connection
</button>
<span id="hifi-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Free lossless downloads via community-run hifi-api instances. No authentication or subscription needed.
</div>
</div>
<div class="form-group">
<label>API Instances:</label>
<div id="hifi-instances-list" style="margin-bottom: 8px;"></div>
<div style="display:flex;gap:8px;margin-bottom:8px;">
<input type="url" id="hifi-new-instance" class="form-input" placeholder="https://example.com" style="flex:1;">
<button class="test-button" onclick="addHiFiInstance()">Add</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap;">
<button class="test-button" onclick="checkHiFiInstances()" id="hifi-instances-check-btn">Check All Instances</button>
<button class="test-button" onclick="restoreDefaultHiFiInstances()" id="hifi-instances-restore-btn" title="Re-add any of the built-in default instances you've removed (keeps the ones you added)">Restore Defaults</button>
</div>
<div id="hifi-instances-status-panel" style="display: none;"></div>
</div>
</div>
<!-- Deezer Download Settings (shown only when deezer_dl mode is selected) -->
<div id="deezer-download-settings-container" style="display: none;">
<div class="form-group">
<label>Deezer Download Quality:</label>
<select id="deezer-download-quality" class="form-select">
<option value="mp3_128">MP3 128kbps (Free)</option>
<option value="mp3_320">MP3 320kbps (Premium)</option>
<option value="flac">FLAC Lossless (HiFi)</option>
</select>
<div class="setting-help-text">
Audio quality for Deezer downloads. FLAC requires a Deezer HiFi subscription.
MP3 320 requires Premium or higher.
</div>
<label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="deezer-allow-fallback" checked>
Allow quality fallback
</label>
<div class="setting-help-text">
When disabled, only downloads at the exact quality selected. If unavailable, skips to the next source.
</div>
</div>
<div class="form-group">
<label>Deezer ARL Token:</label>
<input type="password" id="deezer-download-arl" class="form-input"
placeholder="Paste your ARL cookie token here">
<div class="setting-help-text">
Your ARL token authenticates with Deezer. To get it:
log into <a href="https://www.deezer.com" target="_blank" style="color: rgb(var(--accent-rgb));">deezer.com</a>
→ open browser DevTools (F12) → Application tab → Cookies → copy the <code>arl</code> value.
</div>
</div>
<div class="form-group">
<label>Deezer Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="deezer-download-test-btn" onclick="testDeezerDownloadConnection()">
Test Connection
</button>
<span id="deezer-download-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
</div>
<!-- Amazon Music Download Settings (shown only when amazon mode is selected) -->
<div id="amazon-download-settings-container" style="display: none;">
<div class="form-group">
<label>Amazon Music Quality:</label>
<select id="amazon-quality" class="form-select">
<option value="flac">FLAC Lossless (24-bit/48kHz Hi-Res)</option>
<option value="opus">Opus (320kbps)</option>
<option value="eac3">EAC3 Dolby Atmos (768kbps 5.1)</option>
</select>
<div class="setting-help-text">
Preferred codec tier. FLAC is 24-bit/48kHz Hi-Res — no subscription required.
Downloads via T2Tunes proxy.
</div>
<label class="checkbox-inline" style="margin-top: 8px;">
<input type="checkbox" id="amazon-allow-fallback" checked>
Allow quality fallback
</label>
<div class="setting-help-text">
Fall back to the next codec tier if the preferred one is unavailable.
</div>
</div>
<div class="form-group">
<label>Connection:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" onclick="testAmazonConnection()">Test Connection</button>
<span id="amazon-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="setting-help-text">
No account required — T2Tunes is a public Amazon Music proxy.
</div>
</div>
</div>
<!-- SoundCloud Download Settings -->
<div id="soundcloud-download-settings-container" style="display: none;">
<div class="form-group">
<div class="setting-help-text">
SoundCloud downloads run anonymously — no account required. Use this for DJ mixes,
remixes, and tracks that aren't on Spotify/Tidal/Deezer.
<br><br>
<strong>Quality:</strong> 128 kbps MP3 / AAC depending on the upload (anonymous tier).
SoundCloud doesn't expose lossless audio to anyone.
</div>
</div>
<div class="form-group">
<label>SoundCloud Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="soundcloud-test-btn" onclick="testSoundcloudConnection()">
Test Connection
</button>
<span id="soundcloud-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
</div>
<!-- Lidarr Download Settings -->
<div id="lidarr-download-settings-container" style="display: none;">
<div class="form-group">
<label>Lidarr URL:</label>
<input type="text" id="lidarr-url" placeholder="http://localhost:8686">
<div class="setting-help-text">
Full URL to your Lidarr instance (e.g. http://192.168.1.100:8686)
</div>
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="lidarr-api-key" placeholder="Your Lidarr API key">
<div class="setting-help-text">
Found in Lidarr → Settings → General → Security → API Key
</div>
</div>
<div class="form-group">
<label>Lidarr Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="lidarr-test-btn" onclick="testLidarrConnection()">
Test Connection
</button>
<span id="lidarr-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Uses Lidarr's indexers and download clients (Usenet/torrent) for album downloads.
<br><br>
<strong>Best for full-album grabs.</strong> Lidarr is album-first by design — track-name searches usually return nothing useful, so in hybrid mode Lidarr is mostly a no-op for playlists and falls through to your other sources. Use Lidarr Only when you specifically want to grab whole albums via your indexers.
</div>
</div>
</div>
<!-- Torrent / Usenet Settings — settings live on the Indexers tab, just point the user there -->
<div id="prowlarr-source-redirect" style="display: none;">
<div class="form-group">
<div class="setting-help-text" style="padding: 12px 14px; background: rgba(var(--accent-rgb), 0.06); border: 1px solid rgba(var(--accent-rgb), 0.2); border-radius: 10px;">
📡 Torrent and Usenet downloads run through your Prowlarr indexers and your configured downloader. Set up Prowlarr + the torrent / usenet client on the <strong>Indexers &amp; Downloaders</strong> tab. Once Test Connection is green on both, this source is ready to use.
</div>
</div>
</div>
<!-- YouTube Settings (shown when youtube or hybrid mode) -->
<div id="youtube-settings-container" style="display: none;">
<div class="form-group">
<label>YouTube Browser Cookies:</label>
<select id="youtube-cookies-browser" class="form-select">
<option value="">None (No cookies)</option>
<option value="chrome">Chrome</option>
<option value="firefox">Firefox</option>
<option value="edge">Edge</option>
<option value="brave">Brave</option>
<option value="opera">Opera</option>
<option value="safari">Safari</option>
</select>
<div class="setting-help-text">
If YouTube shows "Sign in to confirm you're not a bot", select your browser here.
SoulSync will use your browser's YouTube cookies to authenticate.
</div>
</div>
<div class="form-group">
<label>YouTube Download Delay:</label>
<input type="number" id="youtube-download-delay" min="0" max="30" step="1"
value="3" placeholder="3">
<div class="setting-help-text">
Seconds between YouTube downloads to avoid bot detection. Default: 3
</div>
</div>
</div>
</div>
<!-- ═══ QUALITY PROFILE ═══ -->
<!-- Whole tile is gated as a unit (Soulseek-only + downloads tab)
by settings.js updateSourceVisibility — gating only the inner
group would leave an empty expandable shell. -->
<div id="quality-profile-tile" data-stg="downloads">
<div class="settings-section-header collapsed" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>Quality Profile</h3>
<span class="settings-section-hint">Format priorities, bitrate, bit depth</span>
</div>
<div class="settings-section-body collapsed" data-stg="downloads">
<!-- Quality Profile Settings (Soulseek only) -->
<div class="settings-group" id="quality-profile-section" data-stg="downloads">
<h3>🎵 Quality Profile</h3>
<!-- Presets -->
<div class="quality-presets">
<label>Quick Presets:</label>
<div class="preset-buttons">
<button class="preset-button" onclick="applyQualityPreset('audiophile')"
title="FLAC only, strict size constraints">
🎧 Audiophile
</button>
<button class="preset-button active" onclick="applyQualityPreset('balanced')"
title="FLAC preferred, MP3 fallback">
⚖️ Balanced
</button>
<button class="preset-button" onclick="applyQualityPreset('space_saver')"
title="MP3 preferred, smaller sizes">
💾 Space Saver
</button>
</div>
</div>
<!-- FLAC Quality -->
<div class="quality-tier">
<div class="quality-tier-header">
<label class="checkbox-label">
<input type="checkbox" id="quality-flac-enabled" checked
onchange="toggleQuality('flac')">
<span class="quality-tier-name">FLAC (Lossless)</span>
</label>
<span class="quality-tier-priority" id="priority-flac">Priority: 1</span>
</div>
<div class="quality-tier-sliders" id="sliders-flac">
<div class="slider-group">
<label>Bitrate Range:</label>
<div class="dual-slider-container">
<input type="range" class="range-slider range-slider-min" id="flac-min"
min="0" max="10000" value="500" step="100"
oninput="updateQualityRange('flac')">
<input type="range" class="range-slider range-slider-max" id="flac-max"
min="0" max="10000" value="10000" step="100"
oninput="updateQualityRange('flac')">
<div class="range-slider-track"></div>
</div>
<div class="slider-values">
<span id="flac-min-value">500 kbps</span>
<span>-</span>
<span id="flac-max-value">10000 kbps</span>
</div>
</div>
</div>
<div class="flac-bit-depth-selector" id="flac-bit-depth-selector">
<label>Bit Depth:</label>
<div class="bit-depth-buttons">
<button class="bit-depth-btn active" data-value="any" onclick="setFlacBitDepth('any')">Any</button>
<button class="bit-depth-btn" data-value="16" onclick="setFlacBitDepth('16')">16-bit</button>
<button class="bit-depth-btn" data-value="24" onclick="setFlacBitDepth('24')">24-bit</button>
</div>
<div class="flac-fallback-toggle" id="flac-fallback-toggle" style="display: none; margin-top: 6px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 0.8em; color: #ccc;">
<input type="checkbox" id="flac-bit-depth-fallback" checked onchange="setFlacBitDepthFallback(this.checked)">
Accept other bit depths as fallback
</label>
<div style="color: #888; font-size: 0.75em; margin-top: 2px;">
When enabled, accepts any FLAC rather than rejecting on bit depth mismatch
</div>
</div>
</div>
</div>
<!-- MP3 320 Quality -->
<div class="quality-tier">
<div class="quality-tier-header">
<label class="checkbox-label">
<input type="checkbox" id="quality-mp3_320-enabled" checked
onchange="toggleQuality('mp3_320')">
<span class="quality-tier-name">MP3 320 kbps</span>
</label>
<span class="quality-tier-priority" id="priority-mp3_320">Priority: 2</span>
</div>
<div class="quality-tier-sliders" id="sliders-mp3_320">
<div class="slider-group">
<label>Bitrate Range:</label>
<div class="dual-slider-container">
<input type="range" class="range-slider range-slider-min"
id="mp3_320-min" min="0" max="500" value="280" step="10"
oninput="updateQualityRange('mp3_320')">
<input type="range" class="range-slider range-slider-max"
id="mp3_320-max" min="0" max="500" value="500" step="10"
oninput="updateQualityRange('mp3_320')">
<div class="range-slider-track"></div>
</div>
<div class="slider-values">
<span id="mp3_320-min-value">280 kbps</span>
<span>-</span>
<span id="mp3_320-max-value">500 kbps</span>
</div>
</div>
</div>
</div>
<!-- MP3 256 Quality -->
<div class="quality-tier">
<div class="quality-tier-header">
<label class="checkbox-label">
<input type="checkbox" id="quality-mp3_256-enabled" checked
onchange="toggleQuality('mp3_256')">
<span class="quality-tier-name">MP3 256 kbps</span>
</label>
<span class="quality-tier-priority" id="priority-mp3_256">Priority: 3</span>
</div>
<div class="quality-tier-sliders" id="sliders-mp3_256">
<div class="slider-group">
<label>Bitrate Range:</label>
<div class="dual-slider-container">
<input type="range" class="range-slider range-slider-min"
id="mp3_256-min" min="0" max="400" value="200" step="10"
oninput="updateQualityRange('mp3_256')">
<input type="range" class="range-slider range-slider-max"
id="mp3_256-max" min="0" max="400" value="400" step="10"
oninput="updateQualityRange('mp3_256')">
<div class="range-slider-track"></div>
</div>
<div class="slider-values">
<span id="mp3_256-min-value">200 kbps</span>
<span>-</span>
<span id="mp3_256-max-value">400 kbps</span>
</div>
</div>
</div>
</div>
<!-- MP3 192 Quality -->
<div class="quality-tier">
<div class="quality-tier-header">
<label class="checkbox-label">
<input type="checkbox" id="quality-mp3_192-enabled"
onchange="toggleQuality('mp3_192')">
<span class="quality-tier-name">MP3 192 kbps</span>
</label>
<span class="quality-tier-priority" id="priority-mp3_192">Priority: 4</span>
</div>
<div class="quality-tier-sliders disabled" id="sliders-mp3_192">
<div class="slider-group">
<label>Bitrate Range:</label>
<div class="dual-slider-container">
<input type="range" class="range-slider range-slider-min"
id="mp3_192-min" min="0" max="300" value="150" step="10"
oninput="updateQualityRange('mp3_192')">
<input type="range" class="range-slider range-slider-max"
id="mp3_192-max" min="0" max="300" value="300" step="10"
oninput="updateQualityRange('mp3_192')">
<div class="range-slider-track"></div>
</div>
<div class="slider-values">
<span id="mp3_192-min-value">150 kbps</span>
<span>-</span>
<span id="mp3_192-max-value">300 kbps</span>
</div>
</div>
</div>
</div>
<!-- Fallback Option -->
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="quality-fallback-enabled" checked>
Allow fallback to any quality if preferred qualities unavailable
</label>
</div>
<div class="help-text">
<strong>How it works:</strong> Downloads try each enabled quality in priority order
(1 = highest).
MIN bitrate catches fake/transcoded files (e.g., FLAC below 500 kbps is likely a
re-encoded MP3). MAX bitrate limits hi-res files if you want to save space.
When track duration is unavailable, a generous file-size safety net is used instead.
</div>
</div>
</div><!-- end Quality Profile body -->
</div><!-- end Quality Profile tile -->
<!-- ═══ RETRY LOGIC ═══ -->
<div class="settings-section-header collapsed" data-stg="downloads" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>Retry Logic</h3>
<span class="settings-section-hint">Next-best candidate, exhaustive per-source retry</span>
</div>
<div class="settings-section-body collapsed" data-stg="downloads">
<div class="settings-group" data-stg="downloads">
<h3>🔁 Retry Logic</h3>
<small class="settings-hint" style="margin-bottom: 10px; display: block;">Controls what happens when a downloaded file is rejected (AcoustID mismatch, wrong version, or duration/integrity failure). Independent of post-processing.</small>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="retry-next-candidate" checked>
Retry next-best candidate on mismatch
</label>
<small class="settings-hint">When a download is quarantined (AcoustID mismatch or duration/integrity failure), automatically try the next-best candidate instead of failing the track. Off = quarantine and fail immediately.</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="retry-exhaustive">
Exhaustive retry (separate budget per source)
</label>
<small class="settings-hint">Give every source (Soulseek, then HiFi/Tidal/…) its own retry budget. Each source spends <em>queries × retries-per-query</em> attempts before the track moves on. Worst case across two sources can mean many downloads — use for hard-to-match tracks (e.g. CJK artist names). Requires the option above.</small>
</div>
<div class="form-group">
<label for="retries-per-query">Retries per query (per source)</label>
<input type="number" id="retries-per-query" min="1" max="20" step="1" value="5" style="width: 100px;" autocomplete="off" data-bwignore="" data-1p-ignore="" data-lpignore="true">
<small class="settings-hint">In exhaustive mode, how many candidates to try per search query per source. The per-source budget is <em>number of search queries × this value</em>. Only used when Exhaustive retry is on.</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="accept-version-mismatch-fallback">
Accept best version mismatch as last resort
</label>
<small class="settings-hint">When retries are fully exhausted and <em>every</em> candidate for a track failed the <strong>same</strong> version mismatch (e.g. only an instrumental exists), accept the best (first-tried) one instead of leaving the track missing. Only AcoustID is bypassed — integrity/duration/bit-depth checks still run, so truncated or genuinely wrong files are never let through. Off = leave such tracks failed.</small>
</div>
<div class="form-group">
<label for="version-mismatch-min-count">Minimum matching mismatches before accepting</label>
<input type="number" id="version-mismatch-min-count" min="1" max="20" step="1" value="2" style="width: 100px;" autocomplete="off" data-bwignore="" data-1p-ignore="" data-lpignore="true">
<small class="settings-hint">How many quarantined candidates must have failed the <em>same</em> version mismatch before the last-resort acceptance kicks in. Higher = more confirmation that no correct version exists. Only used when the option above is on.</small>
</div>
</div>
</div><!-- end Retry Logic body -->
<!-- ═══ INDEXERS & DOWNLOADERS ═══ -->
<!-- Intro hero — explains the search → fetch flow in one glance -->
<div class="settings-group ind-hero" data-stg="indexers">
<div class="ind-hero-title">Indexers &amp; Downloaders</div>
<div class="ind-hero-flow">
<span class="ind-hero-step"><span class="ind-hero-step-num">1</span> Indexers find releases</span>
<span class="ind-hero-arrow"></span>
<span class="ind-hero-step"><span class="ind-hero-step-num">2</span> Downloader fetches them</span>
<span class="ind-hero-arrow"></span>
<span class="ind-hero-step"><span class="ind-hero-step-num">3</span> SoulSync imports to library</span>
</div>
<div class="ind-hero-sub">
Configure your Prowlarr instance to feed search results, and pick a torrent client and/or usenet client to handle the actual downloads. You only need to set up the protocols you actually use.
</div>
<div class="ind-hero-warning">
<div class="ind-hero-warning-title">💡 Filesystem access — easiest setup</div>
<div class="ind-hero-warning-body">
Torrent and usenet clients write files to <strong>their own download folders</strong>, not Soulseek's. SoulSync needs read access to those folders to import the files. <strong>Simplest fix:</strong> point all your downloaders (Soulseek, qBittorrent, SABnzbd / NZBGet) at the <em>same</em> download folder. One folder, one path to worry about, everything just works.
<ul>
<li><strong>Bare-metal:</strong> just set every client's download path to the same folder (e.g. your existing slskd folder). Done.</li>
<li><strong>Docker:</strong> easiest is to reuse the <code>./downloads</code> mount you already have for slskd — point qBit and SAB at <code>/downloads</code> inside their containers too. Or use the commented placeholders in <code>docker-compose.yml</code> if you want separate folders.</li>
<li><strong>Remote client (NAS / different host):</strong> mount the remote folder locally (SMB / NFS / rclone) before SoulSync can see it.</li>
</ul>
</div>
</div>
</div>
<!-- ─── INDEXERS section ─── -->
<div class="settings-section-header" data-stg="indexers" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>🔎 Indexers</h3>
<span class="settings-section-hint">Prowlarr — search aggregator</span>
<span class="ind-status-dot ind-status-dot-unknown" id="prowlarr-status-dot" title="Not tested"></span>
</div>
<div class="settings-section-body" data-stg="indexers">
<div class="settings-group" data-stg="indexers">
<div class="api-service-frame">
<h4 class="service-title prowlarr-title">⬢ Prowlarr</h4>
<div class="setting-help-text" style="margin-bottom: 14px;">
Prowlarr manages your Usenet and torrent indexers and exposes them through one API. SoulSync uses Prowlarr to search across every indexer at once for the Torrent and Usenet download sources.
<br><br>
Don't have it? Grab Prowlarr from <code>prowlarr.com</code> (or your *arr stack). You point Prowlarr at your indexers, then point SoulSync at Prowlarr.
</div>
<div class="form-group">
<label>Prowlarr URL:</label>
<input type="text" id="prowlarr-url" placeholder="http://localhost:9696">
<div class="setting-help-text">
Full URL to your Prowlarr instance (e.g. <code>http://192.168.1.100:9696</code>).
</div>
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="prowlarr-api-key" placeholder="Your Prowlarr API key">
<div class="setting-help-text">
Found in Prowlarr → Settings → General → Security → API Key.
</div>
</div>
<div class="form-group">
<label>Restrict to indexer IDs (optional):</label>
<input type="text" id="prowlarr-indexer-ids" placeholder="e.g. 1,3,7 (blank = all enabled indexers)">
<div class="setting-help-text">
Comma-separated Prowlarr indexer IDs. Leave blank to search every enabled indexer. Restrict when you have a private tracker you want to prioritise or a noisy public one to exclude.
</div>
</div>
<div class="form-group">
<label>Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="prowlarr-test-btn" onclick="testProwlarrConnection()">
Test Connection
</button>
<button class="test-button" id="prowlarr-refresh-indexers-btn" onclick="loadProwlarrIndexers()" style="margin-left: 6px;">
Refresh Indexer List
</button>
<span id="prowlarr-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
<div class="form-group">
<label>Configured Indexers:</label>
<div id="prowlarr-indexer-list" class="ind-indexer-list">
Connect to Prowlarr and click <strong>Refresh Indexer List</strong> to see the indexers SoulSync can search.
</div>
</div>
</div>
</div>
</div>
<!-- ─── TORRENT CLIENT section ─── -->
<div class="settings-section-header collapsed" data-stg="indexers" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>🧲 Torrent Client</h3>
<span class="settings-section-hint">qBittorrent · Transmission · Deluge</span>
<span class="ind-status-dot ind-status-dot-unknown" id="torrent-client-status-dot" title="Not tested"></span>
</div>
<div class="settings-section-body collapsed" data-stg="indexers">
<div class="settings-group" data-stg="indexers">
<div class="api-service-frame">
<h4 class="service-title torrent-title">⬢ Torrent Downloader</h4>
<div class="setting-help-text" style="margin-bottom: 14px;">
Where SoulSync sends torrents once Prowlarr finds them. Pick the client you already use — only one can be active at a time. Add the URL of the client's WebUI, log in if it asks for credentials, and hit Test Connection.
</div>
<div class="form-group">
<label>Client Type:</label>
<select id="torrent-client-type" class="form-select">
<option value="qbittorrent">qBittorrent</option>
<option value="transmission">Transmission</option>
<option value="deluge">Deluge 2.x</option>
<option value="aria2">Aria2 (RPC)</option>
</select>
<div class="setting-help-text" id="torrent-client-help">
qBittorrent: WebUI port (default 8080). Transmission: RPC port (default 9091). Deluge: WebUI port (default 8112). Aria2: RPC port (default 6800) — leave Username blank and put your <code>--rpc-secret</code> in the Password field.
</div>
</div>
<div class="form-group">
<label>WebUI URL:</label>
<input type="text" id="torrent-client-url" placeholder="http://localhost:8080">
</div>
<div class="form-group" id="torrent-client-username-group">
<label>Username:</label>
<input type="text" id="torrent-client-username" placeholder="leave blank if WebUI doesn't require auth">
<div class="setting-help-text">
qBittorrent and Transmission use username + password. Deluge uses password only — paste it in the password field below.
</div>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="torrent-client-password" placeholder="WebUI password">
</div>
<div class="form-group">
<label>Category / Label:</label>
<input type="text" id="torrent-client-category" placeholder="soulsync" value="soulsync">
<div class="setting-help-text">
SoulSync tags every torrent it adds with this label so it's easy to spot in your client. Deluge needs the Label plugin installed for this to stick.
</div>
</div>
<div class="form-group">
<label>Save Path (optional):</label>
<input type="text" id="torrent-client-save-path" placeholder="leave blank to use the client's default">
<div class="setting-help-text">
Override where the torrent client writes downloads. This path is on the <strong>torrent client's</strong> machine, not SoulSync's.
</div>
</div>
<div class="form-group">
<label>Completed Downloads Path — in SoulSync (optional):</label>
<input type="text" id="torrent-download-path" placeholder="leave blank for the shared-volume default">
<div class="setting-help-text">
Where <strong>SoulSync</strong> can read finished torrents — its own in-container path. Set this when your client saves to a category folder (e.g. a "Music" category at <code>/data/downloads/music</code>) that's mounted here at a different path. SoulSync finds the release by name under this folder. Blank = use the Soulseek download/transfer folders.
</div>
</div>
<div class="form-group">
<label>Stalled torrent timeout (minutes):</label>
<input type="number" id="torrent-stall-timeout" min="0" step="1" placeholder="10">
<div class="setting-help-text">
Give up on a torrent that makes <strong>zero download progress</strong> for this long — a dead magnet stuck on "downloading metadata", or a swarm with no seeders. Without this, a dud torrent ties up a download slot for the full 6-hour limit. <strong>0 disables</strong> (wait the full limit).
</div>
</div>
<div class="form-group">
<label>When a torrent stalls:</label>
<select id="torrent-stall-action" class="form-select">
<option value="abandon">Abandon — remove it &amp; its partial data, fail the download</option>
<option value="pause">Pause — pause it in the client, leave it for me</option>
</select>
<div class="setting-help-text">
Abandon frees the slot so the next source can try. Pause keeps the torrent in your client so you can inspect or resume it manually.
</div>
</div>
<div class="form-group">
<label>Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="torrent-client-test-btn" onclick="testTorrentClientConnection()">
Test Connection
</button>
<span id="torrent-client-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
</div>
</div>
</div>
<!-- ─── USENET CLIENT section ─── -->
<div class="settings-section-header collapsed" data-stg="indexers" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>📰 Usenet Client</h3>
<span class="settings-section-hint">SABnzbd · NZBGet</span>
<span class="ind-status-dot ind-status-dot-unknown" id="usenet-client-status-dot" title="Not tested"></span>
</div>
<div class="settings-section-body collapsed" data-stg="indexers">
<div class="settings-group" data-stg="indexers">
<div class="api-service-frame">
<h4 class="service-title usenet-title">⬢ Usenet Downloader</h4>
<div class="setting-help-text" style="margin-bottom: 14px;">
Where SoulSync sends NZBs once Prowlarr finds them. Pick one usenet downloader. SABnzbd uses an API key for auth; NZBGet uses a username + password.
</div>
<div class="form-group">
<label>Client Type:</label>
<select id="usenet-client-type" class="form-select" onchange="updateUsenetClientUI()">
<option value="sabnzbd">SABnzbd</option>
<option value="nzbget">NZBGet</option>
</select>
<div class="setting-help-text">
SABnzbd: default WebUI port 8080. NZBGet: default WebUI port 6789.
</div>
</div>
<div class="form-group">
<label>WebUI URL:</label>
<input type="text" id="usenet-client-url" placeholder="http://localhost:8080">
</div>
<div class="form-group" id="usenet-apikey-group">
<label>API Key:</label>
<input type="password" id="usenet-client-api-key" placeholder="SABnzbd API key">
<div class="setting-help-text">
SABnzbd → Config → General → API Key. Used by SABnzbd only.
</div>
</div>
<div class="form-group" id="usenet-username-group" style="display: none;">
<label>Username:</label>
<input type="text" id="usenet-client-username" placeholder="NZBGet WebUI username">
</div>
<div class="form-group" id="usenet-password-group" style="display: none;">
<label>Password:</label>
<input type="password" id="usenet-client-password" placeholder="NZBGet WebUI password">
</div>
<div class="form-group">
<label>Category / Label:</label>
<input type="text" id="usenet-client-category" placeholder="soulsync" value="soulsync">
<div class="setting-help-text">
SoulSync tags every NZB with this category so it ends up in a predictable post-processing folder.
</div>
</div>
<div class="form-group">
<label>Completed Downloads Path — in SoulSync (optional):</label>
<input type="text" id="usenet-download-path" placeholder="leave blank for the shared-volume default">
<div class="setting-help-text">
Where <strong>SoulSync</strong> can read finished NZB downloads — its own in-container path. Set this when SABnzbd/NZBGet writes that category to a folder (e.g. <code>/data/downloads/music</code>) mounted here at a different path. SoulSync finds the release by name under this folder. Blank = use the Soulseek download/transfer folders.
</div>
</div>
<div class="form-group">
<label>Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="usenet-client-test-btn" onclick="testUsenetClientConnection()">
Test Connection
</button>
<span id="usenet-client-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ PATHS & ORGANIZATION ═══ -->
<div class="settings-section-header collapsed" data-stg="library" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>Paths &amp; Organization</h3>
<span class="settings-section-hint">File templates, music library paths</span>
</div>
<div class="settings-section-body collapsed" data-stg="library">
<!-- File Organization Settings -->
<div class="settings-group" data-stg="library">
<h3>📁 File Organization</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="file-organization-enabled" checked>
Enable custom file organization templates
</label>
</div>
<div class="form-group">
<label>Album Path Template:</label>
<input type="text" id="template-album-path"
placeholder="$albumartist/$albumartist - $album/$track - $title">
<small class="settings-hint">Variables: $albumartist, $artist, $artistletter, $album, $albumtype,
$title, $track, $disc (01), $discnum (1), $cdnum (CD01 on multi-disc, empty on single), $year, $quality (filename only). Use ${var} to append text: ${albumtype}s → Albums</small>
</div>
<div class="form-group">
<label>Single Path Template:</label>
<input type="text" id="template-single-path"
placeholder="$artist/$artist - $title/$title">
<small class="settings-hint">Variables: $albumartist, $artist, $artistletter, $title, $track (01), $album, $albumtype, $year, $quality (filename only). Use ${var} to append text: ${albumtype}s → Singles</small>
</div>
<!-- Playlist Path Template: hidden from the UI. Playlists now import
normally into Artist/Album and the Playlists/ folder is a symlink
view that mirrors the real library filename, so this template no
longer drives playlist-file naming. Kept in the DOM (display:none)
so settings load/save still round-trips the stored value. -->
<div class="form-group" style="display: none;">
<label>Playlist Path Template:</label>
<input type="text" id="template-playlist-path"
placeholder="$playlist/$artist - $title">
<small class="settings-hint">Variables: $playlist, $albumartist, $artist, $artistletter, $title, $year, $quality (filename only). Use ${var} to append text</small>
</div>
<div class="form-group">
<label>Music Video Path Template:</label>
<input type="text" id="template-video-path"
placeholder="$artist/$title-video">
<small class="settings-hint">Variables: $artist, $artistletter, $title, $year. Use ${var} to append text. Default: $artist/$title-video</small>
</div>
<div class="form-group">
<label>Multi-Disc Folder Label:</label>
<select id="disc-label">
<option value="Disc">Disc (e.g., Disc 1/)</option>
<option value="CD">CD (e.g., CD 1/)</option>
</select>
<small class="settings-hint">Label used for auto-created disc subfolders on multi-disc albums.</small>
</div>
<div class="form-group">
<label>Collaborative Album Artist:</label>
<select id="collab-artist-mode" class="form-select">
<option value="first">First Listed Artist (recommended)</option>
<option value="all">All Artists Combined</option>
</select>
<small class="settings-hint">How $albumartist resolves for albums with multiple artists.
"First Listed" uses only the primary artist for folder names (e.g., "Larry June" instead of "Larry June, Curren$y & The Alchemist").
Full artist list is always preserved in file metadata tags.</small>
</div>
<div class="form-group">
<label>Artist Tag Separator:</label>
<select id="artist-separator" class="form-select">
<option value=", ">, (comma)</option>
<option value="; ">; (semicolon)</option>
<option value=" / ">/ (slash)</option>
<option value=" & ">&amp; (ampersand)</option>
</select>
<small class="settings-hint">Separator between multiple artists in the ARTIST tag (e.g. <code>&amp;</code> matches MusicBrainz/Picard style)</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="write-multi-artist">
<span>Write multi-value ARTISTS tag</span>
</label>
<small class="settings-hint">Write each artist as a separate tag value for Navidrome/Jellyfin multi-artist support</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="feat-in-title">
<span>Move featured artists to title</span>
</label>
<small class="settings-hint">Keep only primary artist in ARTIST tag, append others as (feat. ...) in title</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="allow-duplicate-tracks" checked>
Allow duplicate tracks across albums
</label>
<small class="settings-hint">When enabled, the same song can be added to the wishlist from
different albums (e.g., completing a discography where albums share tracks).
When disabled, tracks with the same name and artist are skipped.</small>
</div>
<div class="form-group">
<button class="test-button" onclick="resetFileOrganizationTemplates()"
style="background: #666;">
🔄 Reset to Defaults
</button>
<small class="settings-hint">
Restores original path structure. Your downloads will be organized like before.
</small>
</div>
</div>
<!-- Music Library Paths (moved into Paths & Organization) -->
<div class="settings-group" data-stg="library">
<h3>📂 Music Library Paths</h3>
<div class="help-text" style="margin-bottom: 12px;">
Tell SoulSync where your music files live. Required for tag writing, streaming, and file detection
when your media server stores files at a different path than SoulSync can see.
<strong>Docker users:</strong> mount your music folder(s) into the SoulSync container with read-write access,
then add the <em>container-side</em> path here (e.g. <code>/music</code>).
</div>
<div id="music-paths-list"></div>
<div class="form-actions" style="margin-top: 8px;">
<button class="test-button" onclick="addMusicPathRow()">+ Add Path</button>
</div>
</div>
</div><!-- end Paths & Organization body -->
<!-- ═══ POST-PROCESSING ═══ -->
<div class="settings-section-header collapsed" data-stg="library" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>Post-Processing</h3>
<span class="settings-section-hint">Metadata, tags, conversion, lyrics</span>
</div>
<div class="settings-section-body collapsed" data-stg="library">
<!-- Metadata Enhancement Settings -->
<div class="settings-group" data-stg="library">
<h3>🎵 Post-Processing</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="metadata-enabled" checked
onchange="document.getElementById('post-processing-options').style.display = this.checked ? 'block' : 'none'">
Enable post-processing on downloaded files
</label>
<small class="settings-hint">Master toggle — disabling this skips all metadata, artwork, and lyrics processing</small>
</div>
<div id="post-processing-options">
<!-- Core Features -->
<div class="post-processing-section">
<div class="post-processing-section-title">Core Features</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="embed-album-art" checked>
Embed album art into file
</label>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="cover-art-download" checked>
Download cover.jpg to album folder
</label>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="prefer-caa-art">
Use MusicBrainz Cover Art Archive for album art
</label>
<small>Higher resolution but quality may vary. When off, uses Spotify/iTunes/Deezer art (consistent 640x640). Ignored when a preferred album art source list is set below — add Cover Art Archive to that list instead.</small>
</div>
<div class="form-group">
<label class="checkbox-label" style="cursor:default;">Preferred album art sources</label>
<small class="settings-hint">Order the sources to choose whose cover art is used. The first source that has a cover wins; misses fall through to the next, and if none match, your download's own art is kept. Only sources you're connected to are shown — leave all off to keep current behavior.</small>
<div class="hybrid-source-list" id="art-source-list"></div>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="lrclib-enabled" checked>
Generate .lrc lyrics files (LRClib)
</label>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="replaygain-enabled">
Apply ReplayGain tags after download
</label>
<small class="settings-hint">Analyzes loudness and writes ReplayGain track gain/peak tags. Requires ffmpeg. Adds a few seconds per track.</small>
</div>
<div class="form-group">
<label for="duration-tolerance-seconds">Duration tolerance (seconds)</label>
<input type="number" id="duration-tolerance-seconds" min="0" max="60" step="0.5" value="0" style="width: 100px;">
<small class="settings-hint">Maximum drift between the file's actual length and the metadata source's expected length before the file is quarantined. <strong>0 = auto</strong> (3s normal, 5s for tracks &gt;10min). Raise this if tracks routinely quarantine for being a few seconds off (live recordings, alternate masterings, etc). Capped at 60s.</small>
</div>
</div>
<!-- Tag Embedding — per-tag toggles grouped by source -->
<div class="post-processing-section">
<div class="post-processing-section-title">Tag Embedding</div>
<small class="settings-hint" style="margin-bottom: 8px; display: block;">Toggle a service to enable/disable all its tags, or expand to control individual tags.</small>
<!-- Spotify -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-spotify" checked onchange="toggleServiceTags(this, 'spotify')"> Spotify
</label>
<span class="tag-service-count">3 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.album_id" checked> Album ID</label>
</div>
</div>
<!-- iTunes -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-itunes" checked onchange="toggleServiceTags(this, 'itunes')"> iTunes
</label>
<span class="tag-service-count">3 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.album_id" checked> Album ID</label>
</div>
</div>
<!-- MusicBrainz -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-musicbrainz" checked onchange="toggleServiceTags(this, 'musicbrainz')"> MusicBrainz
</label>
<span class="tag-service-count">18 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.recording_id" checked> Recording ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_id" checked> Release ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_group_id" checked> Release Group ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.album_artist_id" checked> Album Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_track_id" checked> Release Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_type" checked> Release Type</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.original_date" checked> Original Date</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_status" checked> Release Status</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_country" checked> Release Country</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.barcode" checked> Barcode</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.media" checked> Media</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.total_discs" checked> Total Discs</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.catalog_number" checked> Catalog Number</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.script" checked> Script</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.asin" checked> ASIN</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.genres" checked> Genres</label>
</div>
</div>
<!-- Deezer -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-deezer" checked onchange="toggleServiceTags(this, 'deezer')"> Deezer
</label>
<span class="tag-service-count">4 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.bpm" checked> BPM</label>
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.isrc" checked> ISRC</label>
</div>
</div>
<!-- AudioDB -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-audiodb" checked onchange="toggleServiceTags(this, 'audiodb')"> AudioDB
</label>
<span class="tag-service-count">4 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.mood" checked> Mood</label>
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.style" checked> Style</label>
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.genre" checked> Genre</label>
</div>
</div>
<!-- Tidal -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-tidal" checked onchange="toggleServiceTags(this, 'tidal')"> Tidal
</label>
<span class="tag-service-count">5 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.copyright" checked> Copyright</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.bpm" checked> BPM</label>
</div>
</div>
<!-- Qobuz -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-qobuz" checked onchange="toggleServiceTags(this, 'qobuz')"> Qobuz
</label>
<span class="tag-service-count">5 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.copyright" checked> Copyright</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.label" checked> Label</label>
</div>
</div>
<!-- Last.fm -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-lastfm" checked onchange="toggleServiceTags(this, 'lastfm')"> Last.fm
</label>
<span class="tag-service-count">2 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="lastfm.tags.genres" checked> Genres</label>
<label class="checkbox-label"><input type="checkbox" data-config="lastfm.tags.url" checked> URL</label>
</div>
</div>
<!-- Genius -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-genius" checked onchange="toggleServiceTags(this, 'genius')"> Genius
</label>
<span class="tag-service-count">2 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="genius.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="genius.tags.url" checked> URL</label>
</div>
</div>
<!-- HiFi -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-hifi" checked onchange="toggleServiceTags(this, 'hifi')"> HiFi
</label>
<span class="tag-service-count">5 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.bpm" checked> BPM</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.copyright" checked> Copyright</label>
</div>
</div>
<!-- General -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" style="pointer-events:none;">General</label>
<span class="tag-service-count">2 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="metadata_enhancement.tags.quality_tag" checked> Quality
<small class="tag-embed-desc">Audio quality (e.g. FLAC, MP3-320)</small>
</label>
<label class="checkbox-label"><input type="checkbox" data-config="metadata_enhancement.tags.genre_merge" checked> Genre Merging
<small class="tag-embed-desc">Merge genres from Spotify + enabled sources</small>
</label>
</div>
</div>
</div>
<div class="form-group">
<label>Supported Formats:</label>
<div class="supported-formats">MP3, FLAC, MP4/M4A, OGG</div>
</div>
</div>
</div>
<!-- Post-Download Conversion Settings -->
<div class="settings-group" data-stg="library">
<h3>📀 Post-Download Conversion</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="downsample-hires">
Downsample Hi-Res FLAC to CD quality (16-bit / 44.1kHz)
</label>
</div>
<div class="help-text">
When a 24-bit or high sample rate FLAC is downloaded, it will be
automatically converted to 16-bit/44.1kHz. The hi-res original is
replaced — not kept alongside. Requires ffmpeg.
</div>
<div class="form-group" style="margin-top: 16px;">
<label class="checkbox-label">
<input type="checkbox" id="lossy-copy-enabled"
onchange="document.getElementById('lossy-copy-options').style.display = this.checked ? 'block' : 'none'">
Create lossy copy of downloaded FLAC files
</label>
</div>
<div id="lossy-copy-options" style="display: none;">
<div class="form-group">
<label>Codec:</label>
<select id="lossy-copy-codec" onchange="updateLossyBitrateOptions()">
<option value="mp3">MP3</option>
<option value="opus">Opus</option>
<option value="aac">AAC (M4A)</option>
</select>
</div>
<div class="form-group">
<label>Bitrate:</label>
<select id="lossy-copy-bitrate">
<option value="320">320 kbps</option>
<option value="256">256 kbps</option>
<option value="192">192 kbps</option>
<option value="128">128 kbps</option>
</select>
</div>
<div class="help-text">
After downloading a FLAC, a lossy copy at the selected codec and bitrate
will be created in the same folder. Requires ffmpeg.
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="lossy-copy-delete-original">
Blasphemy Mode — Delete original FLAC after conversion
</label>
</div>
<div class="help-text" style="color: rgba(255, 100, 100, 0.8);">
Warning: The original high-quality file will be permanently deleted.
Only the lossy copy will remain.
</div>
</div>
</div>
</div><!-- end Post-Processing body -->
<!-- ═══ LIBRARY PREFERENCES ═══ -->
<div class="settings-section-header collapsed" data-stg="library" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>Library Preferences</h3>
<span class="settings-section-hint">Import, content filter, playlists, stats</span>
</div>
<div class="settings-section-body collapsed" data-stg="library">
<!-- Listening Stats Settings -->
<div class="settings-group" data-stg="library">
<h3>📊 Listening Stats</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="listening-stats-enabled">
Enable listening stats collection from media server
</label>
</div>
<div class="form-group">
<label>Poll Interval (minutes):</label>
<input type="number" id="listening-stats-interval" min="5" max="1440" value="30">
</div>
<div class="help-text">
Polls your active media server (Plex, Jellyfin, or Navidrome) for play history
and displays stats on the Stats page.
</div>
</div>
</div><!-- end Library Preferences body -->
<!-- Security & Access Settings -->
<div class="settings-group" data-stg="advanced">
<h3>🔒 Security &amp; Access</h3>
<div class="setting-help-text" style="margin-bottom: 18px;">
By default, anyone who can reach SoulSync on your network can use it. Protect access below with a simple shared <strong>PIN</strong>, or full <strong>user accounts</strong> with passwords — you only need one. The last group is just for exposing SoulSync over the internet.
</div>
<!-- ── Method A: PIN ── -->
<div class="security-subgroup">
<h4 class="security-subhead">🔑 Lock with a PIN <span class="security-subhead-note">simple · one shared PIN</span></h4>
<div class="form-group" id="security-pin-setup" style="display: none;">
<label>Step 1 — Set admin PIN:</label>
<div class="setting-help-text" style="margin-bottom: 8px;">
Set a PIN before you can turn on the lock screen.
</div>
<input type="password" id="security-new-pin" placeholder="Enter PIN" maxlength="20" autocomplete="off" style="margin-bottom: 6px;">
<input type="password" id="security-confirm-pin" placeholder="Confirm PIN" maxlength="20" autocomplete="off" style="margin-bottom: 6px;">
<button class="auth-button" id="security-save-pin-btn" onclick="saveSecurityPin()">Save PIN</button>
<p id="security-pin-msg" class="setting-help-text" style="margin-top: 6px; display: none;"></p>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="security-require-pin" onchange="handleSecurityPinToggle(this)">
<span>Require PIN to access SoulSync</span>
</label>
<div class="setting-help-text">
A lock screen appears on every page load, verified against the admin PIN. Closing the tab requires re-entry.
</div>
</div>
<div class="form-group" id="security-change-pin-section" style="display: none;">
<button class="auth-button" onclick="showChangeSecurityPin()">Change PIN</button>
</div>
</div>
<!-- ── Method B: Login accounts ── -->
<div class="security-subgroup">
<h4 class="security-subhead">👤 User accounts (login) <span class="security-subhead-note">per-person · best for public access</span></h4>
<div class="setting-help-text" style="margin-bottom: 12px;">
Everyone signs in with their account name + password. Turning this on <strong>replaces</strong> the PIN and the profile picker. Set passwords for other people in <strong>Manage Profiles</strong>.
</div>
<div class="form-group">
<label>Step 1 — Admin login password:</label>
<div class="security-saved-status" id="security-login-password-status" style="display:none;">✓ A login password is set</div>
<input type="password" id="security-login-password" placeholder="Enter password (min 6)" maxlength="200" autocomplete="new-password" style="margin: 6px 0;">
<input type="password" id="security-login-password-confirm" placeholder="Confirm password" maxlength="200" autocomplete="new-password" style="margin-bottom: 6px;">
<button class="auth-button" onclick="saveLoginPassword()">Save Password</button>
<p id="security-login-password-msg" class="setting-help-text" style="margin-top: 6px; display: none;"></p>
</div>
<div class="form-group">
<label>Step 2 <span style="opacity:0.6">(recommended)</span> — Password recovery question:</label>
<div class="setting-help-text" style="margin-bottom: 8px;">
So you can reset a forgotten password by answering it on the sign-in screen.
</div>
<div class="security-saved-status" id="security-recovery-status" style="display:none;"></div>
<select id="security-recovery-question" class="form-select" onchange="handleRecoveryQuestionChange()" style="margin-bottom: 6px;">
<option value="">— Select a question —</option>
<option>What was the name of your first pet?</option>
<option>What city were you born in?</option>
<option>What was your first concert?</option>
<option>What is your all-time favorite album?</option>
<option>What was the make of your first car?</option>
<option value="__custom__">Custom question…</option>
</select>
<input type="text" id="security-recovery-custom" placeholder="Type your own question" maxlength="120" style="display:none; margin-bottom: 6px;">
<input type="text" id="security-recovery-answer" placeholder="Your answer" maxlength="120" autocomplete="off" style="margin-bottom: 6px;">
<button class="auth-button" onclick="saveRecoveryQuestion()">Save Recovery Question</button>
<p id="security-recovery-msg" class="setting-help-text" style="margin-top: 6px; display: none;"></p>
</div>
<div class="form-group" id="security-login-toggle-wrap">
<label class="toggle-label">
<input type="checkbox" id="security-require-login">
<span>Step 3 — Require login (username + password)</span>
</label>
<div class="setting-help-text" id="security-require-login-help">
Replaces the profile picker + PIN with a sign-in screen. Best for instances exposed to the internet.
</div>
</div>
</div>
<!-- ── Reverse proxy & remote access ── -->
<div class="security-subgroup">
<h4 class="security-subhead">🌐 Reverse proxy &amp; remote access</h4>
<div class="setting-help-text" style="margin-bottom: 12px;">
Only needed if you expose SoulSync to the internet behind nginx / Caddy / Traefik. See <code>Support/REVERSE-PROXY.md</code>.
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="security-trust-proxy">
<span>Behind a reverse proxy</span>
</label>
<div class="setting-help-text">
Trusts the proxy's <code>X-Forwarded-*</code> headers, marks the session cookie HTTPS-only, and adds security headers. <strong>Leave OFF for direct / LAN access over http://</strong> — it would otherwise break plain-HTTP login. <strong>Takes effect after a restart.</strong>
</div>
</div>
<div class="form-group security-nested">
<label for="security-auth-proxy-header">↳ Auth proxy user header <span style="opacity:0.6">(optional)</span>:</label>
<input type="text" id="security-auth-proxy-header" placeholder="Remote-User" autocomplete="off" style="width: 100%; font-family: monospace; font-size: 12px;">
<div class="setting-help-text">
If an auth proxy (Authelia / Authentik / oauth2-proxy) logs users in in front of SoulSync, enter the header it sets and SoulSync trusts it. <strong>Only set this behind a proxy that strips any client-supplied copy of the header.</strong> Blank = off.
</div>
</div>
<div class="form-group">
<label for="security-cors-origins">Allowed WebSocket Origins:</label>
<textarea id="security-cors-origins" rows="3" placeholder="https://soulsync.example.com&#10;http://192.168.1.5:8888" style="width: 100%; font-family: monospace; font-size: 12px;"></textarea>
<div class="setting-help-text">
Origins (full URL, no trailing slash) allowed to open WebSocket connections — one per line or comma-separated. Empty = same-origin only (the secure default; fine for direct access and most reverse-proxy setups). Add your public domain if live updates fail behind a proxy. <code>*</code> on its own line allows any origin (insecure).
</div>
</div>
</div>
</div>
<!-- Discovery Settings -->
<div class="settings-group" data-stg="advanced">
<h3>🔍 Discovery Pool Settings</h3>
<div class="form-group">
<label>Lookback Period:</label>
<select id="discovery-lookback-period" class="form-select">
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days (Default)</option>
<option value="90">Last 90 days</option>
<option value="180">Last 6 months</option>
<option value="all">Entire discography</option>
</select>
<div class="setting-help-text">
Controls how far back to scan when adding new artists or refreshing discovery
pool.
Once scanned, artists are kept updated with new releases only.
</div>
</div>
<div class="form-group">
<label>Hemisphere:</label>
<select id="discovery-hemisphere" class="form-select">
<option value="northern">Northern Hemisphere</option>
<option value="southern">Southern Hemisphere</option>
</select>
<div class="setting-help-text">
Adjusts seasonal playlists on the Discover page to match your region.
Southern hemisphere flips seasons (e.g. December = Summer, June = Winter).
Holidays like Christmas and Halloween stay calendar-fixed.
</div>
</div>
</div>
</div>
<!-- Third Column - Database, Metadata, Playlist Sync, Logging -->
<div class="settings-third-column">
<!-- UI Appearance -->
<div class="settings-group" data-stg="appearance">
<h3>UI Appearance</h3>
<div class="form-group">
<label>Accent Color:</label>
<div class="accent-color-selector">
<select id="accent-preset" class="form-select">
<option value="#1db954">Spotify Green</option>
<option value="#1d8ab9">Ocean Blue</option>
<option value="#a78bfa">Purple</option>
<option value="#8b5cf6">Boulder Purple</option>
<option value="#f59e0b">Sunset Orange</option>
<option value="#f43f5e">Rose</option>
<option value="#14b8a6">Teal</option>
<option value="custom">Custom</option>
</select>
<div class="accent-preview-swatch" id="accent-preview-swatch"></div>
</div>
<small class="settings-hint">Changes the accent color across the entire UI</small>
</div>
<div class="form-group" id="custom-color-group" style="display:none;">
<label>Custom Color:</label>
<input type="color" id="accent-custom-color" value="#1db954">
</div>
<div class="form-group">
<label>Sidebar Visualizer:</label>
<select id="sidebar-visualizer-type" class="form-select">
<option value="bars">Bars</option>
<option value="wave">Wave</option>
<option value="spectrum">Spectrum</option>
<option value="mirror">Mirror</option>
<option value="equalizer">Equalizer</option>
<option value="none">None</option>
</select>
<small class="settings-hint">Audio-reactive visualizer on the sidebar edge when music is playing</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="particles-enabled" checked>
Background Particles
</label>
<small class="settings-hint">Animated particle effects behind each page. Disable to reduce GPU usage.</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="worker-orbs-enabled" checked>
Worker Orbs
</label>
<small class="settings-hint">Dashboard header buttons animate as floating orbs. Hover the header to expand. Desktop only.</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="reduce-effects-enabled">
Reduce Visual Effects
</label>
<small class="settings-hint">Disables backdrop blur, animations, transitions, and shadows. Significantly reduces GPU/CPU usage on low-end devices.</small>
</div>
</div>
<!-- Database Settings -->
<div class="settings-group" data-stg="advanced">
<h3>Database Settings</h3>
<div class="form-group">
<label>Concurrent Workers:</label>
<select id="max-workers">
<option value="3">3</option>
<option value="4">4</option>
<option value="5" selected>5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
</div>
<div class="help-text">Number of parallel threads for database updates. Higher values =
faster updates but more server load.</div>
</div>
<!-- Database Maintenance -->
<div class="settings-group" data-stg="advanced">
<h3>Database Maintenance</h3>
<div class="form-group" id="db-maintenance-info">
<label>Database Size:</label>
<span id="db-size-display" class="readonly-field">Loading...</span>
</div>
<div class="form-group">
<label>Free Pages:</label>
<span id="db-freepages-display" class="readonly-field">Loading...</span>
</div>
<div class="form-group">
<label>Auto-Vacuum Mode:</label>
<span id="db-autovacuum-display" class="readonly-field">Loading...</span>
</div>
<div class="form-actions" style="flex-wrap: wrap;">
<button class="test-button" id="db-vacuum-btn" onclick="runDatabaseVacuum()">Compact Database (VACUUM)</button>
<button class="test-button" id="db-incvacuum-btn" onclick="enableIncrementalVacuum()">Enable Incremental Vacuum</button>
</div>
<div class="help-text">
<strong>Compact Database</strong> rewrites the entire DB to reclaim unused space. Locks the database briefly — may take a minute on large databases.<br>
<strong>Incremental Vacuum</strong> enables automatic page reclamation. Requires a one-time full compact to activate. After that, freed pages are reclaimed in small batches automatically.
</div>
<div id="db-vacuum-status" style="display: none; margin-top: 8px; padding: 8px 12px; border-radius: 8px; font-size: 0.85em;"></div>
</div>
<!-- Content Filter Settings -->
<div class="settings-group" data-stg="library">
<h3>🔞 Content Filter</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="allow-explicit" checked>
Allow explicit content in downloads
</label>
</div>
<div class="help-text">
When disabled, tracks marked as explicit on Spotify will be skipped
during matched downloads. Does not affect manual Soulseek searches.
</div>
</div>
<!-- Genre Whitelist -->
<div class="settings-group" data-stg="library">
<h3>🎵 Genre Whitelist</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="genre-whitelist-enabled" onchange="if(typeof debouncedAutoSaveSettings==='function')debouncedAutoSaveSettings()">
Enable strict genre filtering
</label>
</div>
<div class="help-text" style="margin-bottom:12px;">
When enabled, only genres on the whitelist below are kept during enrichment. Junk tags (artist names, radio shows, playlist names) are silently dropped. When disabled, all genres pass through unchanged.
</div>
<div id="genre-whitelist-container" style="display:none">
<div class="form-group" style="margin-bottom:8px;">
<input type="text" id="genre-whitelist-search" placeholder="Search or add genre..." class="form-input" style="margin-bottom:8px;">
</div>
<div id="genre-whitelist-chips" class="genre-whitelist-chips"></div>
<div class="form-actions" style="margin-top:8px;gap:8px;">
<button class="test-button" onclick="_genreWhitelistReset()">Reset to Defaults</button>
<span id="genre-whitelist-count" style="color:rgba(255,255,255,0.4);font-size:12px;"></span>
</div>
</div>
</div>
<!-- Import Quality Settings -->
<div class="settings-group" data-stg="library">
<h3>📥 Import Settings</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="import-replace-lower-quality">
Replace lower quality files on import
</label>
</div>
<div class="help-text">
When importing from Staging, if a track already exists in your library at a lower quality
(e.g. MP3), it will be replaced with the higher quality version (e.g. FLAC).
If disabled, existing tracks are always kept and the import file is skipped.
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="import-folder-artist-override">
Use the top Staging folder as the artist
</label>
</div>
<div class="help-text">
On by default (legacy behaviour). When enabled, files staged as
<code>Artist/Album/…</code> (or <code>Artist/Albums/Album/…</code>) take the top
folder as the album artist — handy for mixtapes/compilations whose embedded tags
carry DJ names. <strong>Turn this off</strong> if you drop a mixed pile of songs
under one container folder: otherwise every file's artist gets overwritten with
that folder's name (the cause of the "soulsync" mass-mislabel). With it off, the
metadata-identified artist is always kept.
</div>
</div>
<!-- M3U Export Settings -->
<div class="settings-group" data-stg="library">
<h3>📋 M3U Playlist Export</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="m3u-export-enabled">
Auto-save M3U file when downloading playlists
</label>
</div>
<div class="help-text">
Saves an M3U playlist file alongside your music. Albums are skipped — they're already grouped by your media server.
</div>
</div>
<!-- Playlist Sync Settings -->
<div class="settings-group" data-stg="library">
<h3>Playlist Sync Settings</h3>
<div class="form-group">
<label>Sync mode:</label>
<select id="playlist-sync-mode" class="form-select">
<option value="replace">Replace — recreate the playlist each sync (default)</option>
<option value="reconcile">Reconcile — edit in place, keep image &amp; description</option>
<option value="append">Append — only add new tracks, never remove</option>
</select>
<div class="help-text">
"Replace" deletes and recreates the server playlist every sync, which wipes its custom image/description. "Reconcile" updates the same playlist in place (adds new tracks, removes ones no longer in the source) so your custom image and description survive. "Append" only ever adds.
</div>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="create-backup" checked>
Create playlist backups before sync
</label>
</div>
</div>
<!-- Logging Settings -->
<div class="settings-group" data-stg="advanced">
<h3>Logging</h3>
<div class="form-group">
<label>Log Level:</label>
<select id="log-level-select" class="form-select" onchange="changeLogLevel()">
<option value="DEBUG">DEBUG (Detailed)</option>
<option value="INFO" selected>INFO (Normal)</option>
<option value="WARNING">WARNING (Minimal)</option>
<option value="ERROR">ERROR (Critical Only)</option>
</select>
<div class="setting-help-text">
Controls the level of detail in application logs. DEBUG shows all details, INFO
shows general operations, WARNING and ERROR show only issues.
</div>
</div>
<div class="form-group">
<label>Log Path:</label>
<div class="readonly-field" id="log-path-display">logs/app.log</div>
</div>
</div>
<!-- SoulSync API Keys -->
<div class="settings-group" data-stg="advanced">
<h3>SoulSync API</h3>
<div class="api-service-frame">
<h4 class="service-title" style="color: #64ffda;">REST API Keys</h4>
<div class="setting-help-text" style="margin-bottom: 12px; color: #888; font-size: 12px;">
Generate API keys to access SoulSync remotely via the <code>/api/v1/</code> endpoints.
Keys are shown once at creation — store them securely.
</div>
<!-- Existing API Keys List -->
<div id="api-keys-list" style="margin-bottom: 12px;">
<div style="color: #666; font-size: 13px; padding: 8px 0;">Loading...</div>
</div>
<!-- Generate New Key -->
<div class="form-group" style="margin-bottom: 8px;">
<label>New Key Label:</label>
<input type="text" id="api-key-label" placeholder="e.g. Discord Bot, Home Assistant">
</div>
<div class="form-actions">
<button class="auth-button" onclick="generateApiKey()">🔑 Generate API Key</button>
</div>
<!-- Newly Generated Key Display (hidden by default) -->
<div id="api-key-generated" style="display: none; margin-top: 12px; padding: 12px; background: rgba(100, 255, 218, 0.08); border: 1px solid rgba(100, 255, 218, 0.2); border-radius: 8px;">
<div style="font-size: 11px; color: #64ffda; margin-bottom: 6px; font-weight: 600;">
Copy this key now — it won't be shown again!
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<code id="api-key-value" style="flex: 1; font-size: 12px; word-break: break-all; color: #fff; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;"></code>
<button onclick="copyApiKey()" style="padding: 6px 12px; background: rgba(100,255,218,0.15); border: 1px solid rgba(100,255,218,0.3); color: #64ffda; border-radius: 4px; cursor: pointer; white-space: nowrap;">Copy</button>
</div>
</div>
</div>
<!-- Inbound Webhook Info -->
<div class="api-service-frame" style="margin-top: 16px;">
<h4 class="service-title" style="color: #64ffda;">Inbound Request Endpoint</h4>
<div class="setting-help-text" style="margin-bottom: 10px; color: #888; font-size: 12px;">
Trigger music downloads from external tools (Discord bots, Home Assistant, curl, etc.)
using any API key above.
</div>
<div style="background: rgba(0,0,0,0.3); border-radius: 6px; padding: 10px; font-size: 12px; color: #ccc; overflow-x: auto;">
<code style="white-space: pre; display: block;">curl -X POST http://&lt;host&gt;:8008/api/v1/request \
-H "Authorization: Bearer &lt;your-api-key&gt;" \
-H "Content-Type: application/json" \
-d '{"query": "Artist - Track Name"}'</code>
</div>
<div class="setting-help-text" style="margin-top: 8px; color: #666; font-size: 11px;">
Returns a <code>request_id</code> for status polling. Add <code>"notify_url"</code> to POST results to a webhook on completion.
Also triggers any <strong>Webhook Received</strong> automations in the Automation Hub.
</div>
</div>
</div>
<!-- Developer Mode -->
<div class="settings-group" data-stg="advanced">
<h3>🔧 Developer Mode</h3>
<div class="form-group">
<label>Password:</label>
<div style="display: flex; gap: 8px;">
<input type="password" id="dev-mode-password" placeholder="Enter dev password">
<button class="test-button" onclick="activateDevMode()">Activate</button>
</div>
<div id="dev-mode-status" style="margin-top: 8px; color: #888;">Inactive</div>
</div>
</div>
</div>
</div>
<!-- ═══ LOGS TAB ═══ -->
<div class="settings-group" data-stg="logs" style="max-width:100%;">
<div class="log-viewer-header">
<div class="log-viewer-controls">
<select id="log-viewer-source" class="log-viewer-select" onchange="_logViewerChangeSource()">
<option value="app">app.log</option>
<option value="post_processing">post_processing.log</option>
<option value="acoustid">acoustid.log</option>
<option value="source_reuse">source_reuse.log</option>
</select>
<div class="log-viewer-filters">
<button class="log-filter-btn active" data-level="" onclick="_logViewerFilterLevel(this)">All</button>
<button class="log-filter-btn lvl-debug" data-level="DEBUG" onclick="_logViewerFilterLevel(this)">Debug</button>
<button class="log-filter-btn lvl-info" data-level="INFO" onclick="_logViewerFilterLevel(this)">Info</button>
<button class="log-filter-btn lvl-warning" data-level="WARNING" onclick="_logViewerFilterLevel(this)">Warn</button>
<button class="log-filter-btn lvl-error" data-level="ERROR" onclick="_logViewerFilterLevel(this)">Error</button>
</div>
</div>
<div class="log-viewer-actions">
<input type="text" id="log-viewer-search" class="log-viewer-search" placeholder="Search logs..." oninput="_logViewerOnSearch(this)">
<button class="log-action-btn" onclick="_logViewerCopy()" title="Copy visible logs">Copy</button>
<button class="log-action-btn" onclick="_logViewerClear()" title="Clear display">Clear</button>
<label class="log-autoscroll-label">
<input type="checkbox" id="log-viewer-autoscroll" checked>
Auto-scroll
</label>
</div>
</div>
<div class="log-viewer-terminal" id="log-viewer-terminal">
<div class="log-viewer-lines" id="log-viewer-lines">
<div class="log-line log-info">Initializing log viewer...</div>
</div>
</div>
<div class="log-viewer-status">
<span id="log-viewer-line-count">0 lines</span>
<span id="log-viewer-live-indicator" class="log-live-dot">● Live</span>
</div>
</div>
<!-- Save Button -->
<div class="settings-actions">
<button class="save-button" id="save-settings">Save Settings</button>
</div>
</div>
</div>
</div>
<!-- Hydrabase Page (Dev Mode Only) -->
<div class="page" id="hydrabase-page">
<div class="page-header">
<h2><img src="/static/hydrabase.png?v=1" class="page-header-icon" alt=""><span>Hydrabase</span></h2>
<div style="display: flex; align-items: center; gap: 12px;">
<span id="hydra-connection-status" style="color: #888; font-size: 13px;">Disconnected</span>
<button class="test-button" id="hydra-connect-btn" onclick="hydrabaseToggleConnection()">Connect</button>
</div>
</div>
<!-- Connection Config -->
<div class="hydrabase-card" style="margin-bottom: 16px;">
<div style="display: flex; gap: 12px; align-items: end;">
<div style="flex: 2;">
<label style="font-size: 12px; color: #888; display: block; margin-bottom: 4px;">WebSocket URL</label>
<input type="text" id="hydra-ws-url" placeholder="ws://localhost:4545" value="ws://localhost:4545">
</div>
<div style="flex: 1;">
<label style="font-size: 12px; color: #888; display: block; margin-bottom: 4px;">API Key</label>
<input type="text" id="hydra-api-key" placeholder="Your Hydrabase API key">
</div>
</div>
</div>
<div class="hydrabase-container">
<!-- Left Panel — API Inputs -->
<div class="hydrabase-panel hydrabase-inputs">
<h3>API Calls</h3>
<div class="hydrabase-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0;">Tracks</h4>
<button class="test-button" onclick="hydrabaseSendRaw('hydra-payload-track')">Send</button>
</div>
<textarea class="hydra-payload" id="hydra-payload-track" rows="4">{ "request": { "type": "track", "query": "" }, "nonce": 0 }</textarea>
</div>
<div class="hydrabase-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0;">Albums</h4>
<button class="test-button" onclick="hydrabaseSendRaw('hydra-payload-album')">Send</button>
</div>
<textarea class="hydra-payload" id="hydra-payload-album" rows="4">{ "request": { "type": "album", "query": "" }, "nonce": 0 }</textarea>
</div>
<div class="hydrabase-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0;">Artists</h4>
<button class="test-button" onclick="hydrabaseSendRaw('hydra-payload-artist')">Send</button>
</div>
<textarea class="hydra-payload" id="hydra-payload-artist" rows="4">{ "request": { "type": "artists", "query": "" }, "nonce": 0 }</textarea>
</div>
<div class="hydrabase-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0;">Discography</h4>
<button class="test-button" onclick="hydrabaseSendRaw('hydra-payload-discog')">Send</button>
</div>
<textarea class="hydra-payload" id="hydra-payload-discog" rows="4">{ "request": { "type": "discography", "query": "" }, "nonce": 0 }</textarea>
</div>
<div class="hydrabase-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0;">Album Tracks</h4>
<button class="test-button" onclick="hydrabaseSendRaw('hydra-payload-albumtracks')">Send</button>
</div>
<textarea class="hydra-payload" id="hydra-payload-albumtracks" rows="4">{ "request": { "type": "album.tracks", "query": "" }, "nonce": 0 }</textarea>
</div>
</div>
<!-- Right Panel — API Responses -->
<div class="hydrabase-panel hydrabase-responses">
<h3>Response</h3>
<div class="hydrabase-response-area" id="hydra-response">API responses will appear here</div>
</div>
</div>
<!-- Network Stats -->
<div class="hydrabase-card" style="margin-top: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">Network</h3>
<span id="hydra-peer-count" style="color: #888; font-size: 13px;">Peers: --</span>
</div>
</div>
<!-- Source Comparisons -->
<div class="hydrabase-card" style="margin-top: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h3 style="margin: 0;">Source Comparisons</h3>
<button class="test-button" onclick="loadHydrabaseComparisons()">Refresh</button>
</div>
<p style="color: #888; font-size: 12px; margin: 0 0 8px 0;">When Hydrabase is the active metadata source, searches are compared against Spotify and iTunes in the background.</p>
<div id="hydra-comparisons-container">
<p style="color: #666; font-size: 13px;">No comparisons yet. Search with Hydrabase active to generate comparisons.</p>
</div>
</div>
</div>
<!-- Help & Docs Page -->
<div class="page" id="help-page">
<div class="docs-layout">
<nav class="docs-sidebar" id="docs-sidebar">
<div class="docs-sidebar-header">
<h3>Documentation</h3>
<input type="text" class="docs-search" id="docs-search-input" placeholder="Search docs..." autocomplete="off">
</div>
<div class="docs-nav" id="docs-nav">
<!-- Populated by JS -->
</div>
</nav>
<main class="docs-content" id="docs-content">
<!-- Populated by JS -->
</main>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════
TOOLS PAGE
═══════════════════════════════════════════════════════════════════ -->
<div class="page" id="tools-page">
<div class="page-shell tools-page-container">
<div class="tools-page-header">
<div class="tools-page-header-left">
<h2 class="tools-page-title">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>
Tools &amp; Operations
</h2>
<p class="tools-page-subtitle">Database management, library scanning, metadata, backups</p>
</div>
</div>
<!-- ── Library Maintenance (hero section) ── -->
<div class="tools-maintenance-hero">
<div class="tools-maintenance-header">
<div class="tools-maintenance-header-left">
<img src="/static/whisoul.png" alt="" class="tools-maintenance-logo" />
<div>
<h3 class="tools-maintenance-title">Library Maintenance</h3>
<p class="tools-maintenance-subtitle">Automated scanning, detection, and repair of library issues</p>
</div>
</div>
<label class="repair-master-toggle">
<input type="checkbox" id="repair-master-toggle" onchange="toggleRepairMaster()">
<span class="repair-toggle-slider"></span>
<span class="repair-toggle-label" id="repair-master-label">Enabled</span>
</label>
</div>
<div class="repair-tabs">
<button class="repair-tab active" data-tab="jobs" onclick="switchRepairTab('jobs')">Jobs</button>
<button class="repair-tab" data-tab="findings" onclick="switchRepairTab('findings')">
Findings <span class="repair-tab-badge" id="repair-findings-tab-badge" style="display:none">0</span>
</button>
<button class="repair-tab" data-tab="history" onclick="switchRepairTab('history')">History</button>
</div>
<div class="repair-tab-content" id="repair-tab-jobs">
<div class="repair-jobs-list" id="repair-jobs-list">
<div class="repair-loading">Loading jobs...</div>
</div>
</div>
<div class="repair-tab-content" id="repair-tab-findings" style="display:none;">
<div class="repair-findings-dashboard" id="repair-findings-dashboard"></div>
<div class="repair-findings-toolbar">
<div class="repair-findings-filters">
<select id="repair-findings-job-filter" onchange="_repairFindingsPage=0;loadRepairFindings()">
<option value="">All Jobs</option>
</select>
<select id="repair-findings-severity-filter" onchange="_repairFindingsPage=0;loadRepairFindings()">
<option value="">All Severity</option>
<option value="info">Info</option>
<option value="warning">Warning</option>
</select>
<select id="repair-findings-status-filter" onchange="_repairFindingsPage=0;_repairFindingsAutoSwitched=true;loadRepairFindings()">
<option value="pending">Pending</option>
<option value="">All Status</option>
<option value="resolved">Resolved</option>
<option value="dismissed">Dismissed</option>
</select>
</div>
<label class="repair-select-all" title="Select all on this page">
<input type="checkbox" id="repair-select-all-cb" onchange="toggleSelectAllFindings(this.checked)">
<span>Select All</span>
</label>
<div class="repair-findings-bulk" id="repair-findings-bulk" style="display:none;">
<span class="repair-bulk-count" id="repair-bulk-count"></span>
<button class="btn btn--sm btn--primary" onclick="bulkFixFindings()">Fix Selected</button>
<button class="btn btn--sm btn--secondary" onclick="bulkRepairAction('dismiss')">Dismiss Selected</button>
<button class="btn btn--sm btn--warning" id="repair-fix-all-btn" style="display:none;" onclick="fixAllMatchingFindings()">Fix All</button>
</div>
<button class="repair-clear-btn" onclick="clearRepairFindings()" title="Clear findings matching current filters">Clear Findings</button>
</div>
<div class="repair-findings-list" id="repair-findings-list">
<div class="repair-loading">Loading findings...</div>
</div>
<div class="repair-findings-pagination" id="repair-findings-pagination"></div>
</div>
<div class="repair-tab-content" id="repair-tab-history" style="display:none;">
<div class="repair-history-list" id="repair-history-list">
<div class="repair-loading">Loading history...</div>
</div>
</div>
</div>
<!-- ── Database & Scanning ── -->
<div class="tools-section">
<h3 class="tools-section-title">Database &amp; Scanning</h3>
<div class="tools-grid">
<div class="tool-card" id="db-updater-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Database Updater</h4>
<button class="tool-help-button" data-tool="db-updater"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Last Full Refresh: <span id="db-last-refresh">Never</span></p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Artists:</span>
<span class="stat-item-value" id="db-stat-artists">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Albums:</span>
<span class="stat-item-value" id="db-stat-albums">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Tracks:</span>
<span class="stat-item-value" id="db-stat-tracks">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Size:</span>
<span class="stat-item-value" id="db-stat-size">0.0 MB</span>
</div>
</div>
<div class="tool-card-controls">
<select id="db-refresh-type">
<option value="incremental">Incremental Update</option>
<option value="full">Full Refresh</option>
<option value="deep">Deep Scan</option>
</select>
<button id="db-update-button">Update Database</button>
</div>
<div class="tool-card-progress-section">
<p class="progress-phase-label" id="db-phase-label">Idle</p>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="db-progress-bar" style="width: 0%;"></div>
</div>
<p class="progress-details-label" id="db-progress-label">0 / 0 artists (0.0%)</p>
</div>
</div>
<div class="tool-card" id="quality-scanner-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Quality Scanner</h4>
<button class="tool-help-button" data-tool="quality-scanner"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Scan library for tracks below quality preferences</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Processed:</span>
<span class="stat-item-value" id="quality-stat-processed">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Quality Met:</span>
<span class="stat-item-value" id="quality-stat-met">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Low Quality:</span>
<span class="stat-item-value" id="quality-stat-low">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Matched:</span>
<span class="stat-item-value" id="quality-stat-matched">0</span>
</div>
</div>
<div class="tool-card-controls">
<select id="quality-scan-scope">
<option value="watchlist">Watchlist Artists Only</option>
<option value="all">All Library Tracks</option>
</select>
<button id="quality-scan-button">Scan Library</button>
</div>
<div class="tool-card-progress-section">
<p class="progress-phase-label" id="quality-phase-label">Ready to scan</p>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="quality-progress-bar" style="width: 0%;">
</div>
</div>
<p class="progress-details-label" id="quality-progress-label">0 / 0 tracks scanned
(0.0%)</p>
</div>
</div>
<div class="tool-card" id="reconcile-ids-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Import IDs from File Tags</h4>
<button class="tool-help-button" data-tool="reconcile-ids"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Read provider IDs (Spotify, MusicBrainz, iTunes, Deezer&hellip;) already embedded in your files and fill them into the database &mdash; lets enrichment workers skip redundant API lookups. Only fills blanks; never overwrites an existing match.</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">IDs Filled:</span>
<span class="stat-item-value" id="reconcile-stat-filled">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Rows Updated:</span>
<span class="stat-item-value" id="reconcile-stat-updated">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Conflicts:</span>
<span class="stat-item-value" id="reconcile-stat-conflicts">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Unreadable:</span>
<span class="stat-item-value" id="reconcile-stat-unreadable">0</span>
</div>
</div>
<div class="tool-card-controls">
<button id="reconcile-ids-button">Scan Library</button>
</div>
<div class="tool-card-progress-section">
<p class="progress-phase-label" id="reconcile-phase-label">Ready to scan</p>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="reconcile-progress-bar" style="width: 0%;"></div>
</div>
<p class="progress-details-label" id="reconcile-progress-label">0 / 0 files scanned (0.0%)</p>
</div>
</div>
<div class="tool-card" id="duplicate-cleaner-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Duplicate Cleaner</h4>
<button class="tool-help-button" data-tool="duplicate-cleaner"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Detect and remove duplicate tracks in output folder</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Files Scanned:</span>
<span class="stat-item-value" id="duplicate-stat-scanned">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Duplicates Found:</span>
<span class="stat-item-value" id="duplicate-stat-found">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Deleted:</span>
<span class="stat-item-value" id="duplicate-stat-deleted">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Space Freed:</span>
<span class="stat-item-value" id="duplicate-stat-space">0 MB</span>
</div>
</div>
<div class="tool-card-controls">
<button id="duplicate-clean-button">Clean Duplicates</button>
</div>
<div class="tool-card-progress-section">
<p class="progress-phase-label" id="duplicate-phase-label">Ready to scan</p>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="duplicate-progress-bar" style="width: 0%;">
</div>
</div>
<p class="progress-details-label" id="duplicate-progress-label">0 files scanned
(0.0%)</p>
</div>
</div>
<!-- media-scan moved here from below for grouping -->
<div class="tool-card" id="media-scan-card" style="display: none;">
<div class="tool-card-header">
<h4 class="tool-card-title">Media Server Scan</h4>
<button class="tool-help-button" data-tool="media-scan"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Manually trigger Plex media library scan for music</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Last Scan:</span>
<span class="stat-item-value" id="media-scan-last-time">Never</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Status:</span>
<span class="stat-item-value" id="media-scan-status">Idle</span>
</div>
</div>
<div class="tool-card-controls">
<button id="media-scan-button" class="media-scan-btn">
<span class="scan-icon">&#128225;</span>
<span class="scan-text">Scan Library</span>
</button>
</div>
<div class="tool-card-progress-section">
<p class="progress-phase-label" id="media-scan-phase-label">Ready to scan</p>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="media-scan-progress-bar" style="width: 0%;"></div>
</div>
<p class="progress-details-label" id="media-scan-progress-label">Waiting for scan request</p>
</div>
</div>
</div></div>
<!-- ── Metadata & Cache ── -->
<div class="tools-section">
<h3 class="tools-section-title">Metadata &amp; Cache</h3>
<div class="tools-grid">
<div class="tool-card" id="metadata-updater-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Metadata Updater</h4>
<button class="tool-help-button" data-tool="metadata-updater"
title="Learn more about this tool">?</button>
</div>
<p class="metadata-updater-description tool-card-info">Updates artist photos, genres,
and album art from Spotify.</p>
<div class="tool-card-controls">
<select id="metadata-refresh-interval">
<option value="180">6 months</option>
<option value="90">3 months</option>
<option value="30" selected>1 month</option>
<option value="14">2 weeks</option>
<option value="7">1 week</option>
<option value="0">Full refresh</option>
</select>
<button id="metadata-update-button">Begin Update</button>
</div>
<div class="tool-card-progress-section">
<p class="progress-phase-label" id="metadata-phase-label">Current Artist: Not
running</p>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="metadata-progress-bar" style="width: 0%;">
</div>
</div>
<p class="progress-details-label" id="metadata-progress-label">0 / 0 artists (0.0%)
</p>
</div>
</div>
<div class="tool-card" id="discovery-pool-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Discovery Pool</h4>
</div>
<p class="tool-card-info">View and fix matched/failed discovery results across all mirrored playlists</p>
<div class="tool-card-stats" id="discovery-pool-stats">
<div class="stat-item">
<span class="stat-item-label">Matched:</span>
<span class="stat-item-value" id="discovery-pool-matched-count">&mdash;</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Failed:</span>
<span class="stat-item-value" id="discovery-pool-failed-count" style="background-color: rgba(239, 68, 68, 0.15); color: #ef4444;">&mdash;</span>
</div>
</div>
<div class="tool-card-controls">
<button onclick="openDiscoveryPoolModal()">Open Discovery Pool</button>
</div>
</div>
<div class="tool-card" id="manual-library-match-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Manual Library Match</h4>
</div>
<p class="tool-card-info">Map wishlist and playlist source tracks to library tracks you already own</p>
<div class="tool-card-stats manual-library-match-stats">
<div class="stat-item">
<span class="stat-item-label">Source:</span>
<span class="stat-item-value">Wishlist / Sync</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Result:</span>
<span class="stat-item-value">Found</span>
</div>
</div>
<div class="tool-card-controls">
<button onclick="openManualLibraryMatchTool()">Open Library Match</button>
</div>
</div>
</div></div>
<!-- ── Management ── -->
<div class="tools-section">
<h3 class="tools-section-title">Management</h3>
<div class="tools-grid">
<div class="tool-card" id="backup-manager-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Backup Manager</h4>
<button class="tool-help-button" data-tool="backup-manager"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Create, download, restore and manage database backups</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Last Backup:</span>
<span class="stat-item-value" id="backup-stat-last">Never</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Backups:</span>
<span class="stat-item-value" id="backup-stat-count">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Latest Size:</span>
<span class="stat-item-value" id="backup-stat-latest-size"></span>
</div>
<div class="stat-item">
<span class="stat-item-label">DB Size:</span>
<span class="stat-item-value" id="backup-stat-db-size"></span>
</div>
</div>
<div class="tool-card-controls">
<button id="backup-now-button">Backup Now</button>
</div>
<div id="backup-list-container" class="backup-list-container"></div>
</div>
<!-- Metadata Cache Tool Card -->
<div class="tool-card" id="metadata-cache-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Metadata Cache</h4>
<button class="tool-help-button" data-tool="metadata-cache"
title="Learn more about this tool">?</button>
</div>
<p class="tool-card-info">Cached API responses from Spotify &amp; iTunes</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Artists:</span>
<span class="stat-item-value" id="mcache-stat-artists">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Albums:</span>
<span class="stat-item-value" id="mcache-stat-albums">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Tracks:</span>
<span class="stat-item-value" id="mcache-stat-tracks">0</span>
</div>
<div class="stat-item">
<span class="stat-item-label">Hits:</span>
<span class="stat-item-value" id="mcache-stat-hits">0</span>
</div>
</div>
<div class="tool-card-controls">
<button id="mcache-browse-button" onclick="openMetadataCacheModal()">Browse Cache</button>
<button onclick="openCacheHealthModal()" class="tool-card-btn-secondary">Cache Health</button>
</div>
</div>
<!-- Download Blacklist Card -->
<div class="tool-card" id="blacklist-card">
<div class="tool-card-header">
<h4 class="tool-card-title">Download Blacklist</h4>
</div>
<p class="tool-card-info">Blocked sources that won't be used for future downloads</p>
<div class="tool-card-stats">
<div class="stat-item">
<span class="stat-item-label">Blocked:</span>
<span class="stat-item-value" id="blacklist-count">0</span>
</div>
</div>
<div class="tool-card-controls">
<button onclick="openBlacklistModal()">View Blacklist</button>
</div>
</div>
</div></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════
WATCHLIST PAGE
═══════════════════════════════════════════════════════════════════ -->
<div class="page" id="watchlist-page">
<div class="page-shell watchlist-page-container">
<!-- Header -->
<div class="watchlist-page-header">
<div class="watchlist-page-header-left">
<h2 class="watchlist-page-title">
<svg width="24" height="24" viewBox="0 0 24 24" fill="rgb(var(--accent-rgb))"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
Watchlist
</h2>
<div class="watchlist-page-meta">
<span class="wl-meta-chip" id="watchlist-page-count">0 artists</span>
<span class="wl-meta-chip wl-meta-chip--accent" id="watchlist-next-auto-timer">Next Auto: --</span>
</div>
</div>
</div>
<!-- Scan status / live activity (IDs match old modal for compatibility) -->
<div id="watchlist-scan-status" class="watchlist-page-scan-status" style="display: none;">
<!-- #831 round 2: full-width live "scan deck" -->
<div id="watchlist-live-activity" class="wl-scan-deck" style="display: none;">
<div class="wl-scan-deck-head">
<span class="wl-scan-live-dot"></span>
<span class="wl-scan-live-label">Scanning</span>
<span id="wl-scan-progress-text" class="wl-scan-progress-text"></span>
<div class="wl-scan-counters">
<div class="wl-scan-counter">
<span id="wl-scan-found" class="wl-scan-counter-num">0</span>
<span class="wl-scan-counter-label">found</span>
</div>
<div class="wl-scan-counter added">
<span id="wl-scan-added" class="wl-scan-counter-num">0</span>
<span class="wl-scan-counter-label">added</span>
</div>
</div>
</div>
<div class="wl-scan-progress"><div id="wl-scan-progress-bar" class="wl-scan-progress-bar" style="width: 0%;"></div></div>
<div class="wl-scan-deck-body">
<div class="wl-scan-hero">
<div class="wl-scan-portrait">
<img id="watchlist-artist-img" class="wl-scan-portrait-img" src="" alt="" onerror="this.style.display='none';" />
<div class="wl-scan-album-thumb">
<img id="watchlist-album-img" src="" alt="" onerror="this.style.display='none';" />
</div>
</div>
<div class="wl-scan-hero-text">
<div id="watchlist-artist-name" class="wl-scan-artist-name">Waiting…</div>
<div id="wl-scan-phase" class="wl-scan-phase">starting…</div>
<div class="wl-scan-now">
<div id="watchlist-album-name" class="wl-scan-album-name"></div>
<div id="watchlist-track-name" class="wl-scan-track-name"></div>
</div>
</div>
</div>
<div class="wl-scan-feed">
<div class="wl-scan-feed-label">Added to wishlist this run</div>
<div id="watchlist-additions-feed" class="wl-scan-feed-list"></div>
</div>
</div>
</div>
<div id="watchlist-page-scan-summary" class="scan-status-summary" style="display: none;"></div>
</div>
<!-- Action buttons -->
<div class="watchlist-page-actions">
<button class="wl-chip wl-chip--cta" id="scan-watchlist-btn" onclick="startWatchlistScan()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Scan for New Releases
<span class="wl-chip-shimmer"></span>
</button>
<button class="wl-chip wl-chip--red" id="cancel-watchlist-scan-btn" onclick="cancelWatchlistScan()" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
Cancel Scan
</button>
<button class="wl-chip wl-chip--blue" id="update-similar-artists-btn" onclick="updateSimilarArtists()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Update Similar Artists
</button>
<button class="wl-chip wl-chip--slate" id="watchlist-page-settings-btn" onclick="openWatchlistGlobalSettingsModal()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
Global Settings
</button>
<button class="wl-chip wl-chip--green" id="watchlist-page-origins-btn" onclick="openDownloadOriginsModal('watchlist')" title="See every track your watchlist downloaded">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Download Origins
</button>
<button class="wl-chip wl-chip--amber" id="watchlist-page-history-btn" onclick="openWatchlistHistoryModal()" title="Every past scan and the tracks it added to the wishlist">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><polyline points="12 7 12 12 15 15"/></svg>
History
</button>
<button class="wl-chip wl-chip--red" id="watchlist-page-blocklist-btn" onclick="openBlocklistModal('artist')" title="Block artists, albums or tracks from ever being downloaded">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/></svg>
Blocklist
</button>
</div>
<!-- Global override banner -->
<div class="watchlist-global-override-banner" id="watchlist-page-override-banner" style="display: none;">
<span>&#9888;&#65039;</span>
<span>Global override is active — per-artist settings are being ignored during scans.</span>
</div>
<!-- Last scan summary -->
<div class="watchlist-last-scan-strip" id="watchlist-last-scan-strip" style="display: none;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span id="watchlist-last-scan-text">Last scan: --</span>
</div>
<!-- Search + Sort row -->
<div class="watchlist-toolbar">
<div class="watchlist-search-container">
<svg class="watchlist-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="rgba(255,255,255,0.35)"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="watchlist-search-input" class="watchlist-search-input" placeholder="Search watchlist..." oninput="filterWatchlistArtists()">
</div>
<select class="watchlist-sort-select" id="watchlist-sort-select" onchange="sortWatchlistArtists(this.value)">
<option value="name-asc">Name A-Z</option>
<option value="name-desc">Name Z-A</option>
<option value="scan-oldest">Oldest Scanned</option>
<option value="scan-newest">Recently Scanned</option>
<option value="added-newest">Recently Added</option>
</select>
</div>
<!-- Batch bar -->
<div class="watchlist-batch-bar" id="watchlist-batch-bar">
<label class="watchlist-select-all-label" onclick="event.stopPropagation();">
<input type="checkbox" id="watchlist-select-all-cb" onchange="toggleWatchlistSelectAll(this.checked)">
<span>Select All</span>
</label>
<span class="watchlist-batch-count" id="watchlist-batch-count"></span>
<button class="btn btn--secondary watchlist-batch-remove-btn" id="watchlist-batch-remove-btn" onclick="batchRemoveFromWatchlist()" style="display: none;">
Remove Selected
</button>
</div>
<!-- Artist grid -->
<div class="watchlist-artists-grid" id="watchlist-artists-list">
<!-- Populated by JS -->
</div>
<!-- Empty state -->
<div class="watchlist-page-empty" id="watchlist-page-empty" style="display: none;">
<div class="watchlist-page-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</div>
<h3>Your watchlist is empty</h3>
<p>Use Search to find an artist, then add them to your watchlist from the artist page.</p>
<button class="btn btn--primary" onclick="navigateToPage('search')">Open Search</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════
WISHLIST PAGE
═══════════════════════════════════════════════════════════════════ -->
<div class="page" id="wishlist-page">
<div class="page-shell wishlist-page-container">
<!-- Header -->
<div class="wishlist-page-header">
<div class="wishlist-page-header-left">
<h2 class="wishlist-page-title">
<span class="wishlist-page-title-icon">&#11088;</span>
Wishlist
</h2>
<div class="wishlist-page-meta">
<span class="wishlist-page-count" id="wishlist-page-count">0 tracks</span>
<span class="wishlist-page-timer" id="wishlist-next-auto-timer">Next Auto: --</span>
</div>
</div>
<div class="wishlist-page-header-right">
<button class="btn btn--secondary" onclick="cleanupWishlistOverview()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/></svg>
Cleanup
</button>
<button class="btn btn--danger" onclick="clearEntireWishlist()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
Clear All
</button>
</div>
</div>
<!-- Stats strip -->
<div class="wishlist-stats-strip" id="wishlist-stats-strip">
<div class="wishlist-stat-item">
<span class="wishlist-stat-value" id="wishlist-stat-albums">0</span>
<span class="wishlist-stat-label">Album Tracks</span>
</div>
<div class="wishlist-stat-divider"></div>
<div class="wishlist-stat-item">
<span class="wishlist-stat-value" id="wishlist-stat-singles">0</span>
<span class="wishlist-stat-label">Singles</span>
</div>
<div class="wishlist-stat-divider"></div>
<div class="wishlist-stat-item">
<span class="wishlist-stat-value wishlist-stat-cycle" id="wishlist-stat-cycle">Albums/EPs</span>
<span class="wishlist-stat-label">Next Cycle</span>
</div>
</div>
<!-- Nebula view — artist orbs with album satellites -->
<div class="wl-nebula" id="wishlist-nebula">
<!-- Action bar -->
<div class="wl-nebula-bar">
<input type="text" class="wl-nebula-search" id="wl-nebula-search" placeholder="Search artists, albums..." oninput="_filterNebula()">
<button class="btn btn--primary" onclick="_nebulaDownload()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Download Wishlist
</button>
</div>
<!-- Artist orbs container -->
<div class="wl-nebula-field" id="wl-nebula-field">
<!-- Populated by JS: artist orbs with album fans -->
</div>
</div>
<!-- Legacy track list (used by selectWishlistCategory for download flow) -->
<div id="wishlist-category-tracks" class="wishlist-category-tracks" style="display: none;">
<div class="wishlist-category-header">
<button class="wishlist-back-btn" onclick="_nebulaBack()">&#8592; Back</button>
<span id="wishlist-category-name" class="wishlist-category-name"></span>
<div class="wishlist-category-header-right">
<button class="wishlist-select-all-btn" id="wishlist-select-all-btn" onclick="toggleWishlistSelectAll()">Select All</button>
<button id="wishlist-download-btn" class="btn btn--primary" style="display: none;" onclick="downloadSelectedCategory()">
Download Selection
</button>
</div>
</div>
<div class="wishlist-batch-bar" id="wishlist-batch-bar" style="display: none;">
<span class="wishlist-batch-count" id="wishlist-batch-count">0 selected</span>
<button class="btn btn--secondary wishlist-batch-remove-btn" onclick="batchRemoveFromWishlist()">
Remove Selected
</button>
</div>
<div id="wishlist-tracks-list" class="wishlist-tracks-scroll">
<div class="loading-indicator">Loading tracks...</div>
</div>
</div>
<!-- Empty state -->
<div class="wishlist-page-empty" id="wishlist-page-empty" style="display: none;">
<div class="wishlist-page-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</div>
<h3>Your wishlist is empty</h3>
<p>Failed downloads and tracks from watchlist scans will appear here automatically.</p>
</div>
</div>
</div>
</div> <!-- /main-content -->
</div> <!-- /main-container -->
<!-- Loading Overlay -->
<div class="loading-overlay hidden" id="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-message">Processing...</div>
</div>
<!-- Toast Notifications -->
<div class="toast-container" id="toast-container"></div>
<!-- Hidden HTML5 Audio Player for Streaming -->
<audio id="audio-player" style="display: none;"></audio>
<!-- Secondary audio element used only for crossfade preloading of the next track -->
<audio id="audio-player-xfade" style="display: none;"></audio>
<!-- Expanded Now Playing Modal -->
<div class="np-modal-overlay hidden" id="np-modal-overlay">
<div class="np-modal">
<button class="np-close-btn" id="np-close-btn" title="Minimize">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="np-body">
<!-- Left: album art + track info -->
<div class="np-left">
<div class="np-album-art-container" id="np-album-art-container" title="Click for visualizer">
<img class="np-album-art hidden" id="np-album-art" src="" alt="Album Art">
<div class="np-album-art-placeholder" id="np-album-art-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/>
</svg>
</div>
<!-- Big music-synced visualizer takeover (Plexamp-style); click art to toggle -->
<div class="np-art-viz" id="np-art-viz" aria-hidden="true"></div>
<div class="np-art-hint" id="np-art-hint" title="Visualizer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="10" width="3" height="11" rx="1"/><rect x="10.5" y="4" width="3" height="17" rx="1"/><rect x="18" y="13" width="3" height="8" rx="1"/></svg>
</div>
</div>
<div class="np-track-info">
<div class="np-play-context hidden" id="np-play-context">
<span class="np-play-context-label">Playing from</span>
<span class="np-play-context-name" id="np-play-context-name"></span>
</div>
<div class="np-track-title" id="np-track-title">No track</div>
<div class="np-artist-name" id="np-artist-name">Unknown Artist</div>
<div class="np-album-name" id="np-album-name">Unknown Album</div>
<div class="np-format-badges" id="np-format-badges"></div>
</div>
<div class="np-action-buttons" id="np-action-buttons">
<a class="np-action-btn" id="np-goto-artist" title="Go to Artist" href="#" tabindex="-1"
style="text-decoration:none;color:inherit;pointer-events:none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>View Artist</span>
</a>
</div>
</div>
<!-- Right: controls -->
<div class="np-right">
<div class="np-util-row">
<button class="np-util-btn" id="np-sleep-btn" title="Sleep timer">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<span id="np-sleep-label">Sleep</span>
</button>
<button class="np-util-btn" id="np-crossfade-btn" title="Crossfade between tracks">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8l-4 4 4 4"/><path d="M17 8l4 4-4 4"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
<span>Crossfade</span>
</button>
</div>
<div class="np-progress-section">
<div class="np-progress-bar-container">
<div class="np-progress-track">
<div class="np-progress-fill" id="np-progress-fill"></div>
</div>
<input type="range" class="np-progress-bar" id="np-progress-bar" min="0" max="100" value="0" step="0.1">
<div class="np-seek-tip" id="np-seek-tip">0:00</div>
</div>
<div class="np-time-display">
<span class="np-current-time" id="np-current-time">0:00</span>
<span class="np-total-time" id="np-total-time">0:00</span>
</div>
</div>
<div class="np-controls-row">
<button class="np-btn np-btn-shuffle" id="np-shuffle-btn" title="Shuffle">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
</button>
<button class="np-btn np-btn-prev" id="np-prev-btn" title="Previous" disabled>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button>
<button class="np-btn np-btn-play" id="np-play-btn" title="Play">
<svg class="np-icon-play" width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<svg class="np-icon-pause hidden" width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
<div class="np-buffering-ring hidden" id="np-buffering-ring"></div>
</button>
<button class="np-btn np-btn-next" id="np-next-btn" title="Next" disabled>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<button class="np-btn np-btn-repeat" id="np-repeat-btn" title="Repeat">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
<span class="np-repeat-one-badge hidden" id="np-repeat-one-badge">1</span>
</button>
</div>
<div class="np-volume-row">
<button class="np-mute-btn" id="np-mute-btn" title="Mute">
<svg class="np-icon-vol" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
<svg class="np-icon-muted hidden" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>
</button>
<div class="np-volume-slider-container">
<div class="np-volume-track">
<div class="np-volume-fill" id="np-volume-fill"></div>
</div>
<input type="range" class="np-volume-slider" id="np-volume-slider" min="0" max="100" value="70">
</div>
</div>
<!-- Up-next peek (hidden when queue has no next track) -->
<div class="np-upnext hidden" id="np-upnext">
<span class="np-upnext-label">Next</span>
<img class="np-upnext-art" id="np-upnext-art" alt="">
<div class="np-upnext-info">
<div class="np-upnext-title" id="np-upnext-title"></div>
<div class="np-upnext-artist" id="np-upnext-artist"></div>
</div>
</div>
<div class="np-stop-row">
<button class="np-btn np-btn-stop" id="np-stop-btn" title="Stop playback">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>
<span>Stop</span>
</button>
</div>
<div class="np-visualizer" id="np-visualizer">
<div class="np-viz-bar"></div>
<div class="np-viz-bar"></div>
<div class="np-viz-bar"></div>
<div class="np-viz-bar"></div>
<div class="np-viz-bar"></div>
<div class="np-viz-bar"></div>
<div class="np-viz-bar"></div>
</div>
</div>
</div>
<!-- Lyrics Panel -->
<div class="np-lyrics-panel collapsed" id="np-lyrics-panel">
<div class="np-lyrics-header">
<button class="np-lyrics-toggle" id="np-lyrics-toggle" title="Toggle lyrics" aria-expanded="false">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
<span>Lyrics</span>
<span class="np-lyrics-status" id="np-lyrics-status"></span>
</button>
</div>
<div class="np-lyrics-body hidden" id="np-lyrics-body">
<div class="np-lyrics-content" id="np-lyrics-content">
<div class="np-lyrics-empty">No lyrics loaded</div>
</div>
</div>
</div>
<!-- Queue Panel -->
<div class="np-queue-panel" id="np-queue-panel">
<div class="np-queue-header">
<button class="np-queue-toggle" id="np-queue-toggle" title="Toggle queue">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
<span>Queue</span>
<span class="np-queue-count" id="np-queue-count"></span>
</button>
<div class="np-queue-header-actions">
<button class="np-radio-btn" id="np-radio-btn" title="Radio mode - auto-add similar tracks" aria-pressed="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h.01"/><path d="M8.5 16.429a5 5 0 0 1 7 0"/><path d="M5 12.859a10 10 0 0 1 14 0"/><path d="M1.5 9.289a15 15 0 0 1 21 0"/></svg>
<span class="np-radio-label">Radio</span>
<span class="np-radio-pulse"></span>
</button>
<button class="np-queue-clear-btn" id="np-queue-clear" title="Clear queue">Clear</button>
</div>
</div>
<div class="np-queue-body hidden" id="np-queue-body">
<div class="np-queue-empty" id="np-queue-empty">Queue is empty</div>
<div class="np-queue-list" id="np-queue-list"></div>
</div>
</div>
</div>
</div>
<!-- Matched Download Modal -->
<div class="modal-overlay hidden" id="matching-modal-overlay">
<div class="matching-modal" id="matching-modal">
<div class="matching-modal-header">
<h2 id="matching-modal-title">Match track download to release</h2>
<button class="matching-modal-close" onclick="closeMatchingModal()"></button>
</div>
<div class="matching-modal-content">
<!-- Artist Selection Stage -->
<div id="artist-selection-stage" class="selection-stage">
<div class="stage-header">
<h3 id="artist-stage-title">Step 1: Select the correct Artist</h3>
<p class="stage-subtitle">Choose the artist that best matches your download</p>
</div>
<div class="suggestions-section">
<h4 class="suggestions-title">Top Suggestions</h4>
<div class="suggestions-container" id="artist-suggestions">
<!-- Artist suggestion cards will be populated here -->
</div>
</div>
<div class="manual-search-section">
<h4 class="suggestions-title">Or, Search Manually</h4>
<input type="text" id="artist-search-input" class="search-input"
placeholder="Search for an artist...">
<div class="suggestions-container" id="artist-manual-results">
<!-- Manual search results will be populated here -->
</div>
</div>
</div>
<!-- Album Selection Stage (for album downloads) -->
<div id="album-selection-stage" class="selection-stage hidden">
<div class="stage-header">
<h3 id="album-stage-title">Step 2: Select the correct Album</h3>
<p class="stage-subtitle">Choose the album that best matches your download for <span
id="selected-artist-name"></span></p>
</div>
<div class="suggestions-section">
<h4 class="suggestions-title">Top Suggestions</h4>
<div class="suggestions-container" id="album-suggestions">
<!-- Album suggestion cards will be populated here -->
</div>
</div>
<div class="manual-search-section">
<h4 class="suggestions-title">Or, Search Manually</h4>
<input type="text" id="album-search-input" class="search-input"
placeholder="Search for an album...">
<div class="suggestions-container" id="album-manual-results">
<!-- Manual search results will be populated here -->
</div>
</div>
</div>
</div>
<div class="matching-modal-actions">
<button id="skip-matching-btn" class="modal-button modal-button--secondary">Skip Matching</button>
<button id="cancel-match-btn" class="modal-button modal-button--cancel">Cancel</button>
<button id="confirm-match-btn" class="modal-button modal-button--primary" disabled>Confirm
Selection</button>
</div>
</div>
</div>
<!-- Support Modal -->
<div class="support-modal-overlay hidden" id="support-modal-overlay" onclick="closeSupportModal()">
<div class="support-modal" onclick="event.stopPropagation()">
<button class="support-modal-close-btn" onclick="closeSupportModal()">&times;</button>
<div class="support-modal-logo">
<img src="/static/favicon.png" alt="SoulSync" class="support-modal-logo-img">
<h2 class="support-modal-title">SoulSync</h2>
</div>
<p class="support-modal-message">
I built this app because I wanted it, not to make money. SoulSync is and always will be free.
If it's saved you time or you just want to show some love, any support is genuinely appreciated.
</p>
<div class="support-modal-links">
<a href="https://ko-fi.com/boulderbadgedad" target="_blank" rel="noopener" class="support-link support-link-kofi">
<svg class="support-link-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09-.443-.441-3.368-3.049-4.034-3.954-.709-.965-1.041-2.7-.091-3.71.951-1.01 3.005-1.086 4.363.407 0 0 1.565-1.782 3.468-.963 1.904.82 1.832 3.011.723 4.311zm6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015z"/></svg>
<span>Ko-fi</span>
</a>
<button class="support-link support-link-btc" onclick="copyAddress('3JVWrRSkozAQSmw5DXYVxYKsM9bndPTqdS', 'Bitcoin')">
<svg class="support-link-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.546zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.7-.169-1.053-.25l.53-2.12-1.32-.33-.54 2.16c-.285-.065-.565-.13-.84-.2l-1.815-.45-.35 1.407s.975.225.955.238c.535.136.63.494.615.78l-.617 2.48c.037.01.085.025.138.047l-.14-.036-.865 3.47c-.067.165-.235.413-.615.32.015.02-.96-.24-.96-.24l-.655 1.515 1.71.426c.32.08.63.163.94.24l-.55 2.19 1.32.33.54-2.17c.36.1.705.19 1.05.273l-.535 2.16 1.32.33.55-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.753 1.67-1.907zm-3 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.225 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.548l.654-2.64c.744.186 3.137.537 2.75 2.084z"/></svg>
<span>Bitcoin</span>
</button>
<button class="support-link support-link-eth" onclick="copyAddress('0xAe2343c88a657436941181D7dD07DE83d29D24eD', 'Ethereum')">
<svg class="support-link-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 17.97L4.58 13.62 11.943 24l7.37-10.38-7.372 4.35h.003zM12.056 0L4.69 12.223l7.365 4.354 7.365-4.35L12.056 0z"/></svg>
<span>Ethereum</span>
</button>
</div>
</div>
</div>
<!-- Version Info Modal -->
<div class="version-modal-overlay hidden" id="version-modal-overlay" onclick="closeVersionModal()">
<div class="version-modal" onclick="event.stopPropagation()">
<!-- Header -->
<div class="version-modal-header">
<h2 class="version-modal-title">What's New in SoulSync</h2>
<div class="version-modal-subtitle">Version {{ soulsync_base_version }} — Latest Changes</div>
</div>
<!-- Content Area with Scroll -->
<div class="version-modal-content">
<div class="version-content-container" id="version-content-container">
<!-- Content will be populated by JavaScript -->
</div>
</div>
<!-- Footer -->
<div class="version-modal-footer">
<button class="version-modal-close" onclick="closeVersionModal()">Close</button>
</div>
</div>
</div>
<!-- Add to Wishlist Modal -->
<div class="modal-overlay hidden" id="add-to-wishlist-modal-overlay">
<div class="add-to-wishlist-modal" id="add-to-wishlist-modal">
<div class="add-to-wishlist-modal-header">
<div class="add-to-wishlist-modal-hero" id="add-to-wishlist-modal-hero">
<!-- Hero content will be dynamically populated -->
</div>
<div class="add-to-wishlist-modal-header-actions">
<span class="add-to-wishlist-modal-close" onclick="closeAddToWishlistModal()">&times;</span>
</div>
</div>
<div class="add-to-wishlist-modal-content">
<div class="add-to-wishlist-modal-body">
<div class="wishlist-track-list-container">
<div class="wishlist-track-list-header">
<h3>Tracks to Add to Wishlist</h3>
<p class="wishlist-track-list-subtitle">All tracks from this release will be added to your
wishlist</p>
</div>
<div class="wishlist-track-list" id="wishlist-track-list">
<!-- Track list will be dynamically populated -->
</div>
</div>
</div>
<div class="add-to-wishlist-modal-footer">
<div class="wishlist-modal-actions">
<button class="btn btn--secondary"
onclick="closeAddToWishlistModal()">
Close
</button>
<button class="btn btn--download" id="wishlist-download-now-btn"
onclick="handleWishlistDownloadNow()">
Download Now
</button>
<button class="btn btn--primary" id="confirm-add-to-wishlist-btn">
Add to Wishlist
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Watchlist Artist Config Modal -->
<div class="modal-overlay hidden" id="watchlist-artist-config-modal-overlay">
<div class="watchlist-artist-config-modal" id="watchlist-artist-config-modal">
<div class="watchlist-artist-config-header">
<div class="watchlist-artist-config-hero" id="watchlist-artist-config-hero">
<!-- Hero content will be dynamically populated -->
</div>
<span class="watchlist-artist-config-close" onclick="closeWatchlistArtistConfigModal()">&times;</span>
</div>
<div class="watchlist-artist-config-content">
<div class="watchlist-artist-config-body">
<div class="config-section">
<h3 class="config-section-title">Auto-Download</h3>
<p class="config-section-subtitle">When on, new releases are added to the wishlist and downloaded automatically. Turn off to <strong>follow only</strong> — still see new releases in scans, but pick what to download yourself.</p>
<div class="config-options">
<label class="config-option">
<input type="checkbox" id="config-auto-download" checked>
<div class="config-option-content">
<div class="config-option-icon">⬇️</div>
<div class="config-option-text">
<span class="config-option-title">Auto-download new releases</span>
<span class="config-option-description">Off = follow only (discover but don't auto-add to wishlist)</span>
</div>
</div>
</label>
</div>
</div>
<div class="config-section">
<h3 class="config-section-title">Download Preferences</h3>
<p class="config-section-subtitle">Select which types of releases to monitor for this artist</p>
<div class="config-options">
<label class="config-option">
<input type="checkbox" id="config-include-albums" checked>
<div class="config-option-content">
<div class="config-option-icon">💿</div>
<div class="config-option-text">
<span class="config-option-title">Albums</span>
<span class="config-option-description">Full-length studio albums</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="config-include-eps" checked>
<div class="config-option-content">
<div class="config-option-icon">🎵</div>
<div class="config-option-text">
<span class="config-option-title">EPs</span>
<span class="config-option-description">Extended plays (4-6 tracks)</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="config-include-singles" checked>
<div class="config-option-content">
<div class="config-option-icon">🎶</div>
<div class="config-option-text">
<span class="config-option-title">Singles</span>
<span class="config-option-description">Single tracks and 2-3 track
releases</span>
</div>
</div>
</label>
</div>
</div>
<div class="config-section">
<h3 class="config-section-title">Content Filters</h3>
<p class="config-section-subtitle">Check to INCLUDE, leave unchecked to EXCLUDE (default: all
excluded)</p>
<div class="config-options">
<label class="config-option">
<input type="checkbox" id="config-include-live">
<div class="config-option-content">
<div class="config-option-icon">🎤</div>
<div class="config-option-text">
<span class="config-option-title">Include Live Versions</span>
<span class="config-option-description">Check to include live performances and
concerts</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="config-include-remixes">
<div class="config-option-content">
<div class="config-option-icon">🎧</div>
<div class="config-option-text">
<span class="config-option-title">Include Remixes</span>
<span class="config-option-description">Check to include remix versions and
edits</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="config-include-acoustic">
<div class="config-option-content">
<div class="config-option-icon">🎸</div>
<div class="config-option-text">
<span class="config-option-title">Include Acoustic Versions</span>
<span class="config-option-description">Check to include acoustic and stripped
versions</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="config-include-compilations">
<div class="config-option-content">
<div class="config-option-icon">📀</div>
<div class="config-option-text">
<span class="config-option-title">Include Compilations</span>
<span class="config-option-description">Check to include greatest hits and
collections</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="config-include-instrumentals">
<div class="config-option-content">
<div class="config-option-icon">🎹</div>
<div class="config-option-text">
<span class="config-option-title">Include Instrumentals</span>
<span class="config-option-description">Check to include instrumental, karaoke,
and backing track versions</span>
</div>
</div>
</label>
</div>
</div>
<div class="config-section">
<h3 class="config-section-title">Scan Lookback</h3>
<p class="config-section-subtitle">How far back to look for releases on first scan of this artist</p>
<div class="form-group" style="margin: 12px 0 0;">
<select id="config-lookback-days" class="form-select">
<option value="">Use Global Setting</option>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="180">Last 6 months</option>
<option value="365">Last year</option>
<option value="730">Last 2 years</option>
<option value="1825">Last 5 years</option>
<option value="36500">Entire discography</option>
</select>
</div>
</div>
<!-- Metadata Source Override -->
<div class="config-section">
<h3 class="config-section-title">Scan Source</h3>
<p class="config-section-subtitle">Override which metadata provider is used when scanning this artist for new releases</p>
<div class="config-metadata-source-selector" id="config-metadata-source-selector">
<!-- Dynamically populated with source buttons -->
</div>
</div>
<!-- Linked Provider Section -->
<div class="config-section" id="watchlist-linked-provider-section" style="display:none">
<h3 class="config-section-title">Linked Artist</h3>
<p class="config-section-subtitle">The metadata provider artist linked to this watchlist entry</p>
<div id="watchlist-linked-provider-content">
<!-- Dynamically populated -->
</div>
</div>
</div>
<div class="watchlist-artist-config-footer">
<div class="config-modal-actions">
<button class="btn btn--secondary"
onclick="closeWatchlistArtistConfigModal()">
Cancel
</button>
<button class="btn btn--primary" id="save-artist-config-btn">
Save Preferences
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Watchlist Global Config Modal -->
<div class="modal-overlay hidden" id="watchlist-global-config-modal-overlay">
<div class="watchlist-artist-config-modal wl-global-modal" id="watchlist-global-config-modal">
<div class="wl-global-modal-head">
<div>
<h2 class="wl-global-modal-title">Global Watchlist Settings</h2>
<p class="wl-global-modal-sub">Override per-artist settings for all watchlist scans.</p>
</div>
<button class="wl-global-modal-close" onclick="closeWatchlistGlobalSettingsModal()" aria-label="Close"></button>
</div>
<div class="watchlist-artist-config-content">
<div class="watchlist-artist-config-body">
<!-- Global Override Toggle -->
<div class="config-section">
<label class="config-option global-override-toggle" id="global-override-toggle-label">
<input type="checkbox" id="global-override-enabled" onchange="toggleGlobalOverrideOptions()">
<div class="config-option-content">
<div class="config-option-icon">🌐</div>
<div class="config-option-text">
<span class="config-option-title">Enable Global Override</span>
<span class="config-option-description">When enabled, the settings below apply to ALL
artists, ignoring individual artist configs</span>
</div>
</div>
</label>
</div>
<div id="global-override-options" style="opacity: 0.4; pointer-events: none; transition: opacity 0.3s;">
<!-- Release Types -->
<div class="config-section">
<h3 class="config-section-title">Release Types</h3>
<p class="config-section-subtitle">Select which release types to download for all artists</p>
<div class="config-options">
<label class="config-option">
<input type="checkbox" id="global-include-albums" checked>
<div class="config-option-content">
<div class="config-option-icon">💿</div>
<div class="config-option-text">
<span class="config-option-title">Albums</span>
<span class="config-option-description">Full-length studio albums</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="global-include-eps" checked>
<div class="config-option-content">
<div class="config-option-icon">🎵</div>
<div class="config-option-text">
<span class="config-option-title">EPs</span>
<span class="config-option-description">Extended plays (4-6 tracks)</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="global-include-singles" checked>
<div class="config-option-content">
<div class="config-option-icon">🎶</div>
<div class="config-option-text">
<span class="config-option-title">Singles</span>
<span class="config-option-description">Single tracks and 2-3 track
releases</span>
</div>
</div>
</label>
</div>
</div>
<!-- Content Filters -->
<div class="config-section">
<h3 class="config-section-title">Content Filters</h3>
<p class="config-section-subtitle">Check to INCLUDE, leave unchecked to EXCLUDE (default: all
excluded)</p>
<div class="config-options">
<label class="config-option">
<input type="checkbox" id="global-include-live">
<div class="config-option-content">
<div class="config-option-icon">🎤</div>
<div class="config-option-text">
<span class="config-option-title">Include Live Versions</span>
<span class="config-option-description">Live performances and concerts</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="global-include-remixes">
<div class="config-option-content">
<div class="config-option-icon">🎧</div>
<div class="config-option-text">
<span class="config-option-title">Include Remixes</span>
<span class="config-option-description">Remix versions and edits</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="global-include-acoustic">
<div class="config-option-content">
<div class="config-option-icon">🎸</div>
<div class="config-option-text">
<span class="config-option-title">Include Acoustic Versions</span>
<span class="config-option-description">Acoustic and stripped versions</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="global-include-compilations">
<div class="config-option-content">
<div class="config-option-icon">📀</div>
<div class="config-option-text">
<span class="config-option-title">Include Compilations</span>
<span class="config-option-description">Greatest hits and collections</span>
</div>
</div>
</label>
<label class="config-option">
<input type="checkbox" id="global-include-instrumentals">
<div class="config-option-content">
<div class="config-option-icon">🎹</div>
<div class="config-option-text">
<span class="config-option-title">Include Instrumentals</span>
<span class="config-option-description">Instrumental, karaoke, and backing track versions</span>
</div>
</div>
</label>
</div>
</div>
<!-- Include Everything shortcut -->
<div class="config-section">
<label class="config-option include-everything-option">
<input type="checkbox" id="global-include-all" onchange="toggleGlobalIncludeAll()">
<div class="config-option-content">
<div class="config-option-icon"></div>
<div class="config-option-text">
<span class="config-option-title">Include Everything</span>
<span class="config-option-description">Enable all release types AND content filters
(live, remixes, acoustic, compilations, instrumentals)</span>
</div>
</div>
</label>
</div>
</div>
<!-- Custom Exclusion Terms (always visible, independent of global override) -->
<div class="config-section">
<h3 class="config-section-title">Custom Exclusion Terms</h3>
<p class="config-section-subtitle">Comma-separated terms — tracks or albums matching any term will be skipped during scans</p>
<div class="config-exclude-terms-container">
<input type="text" id="global-exclude-terms" class="config-exclude-terms-input"
placeholder="e.g. commentary, interlude, skit, demo, a cappella">
<p class="config-exclude-terms-hint">Applied globally to all watchlist scans regardless of override setting. Case-insensitive substring matching.</p>
</div>
</div>
</div>
</div>
<div class="watchlist-artist-config-footer">
<div class="config-modal-actions">
<button class="btn btn--secondary"
onclick="closeWatchlistGlobalSettingsModal()">
Cancel
</button>
<button class="btn btn--primary" id="save-global-config-btn"
onclick="saveWatchlistGlobalConfig()">
Save Global Settings
</button>
</div>
</div>
</div>
</div>
<!-- Genre Browser Modal -->
<div class="genre-browser-modal-overlay" id="genre-browser-modal">
<div class="genre-browser-modal-container">
<div class="genre-browser-modal-header">
<h2 class="genre-browser-modal-title">🎵 Browse by Genre</h2>
<button class="genre-browser-modal-close" id="genre-browser-modal-close">
<span class="genre-browser-close-icon">×</span>
</button>
</div>
<div class="genre-browser-modal-content">
<div class="genre-browser-search-section">
<div class="genre-browser-search-container">
<input type="text" class="genre-browser-search-input" placeholder="Search genres..."
id="genre-browser-search">
<span class="genre-browser-search-icon">🔍</span>
</div>
</div>
<div class="genre-browser-genres-section">
<div class="genre-browser-genres-grid" id="genre-browser-genres-grid">
<!-- Loading placeholder -->
<div class="genre-browser-loading-container">
<div class="genre-browser-loading-spinner"></div>
<p class="genre-browser-loading-text">🔍 Discovering current Beatport genres...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tool Help Modal -->
<!-- Retag Tool Modal -->
<div class="tool-help-modal" id="tool-help-modal" onclick="if(event.target===this)closeToolHelpModal()">
<div class="tool-help-modal-content">
<div class="tool-help-modal-header">
<h3 id="tool-help-modal-title">Tool Information</h3>
<button class="tool-help-modal-close" onclick="closeToolHelpModal()">&times;</button>
</div>
<div class="tool-help-modal-body" id="tool-help-modal-body">
<!-- Content will be dynamically inserted -->
</div>
</div>
</div>
<!-- Library History Modal -->
<div class="modal-overlay hidden" id="library-history-overlay" onclick="if(event.target===this)closeLibraryHistoryModal()">
<div class="library-history-modal">
<div class="library-history-modal-header">
<h3>Library History</h3>
<button class="library-history-modal-close" onclick="closeLibraryHistoryModal()">&times;</button>
</div>
<div class="library-history-tabs">
<button class="library-history-tab active" data-tab="download" onclick="switchHistoryTab('download')">
Downloads <span class="library-history-tab-count" id="history-download-count">0</span>
</button>
<button class="library-history-tab" data-tab="import" onclick="switchHistoryTab('import')">
Server Imports <span class="library-history-tab-count" id="history-import-count">0</span>
</button>
<button class="library-history-tab" data-tab="quarantine" onclick="switchHistoryTab('quarantine')">
Quarantine <span class="library-history-tab-count" id="history-quarantine-count">0</span>
</button>
</div>
<div class="history-source-bar" id="history-source-bar" style="display:none"></div>
<div class="library-history-list" id="library-history-list"></div>
<div class="library-history-pagination" id="library-history-pagination"></div>
</div>
</div>
<!-- Download Audit Trail Modal -->
<div class="modal-overlay hidden" id="download-audit-overlay" onclick="if(event.target===this)closeDownloadAuditModal()">
<div class="download-audit-modal">
<button class="download-audit-close" onclick="closeDownloadAuditModal()" title="Close">&times;</button>
<div class="download-audit-hero" id="download-audit-hero"></div>
<div class="download-audit-tabs" id="download-audit-tabs"></div>
<div class="download-audit-body" id="download-audit-body"></div>
</div>
</div>
<!-- Sync History Modal -->
<div class="modal-overlay hidden" id="sync-history-overlay" onclick="if(event.target===this)closeSyncHistoryModal()">
<div class="sync-history-modal">
<div class="sync-history-modal-header">
<h3>Sync History</h3>
<button class="sync-history-modal-close" onclick="closeSyncHistoryModal()">&times;</button>
</div>
<div class="sync-history-tabs" id="sync-history-tabs"></div>
<div class="sync-history-list" id="sync-history-list"></div>
<div class="sync-history-pagination" id="sync-history-pagination"></div>
</div>
</div>
<!-- Metadata Cache Browse Modal -->
<div class="mcache-modal-overlay" id="mcache-browse-modal" style="display:none;">
<div class="mcache-modal">
<div class="mcache-modal-header">
<h3 class="mcache-modal-title">Metadata Cache Browser</h3>
<div class="mcache-modal-header-actions">
<div class="mcache-clear-dropdown" id="mcache-clear-dropdown">
<button class="mcache-btn-clear" onclick="toggleMcacheClearDropdown(event)" title="Clear cached data">Clear ▾</button>
<div class="mcache-clear-dropdown-menu" id="mcache-clear-dropdown-menu">
<button onclick="clearMetadataCacheBySource('spotify')"><span class="mcache-source-badge spotify" style="margin-right:6px">spotify</span>Clear Spotify</button>
<button onclick="clearMetadataCacheBySource('itunes')"><span class="mcache-source-badge itunes" style="margin-right:6px">itunes</span>Clear iTunes</button>
<button onclick="clearMetadataCacheBySource('deezer')"><span class="mcache-source-badge deezer" style="margin-right:6px">deezer</span>Clear Deezer</button>
<button onclick="clearMetadataCacheBySource('beatport')"><span class="mcache-source-badge beatport" style="margin-right:6px">beatport</span>Clear Beatport</button>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin:4px 0"></div>
<button onclick="clearMusicBrainzCache()"><span class="mcache-source-badge musicbrainz" style="margin-right:6px">musicbrainz</span>Clear MusicBrainz</button>
<button onclick="clearMetadataCacheBySource('discogs')"><span class="mcache-source-badge discogs" style="margin-right:6px">discogs</span>Clear Discogs</button>
<button onclick="clearMusicBrainzCache(true)"><span class="mcache-source-badge musicbrainz" style="margin-right:6px;opacity:0.6">musicbrainz</span>Clear Failed MB Only</button>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin:4px 0"></div>
<button onclick="clearMetadataCache()">Clear All</button>
</div>
</div>
<button class="mcache-modal-close" onclick="closeMetadataCacheModal()">&times;</button>
</div>
</div>
<div class="mcache-stats-bar" id="mcache-stats-bar">
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Spotify</span>
<span class="mcache-stat-pill-value" id="mcache-browse-spotify-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">iTunes</span>
<span class="mcache-stat-pill-value" id="mcache-browse-itunes-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Deezer</span>
<span class="mcache-stat-pill-value" id="mcache-browse-deezer-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Beatport</span>
<span class="mcache-stat-pill-value" id="mcache-browse-beatport-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">MusicBrainz</span>
<span class="mcache-stat-pill-value" id="mcache-browse-musicbrainz-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Discogs</span>
<span class="mcache-stat-pill-value" id="mcache-browse-discogs-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Total Hits</span>
<span class="mcache-stat-pill-value" id="mcache-browse-hits">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Searches</span>
<span class="mcache-stat-pill-value" id="mcache-browse-searches">0</span>
</div>
</div>
<div class="mcache-tabs">
<button class="mcache-tab active" data-tab="artist" onclick="switchMetadataCacheTab('artist')">Artists</button>
<button class="mcache-tab" data-tab="album" onclick="switchMetadataCacheTab('album')">Albums</button>
<button class="mcache-tab" data-tab="track" onclick="switchMetadataCacheTab('track')">Tracks</button>
</div>
<div class="mcache-filters">
<input type="text" class="mcache-search-input" id="mcache-search" placeholder="Search cached metadata..." oninput="debouncedMetadataCacheSearch()">
<select class="mcache-source-filter" id="mcache-source-filter" onchange="loadMetadataCacheBrowse()">
<option value="">All Sources</option>
<option value="spotify">Spotify</option>
<option value="itunes">iTunes</option>
<option value="deezer">Deezer</option>
<option value="beatport">Beatport</option>
<option value="musicbrainz">MusicBrainz</option>
<option value="discogs">Discogs</option>
</select>
<select class="mcache-sort-filter" id="mcache-sort-filter" onchange="loadMetadataCacheBrowse()">
<option value="last_accessed_at">Recently Accessed</option>
<option value="updated_at">Recently Updated</option>
<option value="created_at">Recently Added</option>
<option value="access_count">Most Accessed</option>
<option value="name">Name (A-Z)</option>
</select>
</div>
<div class="mcache-grid" id="mcache-grid">
<!-- Cards populated by JS -->
</div>
<div class="mcache-pagination" id="mcache-pagination"></div>
</div>
</div>
<!-- Metadata Cache Detail Modal (overlays browse modal) -->
<div class="mcache-detail-overlay" id="mcache-detail-modal" style="display:none;">
<div class="mcache-detail-modal">
<div class="mcache-detail-header">
<h3 class="mcache-detail-title" id="mcache-detail-title">Entity Detail</h3>
<button class="mcache-modal-close" onclick="closeMetadataCacheDetail()">&times;</button>
</div>
<div class="mcache-detail-body" id="mcache-detail-body">
<!-- Detail content populated by JS -->
</div>
</div>
</div>
<!-- Right Sidebar Download Indicator (Global - outside all containers) -->
<div class="discover-download-sidebar" id="discover-download-sidebar">
<div class="discover-download-sidebar-header">
<span class="discover-download-sidebar-icon">🎵</span>
<span class="discover-download-sidebar-title">Downloads</span>
<span class="discover-download-sidebar-count" id="discover-download-count">0</span>
</div>
<div class="discover-download-bubbles" id="discover-download-bubbles">
<!-- Download bubbles will be added here dynamically -->
</div>
</div>
<!-- Confirm Dialog Modal -->
<div class="modal-overlay hidden" id="confirm-modal-overlay">
<div class="confirm-modal">
<div class="confirm-modal-header">
<h2 id="confirm-modal-title">Confirm</h2>
<button class="confirm-modal-close" onclick="resolveConfirmDialog(false)"></button>
</div>
<div class="confirm-modal-content">
<p id="confirm-modal-message"></p>
</div>
<div class="confirm-modal-actions">
<button class="modal-button modal-button--secondary" id="confirm-modal-cancel"
onclick="resolveConfirmDialog(false)">Cancel</button>
<button class="modal-button modal-button--primary" id="confirm-modal-confirm"
onclick="resolveConfirmDialog(true)">Confirm</button>
</div>
</div>
</div>
<!-- Spotify Rate Limit Modal -->
<div class="modal-overlay hidden" id="rate-limit-modal-overlay">
<div class="confirm-modal rate-limit-modal">
<div class="confirm-modal-header rate-limit-modal-header">
<div class="rate-limit-title-row">
<svg class="rate-limit-icon" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<h2>Spotify Rate Limited</h2>
</div>
<button class="confirm-modal-close" onclick="closeRateLimitModal()"></button>
</div>
<div class="confirm-modal-content rate-limit-modal-content">
<p class="rate-limit-description">Spotify has temporarily blocked API access. All Spotify features (search, enrichment, playlists) are paused until the ban expires.</p>
<div class="rate-limit-details">
<div class="rate-limit-detail-row">
<span class="rate-limit-label">Ban Duration</span>
<span class="rate-limit-value" id="rate-limit-ban-duration"></span>
</div>
<div class="rate-limit-detail-row">
<span class="rate-limit-label">Triggered By</span>
<span class="rate-limit-value rate-limit-endpoint" id="rate-limit-endpoint"></span>
</div>
<div class="rate-limit-detail-row">
<span class="rate-limit-label">Time Remaining</span>
<span class="rate-limit-value rate-limit-countdown" id="rate-limit-countdown"></span>
</div>
</div>
<p class="rate-limit-hint">While rate limiting is active, Spotify-specific features are unavailable. You can wait for the ban to expire or disconnect Spotify to clear it immediately.</p>
</div>
<div class="confirm-modal-actions rate-limit-modal-actions">
<button class="modal-button modal-button--secondary" onclick="closeRateLimitModal()">Dismiss</button>
<button class="modal-button rate-limit-disconnect-btn" onclick="disconnectSpotifyFromRateLimit()">
Disconnect
<span class="rate-limit-disconnect-sub">Clear the ban and pause Spotify-specific features</span>
</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='vendor/socket.io.min.js', v=static_v) }}"></script>
{{ vite_assets('body')|safe }}
<script src="{{ url_for('static', filename='setup-wizard.js', v=static_v) }}"></script>
<!-- Split modules (was: script.js) — core.js must load first, init.js before shell-bridge.js -->
<script src="{{ url_for('static', filename='core.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='shared-helpers.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='media-player.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='settings.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='search.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='sync-spotify.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='downloads.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='track-detail.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='wishlist-tools.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='origin-history.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='watchlist-history.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='blocklist.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='service-switch.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='my-accounts.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='sync-services.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='sync-listenbrainz.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='sync-lastfm.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='sync-soulsync-discovery.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='api-monitor.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='library.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='beatport-ui.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='discover-section-controller.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='discover.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='enrichment.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='enrichment-manager.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='stats-automations.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='auto-sync.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='pages-extra.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='init.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='shell-bridge.js', v=static_v) }}"></script>
<!-- Notification bell + floating helper toggle — always accessible above modals -->
<!-- Ambient glow under the global search bar. Radial gradient, brightest
directly under the bar, tapering out toward the window corners.
Purely decorative (pointer-events: none). Visibility follows the bar
via _gsUpdateVisibility(). -->
<div class="gsearch-aura" id="gsearch-aura"></div>
<!-- Global Search Bar — Spotlight-style search from anywhere -->
<div class="gsearch-bar" id="gsearch-bar">
<div class="gsearch-icon">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
</div>
<input type="text" class="gsearch-input" id="gsearch-input" placeholder="Search artists, albums, tracks..." autocomplete="off" spellcheck="false">
<button class="gsearch-clear" id="gsearch-clear" style="display:none" title="Clear">&times;</button>
<span class="gsearch-shortcut" id="gsearch-shortcut">/</span>
</div>
<div class="gsearch-results" id="gsearch-results"></div>
{% include 'track-detail-modal.html' %}
<!-- Floating Mini Media Player — fixed bottom-right, above bell/help -->
<div class="media-player idle" id="media-player">
<!-- Thin interactive progress bar spanning full width at top -->
<div class="player-top-progress">
<div class="progress-bar-container">
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<input type="range" class="progress-bar" id="progress-bar" min="0" max="100" value="0" step="0.1">
</div>
</div>
<!-- Loading animation (shown while buffering/downloading) -->
<div class="loading-animation hidden" id="loading-animation">
<div class="loading-bar"><div class="loading-progress"></div></div>
<div class="loading-text">0%</div>
</div>
<!-- Main body: album art + track info + controls -->
<div class="mini-player-body">
<img class="sidebar-album-art" id="sidebar-album-art" src="/static/trans2.png" alt="" onerror="this.src='/static/trans2.png'">
<div class="media-info">
<div class="track-title" id="track-title"><span class="title-text">No track</span></div>
<div class="artist-name" id="artist-name">Unknown Artist</div>
</div>
<div class="mini-player-controls">
<button class="mini-toggle-btn" id="mini-shuffle-btn" title="Shuffle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
</button>
<button class="mini-nav-btn" id="mini-prev-btn" title="Previous" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button>
<button class="play-button" id="play-button" disabled>
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<svg class="pause-icon" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 4h4v16H6zm8 0h4v16h-4z"/></svg>
</button>
<button class="mini-nav-btn" id="mini-next-btn" title="Next" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<button class="mini-toggle-btn" id="mini-repeat-btn" title="Repeat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
<span class="mini-repeat-one" id="mini-repeat-one-badge" style="display:none">1</span>
</button>
<button class="stop-button" id="stop-button" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="10" height="10"><rect x="6" y="6" width="12" height="12" rx="1.5"/></svg>
</button>
<button class="expand-hint" aria-label="Expand player" title="Now Playing">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
</button>
</div>
</div>
<!-- Elements kept in DOM for JS compatibility — not visible in mini player -->
<div class="volume-control" style="display:none">
<span class="volume-icon"></span>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="70">
</div>
<span id="current-time" style="display:none">0:00</span>
<span id="total-time" style="display:none">0:00</span>
<div id="album-name" style="display:none"></div>
<div class="media-expanded hidden" id="media-expanded"></div>
<div class="no-track-message hidden" id="no-track-message"><span>Play a track to get started</span></div>
</div>
<button class="notif-bell-btn" id="notif-bell-btn" onclick="toggleNotifPanel()" title="Notifications">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notif-bell-badge" id="notif-bell-badge" style="display:none">0</span>
</button>
<button class="helper-float-btn" id="helper-float-btn" onclick="toggleHelperMode()" title="Interactive Help — click any element to learn about it">
<span>?</span>
</button>
<script src="{{ url_for('static', filename='docs.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='helper.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='particles.js', v=static_v) }}"></script>
<script src="{{ url_for('static', filename='worker-orbs.js', v=static_v) }}"></script>
</body>
</html>