|
|
<!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') }}">
|
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='mobile.css') }}">
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- 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>
|
|
|
</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</h3>
|
|
|
<button id="profile-manage-close" class="profile-manage-close-btn">×</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">
|
|
|
<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="downloads">Search</option>
|
|
|
<option value="discover">Discover</option>
|
|
|
<option value="artists">Artists</option>
|
|
|
<option value="automations">Automations</option>
|
|
|
<option value="library">Library</option>
|
|
|
<option value="import">Import</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="downloads" checked> Search</label>
|
|
|
<label><input type="checkbox" value="discover" checked> Discover</label>
|
|
|
<label><input type="checkbox" value="artists" checked> Artists</label>
|
|
|
<label><input type="checkbox" value="automations" checked> Automations</label>
|
|
|
<label><input type="checkbox" value="library" checked> Library</label>
|
|
|
<label><input type="checkbox" value="import" checked> Import</label>
|
|
|
<label><input type="checkbox" value="help" checked disabled> Help & Docs</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="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="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()">×</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="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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Navigation Section -->
|
|
|
<nav class="sidebar-nav">
|
|
|
<button class="nav-button active" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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"><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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="artists">
|
|
|
<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="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg></span>
|
|
|
<span class="nav-text">Artists</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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="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">Import</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="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>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="hydrabase" id="hydrabase-nav" 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>
|
|
|
</button>
|
|
|
</nav>
|
|
|
|
|
|
<!-- Spacer -->
|
|
|
<div class="sidebar-spacer"></div>
|
|
|
|
|
|
<!-- Media Player Section -->
|
|
|
<div class="media-player" id="media-player">
|
|
|
<!-- Progress bar at top of player -->
|
|
|
<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 class="time-display">
|
|
|
<span class="current-time" id="current-time">0:00</span>
|
|
|
<span class="total-time" id="total-time">0:00</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Loading Animation -->
|
|
|
<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>
|
|
|
|
|
|
<!-- Track info row -->
|
|
|
<div class="media-header">
|
|
|
<img class="sidebar-album-art" id="sidebar-album-art" src="" alt="">
|
|
|
<div class="media-info">
|
|
|
<div class="track-title" id="track-title">No track</div>
|
|
|
<div class="artist-name" id="artist-name">Unknown Artist</div>
|
|
|
<div class="album-name header-album" id="album-name">Unknown Album</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Controls row (always visible) -->
|
|
|
<div class="media-controls-row">
|
|
|
<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>
|
|
|
<div class="volume-control">
|
|
|
<span class="volume-icon">
|
|
|
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.16c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.16c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
|
|
|
</span>
|
|
|
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="70">
|
|
|
</div>
|
|
|
<button class="stop-button" id="stop-button" disabled>
|
|
|
<svg viewBox="0 0 24 24" fill="currentColor" width="12" height="12"><rect x="6" y="6" width="12" height="12" rx="1.5"/></svg>
|
|
|
</button>
|
|
|
<button class="expand-hint" aria-label="Expand player">
|
|
|
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><path d="M3 10l5 4 5-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Expanded Content (kept for backward compat, hidden) -->
|
|
|
<div class="media-expanded hidden" id="media-expanded"></div>
|
|
|
|
|
|
<!-- No Track Message -->
|
|
|
<div class="no-track-message" id="no-track-message">
|
|
|
<svg class="no-track-icon" viewBox="0 0 48 48" fill="none" width="28" height="28">
|
|
|
<circle cx="24" cy="24" r="22" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
|
|
|
<path d="M20 16l12 8-12 8z" fill="currentColor" opacity="0.2"/>
|
|
|
</svg>
|
|
|
<span>Play a track to get started</span>
|
|
|
</div>
|
|
|
</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()">v2.1</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Status Section -->
|
|
|
<div class="status-section">
|
|
|
<h4 class="status-title">Service Status</h4>
|
|
|
<div class="status-indicator" id="spotify-indicator">
|
|
|
<span class="status-dot disconnected"></span>
|
|
|
<span class="status-name" id="music-source-name">Spotify</span>
|
|
|
</div>
|
|
|
<div class="status-indicator" id="media-server-indicator">
|
|
|
<span class="status-dot disconnected"></span>
|
|
|
<span class="status-name" id="media-server-name">Plex</span>
|
|
|
</div>
|
|
|
<div class="status-indicator" id="soulseek-indicator">
|
|
|
<span class="status-dot disconnected"></span>
|
|
|
<span class="status-name" id="download-source-name">Soulseek</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>
|
|
|
|
|
|
<!-- Dashboard Page -->
|
|
|
<div class="page active" id="dashboard-page">
|
|
|
<div class="dashboard-container">
|
|
|
<div class="dashboard-header">
|
|
|
<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-spacer"></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>
|
|
|
<!-- 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>
|
|
|
<button class="import-button" id="import-button" onclick="navigateToPage('import')"
|
|
|
title="Import Music from Staging">
|
|
|
<img src="https://cdn-icons-png.flaticon.com/512/8765/8765164.png" alt="Import"
|
|
|
class="import-logo">
|
|
|
</button>
|
|
|
<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>
|
|
|
|
|
|
<div class="dashboard-section">
|
|
|
<h3 class="section-title">Service Status</h3>
|
|
|
<div class="service-status-grid">
|
|
|
<div class="service-card" id="spotify-service-card">
|
|
|
<div class="service-card-header">
|
|
|
<span class="service-card-title" id="music-source-title">Spotify</span>
|
|
|
<span class="service-card-indicator disconnected"
|
|
|
id="spotify-status-indicator">●</span>
|
|
|
</div>
|
|
|
<p class="service-card-status-text" id="spotify-status-text">Disconnected</p>
|
|
|
<p class="service-card-response-time" id="spotify-response-time">Response: --</p>
|
|
|
<div class="service-card-footer">
|
|
|
<button class="service-card-button"
|
|
|
onclick="testDashboardConnection('spotify')">Test Connection</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="service-card" id="media-server-service-card">
|
|
|
<div class="service-card-header">
|
|
|
<span class="service-card-title" id="media-server-service-name">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
|
|
|
Connection</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="service-card" id="soulseek-service-card">
|
|
|
<div class="service-card-header">
|
|
|
<span class="service-card-title" id="download-source-title">Soulseek</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 Connection</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="enrichment-section">
|
|
|
<div class="enrichment-section-header">
|
|
|
<span class="enrichment-section-label">Enrichment Services</span>
|
|
|
</div>
|
|
|
<div class="enrichment-status-grid" id="enrichment-status-grid">
|
|
|
<!-- Dynamically populated by JS -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Recent Syncs Section -->
|
|
|
<div class="dashboard-section">
|
|
|
<h3 class="section-title">Recent Syncs</h3>
|
|
|
<div class="sync-history-cards" id="sync-history-cards">
|
|
|
<!-- Dynamically populated by JS -->
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="dashboard-section">
|
|
|
<h3 class="section-title">System Statistics</h3>
|
|
|
<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>
|
|
|
|
|
|
<div class="dashboard-section" id="dashboard-active-downloads-section" style="display: none;">
|
|
|
<h3 class="section-title">Active Downloads</h3>
|
|
|
<div id="dashboard-downloads-container"></div>
|
|
|
</div>
|
|
|
|
|
|
<div class="dashboard-section">
|
|
|
<h3 class="section-title">Tools & Operations</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>
|
|
|
</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="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="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="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 Transfer 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>
|
|
|
|
|
|
<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">—</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;">—</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="tool-card-controls">
|
|
|
<button onclick="openDiscoveryPoolModal()">Open Discovery Pool</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="tool-card" id="retag-tool-card">
|
|
|
<div class="tool-card-header">
|
|
|
<h4 class="tool-card-title">Retag Tool</h4>
|
|
|
<button class="tool-help-button" data-tool="retag-tool"
|
|
|
title="Learn more about this tool">?</button>
|
|
|
</div>
|
|
|
<p class="tool-card-info">Fix metadata on previously downloaded albums & singles</p>
|
|
|
<div class="tool-card-stats">
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-item-label">Groups:</span>
|
|
|
<span class="stat-item-value" id="retag-stat-groups">0</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-item-label">Tracks:</span>
|
|
|
<span class="stat-item-value" id="retag-stat-tracks">0</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-item-label">Artists:</span>
|
|
|
<span class="stat-item-value" id="retag-stat-artists">0</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-item-label">Status:</span>
|
|
|
<span class="stat-item-value" id="retag-stat-status">Idle</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="tool-card-controls">
|
|
|
<button id="retag-open-button">Open Retag Tool</button>
|
|
|
</div>
|
|
|
<div class="tool-card-progress-section">
|
|
|
<p class="progress-phase-label" id="retag-phase-label">Ready</p>
|
|
|
<div class="progress-bar-container">
|
|
|
<div class="progress-bar-fill" id="retag-progress-bar" style="width: 0%;">
|
|
|
</div>
|
|
|
</div>
|
|
|
<p class="progress-details-label" id="retag-progress-label">0 / 0 tracks (0.0%)</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<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">📡</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 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 & 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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="dashboard-section">
|
|
|
<div class="section-title-row">
|
|
|
<h3 class="section-title">Recent Activity</h3>
|
|
|
<button class="library-history-btn" onclick="openLibraryHistoryModal()" title="View full library history">History</button>
|
|
|
</div>
|
|
|
<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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Main container for the Sync page -->
|
|
|
<div class="page" id="sync-page">
|
|
|
<!-- 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>
|
|
|
<button class="sync-history-btn" onclick="openSyncHistoryModal()" title="View sync history">Sync History</button>
|
|
|
</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">
|
|
|
<span class="tab-icon server-icon"></span> Server Playlists
|
|
|
</button>
|
|
|
<div class="sync-tab-divider"></div>
|
|
|
<button class="sync-tab-button" data-tab="spotify">
|
|
|
<span class="tab-icon spotify-icon"></span> Spotify
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="spotify-public">
|
|
|
<span class="tab-icon spotify-icon"></span> Spotify Link
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="tidal">
|
|
|
<span class="tab-icon tidal-icon"></span> Tidal
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="deezer">
|
|
|
<span class="tab-icon deezer-icon"></span> Deezer
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="youtube">
|
|
|
<span class="tab-icon youtube-icon"></span> YouTube
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="beatport">
|
|
|
<span class="tab-icon beatport-icon"></span> Beatport
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="import-file">
|
|
|
<span class="tab-icon import-file-icon"></span> Import
|
|
|
</button>
|
|
|
<button class="sync-tab-button" data-tab="mirrored">
|
|
|
<span class="tab-icon mirrored-icon"></span> Mirrored
|
|
|
</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 -->
|
|
|
<div class="sync-tab-content" id="deezer-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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- 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">Refresh</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()">×</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>
|
|
|
</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>
|
|
|
|
|
|
<!-- Downloads Page -->
|
|
|
<div class="page" id="downloads-page">
|
|
|
<!--
|
|
|
This top-level container replicates the QSplitter from downloads.py,
|
|
|
creating the two-panel layout for the page.
|
|
|
-->
|
|
|
<div class="downloads-content manager-hidden">
|
|
|
|
|
|
<!-- ======================================================= -->
|
|
|
<!-- == LEFT PANEL: Search, Filters, and Results == -->
|
|
|
<!-- ======================================================= -->
|
|
|
<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>Music Downloads</span></h2>
|
|
|
<p class="downloads-subtitle">Search, discover, and download high-quality music</p>
|
|
|
</div>
|
|
|
<button id="toggle-download-manager-btn" class="toggle-manager-btn"
|
|
|
title="Toggle Download Manager">
|
|
|
<span class="toggle-icon">›</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Search Mode Toggle -->
|
|
|
<div class="search-mode-toggle-container">
|
|
|
<div class="search-mode-toggle" data-active="enhanced">
|
|
|
<button class="search-mode-btn active" data-mode="enhanced">
|
|
|
<span class="mode-icon">✨</span>
|
|
|
<span class="mode-label">Enhanced Search</span>
|
|
|
</button>
|
|
|
<button class="search-mode-btn" data-mode="basic">
|
|
|
<span class="mode-icon">🔍</span>
|
|
|
<span class="mode-label">Basic Search</span>
|
|
|
</button>
|
|
|
<div class="toggle-slider"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Basic Search Section (Current) -->
|
|
|
<div id="basic-search-section" class="search-section">
|
|
|
<!-- Search Bar: Replicates create_elegant_search_bar() -->
|
|
|
<div class="search-bar-container">
|
|
|
<input type="text" id="downloads-search-input"
|
|
|
placeholder="Search for music... (e.g., 'Virtual Mage', 'Queen Bohemian Rhapsody')">
|
|
|
<button id="downloads-cancel-btn" class="hidden">✕ Cancel</button>
|
|
|
<button id="downloads-search-btn">🔍 Search</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="filters-container" class="filters-container hidden">
|
|
|
<div class="filter-toggle-header">
|
|
|
<button id="filter-toggle-btn" class="filter-toggle-btn">⏷ Filters</button>
|
|
|
</div>
|
|
|
|
|
|
<div id="filter-content" class="filter-content hidden">
|
|
|
<!-- Filter by Type -->
|
|
|
<div class="filter-group">
|
|
|
<label class="filter-label">Type:</label>
|
|
|
<button class="filter-btn active" data-filter-type="type"
|
|
|
data-value="all">All</button>
|
|
|
<button class="filter-btn" data-filter-type="type"
|
|
|
data-value="album">Albums</button>
|
|
|
<button class="filter-btn" data-filter-type="type"
|
|
|
data-value="track">Singles</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Filter by Format -->
|
|
|
<div class="filter-group">
|
|
|
<label class="filter-label">Format:</label>
|
|
|
<button class="filter-btn active" data-filter-type="format"
|
|
|
data-value="all">All</button>
|
|
|
<button class="filter-btn" data-filter-type="format"
|
|
|
data-value="flac">FLAC</button>
|
|
|
<button class="filter-btn" data-filter-type="format"
|
|
|
data-value="mp3">MP3</button>
|
|
|
<!-- Added missing format buttons -->
|
|
|
<button class="filter-btn" data-filter-type="format"
|
|
|
data-value="ogg">OGG</button>
|
|
|
<button class="filter-btn" data-filter-type="format"
|
|
|
data-value="aac">AAC</button>
|
|
|
<button class="filter-btn" data-filter-type="format"
|
|
|
data-value="wma">WMA</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Sort Controls -->
|
|
|
<div class="filter-group">
|
|
|
<label class="filter-label">Sort by:</label>
|
|
|
<button id="sort-order-btn" class="filter-btn sort-order-btn"
|
|
|
data-order="desc">↓</button>
|
|
|
<!-- Added all sort options from the GUI -->
|
|
|
<button class="filter-btn active" data-filter-type="sort"
|
|
|
data-value="relevance">Relevance</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="quality_score">Quality</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="size">Size</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="title">Name</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="username">Uploader</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="bitrate">Bitrate</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="duration">Duration</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="availability">Available</button>
|
|
|
<button class="filter-btn" data-filter-type="sort"
|
|
|
data-value="upload_speed">Speed</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<!-- Search Status Bar -->
|
|
|
<div class="search-status-container">
|
|
|
<div class="spinner-animation hidden"></div>
|
|
|
<p id="search-status-text">Ready to search • Enter artist, song, or album name</p>
|
|
|
<div class="dots-animation hidden"></div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Search Results Area: Replicates the QScrollArea -->
|
|
|
<div class="search-results-container">
|
|
|
<div class="search-results-header">
|
|
|
<h3>Search Results</h3>
|
|
|
</div>
|
|
|
<div class="search-results-scroll-area" id="search-results-area">
|
|
|
<!--
|
|
|
The placeholder search results have been removed.
|
|
|
This area will now be populated by JavaScript based on API responses.
|
|
|
-->
|
|
|
<div class="search-results-placeholder">
|
|
|
<p>Your search results will appear here.</p>
|
|
|
</div>
|
|
|
</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>
|
|
|
<button id="enhanced-search-btn" class="enhanced-search-btn">
|
|
|
<span class="btn-icon">👁️</span>
|
|
|
<span class="btn-text">Show Results</span>
|
|
|
</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">
|
|
|
|
|
|
<!-- Source Tabs -->
|
|
|
<div id="enh-source-tabs" class="enh-source-tabs 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>
|
|
|
|
|
|
<!-- ======================================================= -->
|
|
|
<!-- == RIGHT PANEL: Controls and Download Queue == -->
|
|
|
<!-- ======================================================= -->
|
|
|
<div class="downloads-side-panel">
|
|
|
|
|
|
<!-- Controls Panel: Replicates create_collapsible_controls_panel() -->
|
|
|
<div class="controls-panel">
|
|
|
<h3 class="controls-panel__header">Download Manager</h3>
|
|
|
<div class="controls-panel__stats">
|
|
|
<p id="active-downloads-label">• Active Downloads: 0</p>
|
|
|
<p id="finished-downloads-label">• Finished Downloads: 0</p>
|
|
|
</div>
|
|
|
<div class="controls-panel__actions">
|
|
|
<button class="controls-panel__clear-btn">🗑️ Clear Completed</button>
|
|
|
<button class="controls-panel__cancel-all-btn">⛔ Clear Current</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Download Queue: Replicates TabbedDownloadManager -->
|
|
|
<div class="download-manager">
|
|
|
<div class="download-manager__tabs">
|
|
|
<button class="tab-btn active" data-tab="active-queue">Download Queue (0)</button>
|
|
|
<button class="tab-btn" data-tab="finished-queue">Finished (0)</button>
|
|
|
</div>
|
|
|
<div class="download-manager__content">
|
|
|
|
|
|
<!-- Active Queue -->
|
|
|
<div class="download-queue active" id="active-queue">
|
|
|
<div class="download-queue__empty-message">No active downloads.</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Finished Queue -->
|
|
|
<div class="download-queue" id="finished-queue">
|
|
|
<div class="download-queue__empty-message">No finished downloads.</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Artists Page -->
|
|
|
<div class="page" id="artists-page">
|
|
|
<!-- Initial Search State -->
|
|
|
<div class="artists-search-state" id="artists-search-state">
|
|
|
<div class="artists-search-container">
|
|
|
<div class="artists-welcome-section">
|
|
|
<h2 class="artists-welcome-title"><img src="/static/discover.png" class="page-header-icon" alt=""><span>Discover Artists</span></h2>
|
|
|
<p class="artists-welcome-subtitle">Search for your favorite artists and explore their
|
|
|
complete discography</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="artists-search-input-container">
|
|
|
<input type="text" id="artists-search-input" class="artists-search-input"
|
|
|
placeholder="Search for an artist...">
|
|
|
<div class="artists-search-icon">🔍</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="artists-search-status" id="artists-search-status">
|
|
|
Start typing to search for artists
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Search Results State -->
|
|
|
<div class="artists-results-state hidden" id="artists-results-state">
|
|
|
<div class="artists-results-header">
|
|
|
<button class="artists-back-button" id="artists-back-button">
|
|
|
<span class="back-icon">←</span>
|
|
|
<span>Back to Search</span>
|
|
|
</button>
|
|
|
|
|
|
<div class="artists-search-header">
|
|
|
<input type="text" id="artists-header-search-input" class="artists-header-search-input"
|
|
|
placeholder="Search for an artist...">
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="artists-results-content">
|
|
|
<div class="artists-results-title">Search Results</div>
|
|
|
<div class="artists-cards-container" id="artists-cards-container">
|
|
|
<!-- Artist cards will be dynamically populated here -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Artist Detail State -->
|
|
|
<div class="artist-detail-state hidden" id="artist-detail-state">
|
|
|
<!-- Hero Section -->
|
|
|
<div class="artists-hero-section" id="artists-hero-section">
|
|
|
<div class="artists-hero-bg" id="artists-hero-bg"></div>
|
|
|
<div class="artists-hero-overlay"></div>
|
|
|
<div class="artists-hero-content">
|
|
|
<button class="artists-hero-back" id="artist-detail-back-button">
|
|
|
<span>←</span> Back
|
|
|
</button>
|
|
|
<div class="artists-hero-main">
|
|
|
<div class="artists-hero-image" id="artists-hero-image"></div>
|
|
|
<div class="artists-hero-info">
|
|
|
<h1 class="artists-hero-name" id="artists-hero-name">Artist Name</h1>
|
|
|
<div class="artists-hero-badges" id="artists-hero-badges"></div>
|
|
|
<div class="artists-hero-genres" id="artists-hero-genres"></div>
|
|
|
<div class="artists-hero-bio" id="artists-hero-bio"></div>
|
|
|
<div class="artists-hero-stats" id="artists-hero-stats"></div>
|
|
|
<button class="discog-download-btn discog-btn-compact" id="discog-download-btn-artists" onclick="openDiscographyModal()" style="display:none;">
|
|
|
<span class="discog-btn-icon">⬇</span>
|
|
|
<span class="discog-btn-text">Download Discography</span>
|
|
|
<span class="discog-btn-shimmer"></span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="artists-hero-actions">
|
|
|
<button class="artist-detail-watchlist-btn" id="artist-detail-watchlist-btn">
|
|
|
<span class="watchlist-icon">👁️</span>
|
|
|
<span class="watchlist-text">Add to Watchlist</span>
|
|
|
</button>
|
|
|
<button class="artist-detail-watchlist-settings-btn hidden" id="artist-detail-watchlist-settings-btn" title="Watchlist Settings">
|
|
|
⚙
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Keep old hidden elements for backward compatibility with JS refs -->
|
|
|
<div id="search-artist-detail-image" style="display:none"></div>
|
|
|
<div id="search-artist-detail-name" style="display:none"></div>
|
|
|
<div id="search-artist-detail-genres" style="display:none"></div>
|
|
|
|
|
|
<div class="artist-detail-content">
|
|
|
<div class="artist-detail-tabs">
|
|
|
<button class="artist-tab active" data-tab="albums" id="albums-tab">
|
|
|
<span class="tab-icon">💿</span>
|
|
|
<span>Albums</span>
|
|
|
</button>
|
|
|
<button class="artist-tab" data-tab="singles" id="singles-tab">
|
|
|
<span class="tab-icon">🎵</span>
|
|
|
<span>Singles & EPs</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="artist-detail-discography">
|
|
|
<div class="tab-content active" id="albums-content">
|
|
|
<div class="album-cards-container" id="album-cards-container">
|
|
|
<!-- Album cards will be populated here -->
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="tab-content" id="singles-content">
|
|
|
<div class="singles-cards-container" id="singles-cards-container">
|
|
|
<!-- Singles cards will be populated here -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Similar Artists Section -->
|
|
|
<div class="similar-artists-section" id="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>
|
|
|
|
|
|
<!-- Loading State -->
|
|
|
<div class="similar-artists-loading hidden" id="similar-artists-loading">
|
|
|
<div class="loading-spinner-small"></div>
|
|
|
<span>Finding similar artists...</span>
|
|
|
</div>
|
|
|
|
|
|
<!-- Error State -->
|
|
|
<div class="similar-artists-error hidden" id="similar-artists-error">
|
|
|
<span class="error-icon">⚠️</span>
|
|
|
<span class="error-text">Unable to load similar artists</span>
|
|
|
</div>
|
|
|
|
|
|
<!-- Similar Artists Bubbles Container -->
|
|
|
<div class="similar-artists-bubbles-container" id="similar-artists-bubbles-container">
|
|
|
<!-- Artist bubble cards will be populated here -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Automations Page -->
|
|
|
<div class="page" id="automations-page">
|
|
|
<!-- List View -->
|
|
|
<div class="automations-list-view" id="automations-list-view">
|
|
|
<div class="automations-container">
|
|
|
<div class="dashboard-header">
|
|
|
<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">⚡</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">←</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>
|
|
|
|
|
|
<!-- 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>
|
|
|
</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>
|
|
|
|
|
|
<!-- 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 -->
|
|
|
<div class="library-empty hidden" id="library-empty">
|
|
|
<div class="empty-icon">🎵</div>
|
|
|
<div class="empty-title">No artists found</div>
|
|
|
<div class="empty-subtitle">Try adjusting your search or filters</div>
|
|
|
</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 to Library</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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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>
|
|
|
</div>
|
|
|
|
|
|
<!-- Artist Hero Section -->
|
|
|
<div class="artist-hero-section" id="artist-hero-section">
|
|
|
<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-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="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>
|
|
|
|
|
|
<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">Popular on Last.fm</div>
|
|
|
<div class="hero-top-tracks" id="hero-top-tracks"></div>
|
|
|
</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>
|
|
|
|
|
|
<!-- 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()">×</button>
|
|
|
</div>
|
|
|
<div class="enhanced-bulk-modal-body" id="enhanced-bulk-modal-body">
|
|
|
<!-- Populated dynamically -->
|
|
|
</div>
|
|
|
<div class="enhanced-bulk-modal-footer">
|
|
|
<button class="enhanced-bulk-btn secondary" onclick="closeBulkEditModal()">Cancel</button>
|
|
|
<button class="enhanced-bulk-btn primary" 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="enhanced-bulk-btn secondary" onclick="showBulkEditModal()">Edit Selected</button>
|
|
|
<button class="enhanced-bulk-btn tag-write" onclick="batchWriteTagsSelected()">Write Tags</button>
|
|
|
<button class="enhanced-bulk-btn clear" 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()">×</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="enhanced-bulk-btn secondary" onclick="closeTagPreviewModal()">Cancel</button>
|
|
|
<button class="enhanced-bulk-btn primary" 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()">×</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="enhanced-bulk-btn secondary" onclick="closeBatchTagPreviewModal()">Cancel</button>
|
|
|
<button class="enhanced-bulk-btn primary" 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()">×</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="enhanced-bulk-btn secondary" onclick="closeReorganizeModal()">Cancel</button>
|
|
|
<button class="enhanced-bulk-btn primary" 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>
|
|
|
|
|
|
<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">
|
|
|
<button class="discover-hero-button secondary" id="discover-hero-discography"
|
|
|
onclick="viewDiscoverHeroDiscography()">
|
|
|
<span class="button-icon">📀</span>
|
|
|
<span class="button-text">View Discography</span>
|
|
|
</button>
|
|
|
<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>
|
|
|
|
|
|
<!-- Spotify Library Section -->
|
|
|
<div class="discover-section" id="spotify-library-section" style="display: none;">
|
|
|
<div class="discover-section-header">
|
|
|
<div>
|
|
|
<h2 class="discover-section-title">Your Spotify Library</h2>
|
|
|
<p class="discover-section-subtitle" id="spotify-library-subtitle">Your saved albums on Spotify</p>
|
|
|
</div>
|
|
|
<div class="spotify-library-header-actions">
|
|
|
<button class="spotify-library-action-btn spotify-library-refresh-btn" onclick="refreshSpotifyLibraryCache()" title="Refresh from Spotify">
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
|
|
|
Refresh
|
|
|
</button>
|
|
|
<button class="spotify-library-action-btn spotify-library-download-btn" id="spotify-library-download-missing-btn" onclick="downloadMissingSpotifyLibraryAlbums()" style="display: none;">
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
|
|
Download Missing
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="spotify-library-filters" id="spotify-library-filters" style="display: none;">
|
|
|
<input type="text" class="spotify-library-search" id="spotify-library-search"
|
|
|
placeholder="Search by artist or album..." oninput="debouncedSpotifyLibrarySearch()">
|
|
|
<select id="spotify-library-status-filter" class="spotify-library-select" onchange="loadSpotifyLibraryAlbums()">
|
|
|
<option value="all">All Albums</option>
|
|
|
<option value="missing">Missing</option>
|
|
|
<option value="owned">Owned</option>
|
|
|
</select>
|
|
|
<select id="spotify-library-sort" class="spotify-library-select" onchange="loadSpotifyLibraryAlbums()">
|
|
|
<option value="date_saved">Date Saved</option>
|
|
|
<option value="artist_name">Artist</option>
|
|
|
<option value="album_name">Album</option>
|
|
|
<option value="release_date">Release Date</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="spotify-library-grid" id="spotify-library-grid">
|
|
|
<div class="discover-loading">
|
|
|
<div class="loading-spinner"></div>
|
|
|
<p>Loading your Spotify library...</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="spotify-library-pagination" id="spotify-library-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>
|
|
|
|
|
|
<!-- Recently Added Section -->
|
|
|
<div class="discover-section" style="display: none;">
|
|
|
<div class="discover-section-header">
|
|
|
<h2 class="discover-section-title">🆕 Recently Added</h2>
|
|
|
<p class="discover-section-subtitle">Latest additions to your library</p>
|
|
|
</div>
|
|
|
<div class="discover-playlist-container compact" id="personalized-recently-added">
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- Your Top 50 Section -->
|
|
|
<div class="discover-section" style="display: none;">
|
|
|
<div class="discover-section-header">
|
|
|
<h2 class="discover-section-title">🏆 Your Top 50</h2>
|
|
|
<p class="discover-section-subtitle">All-time favorites from your library</p>
|
|
|
</div>
|
|
|
<div class="discover-playlist-container compact" id="personalized-top-tracks">
|
|
|
<!-- Content will be populated dynamically -->
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Forgotten Favorites Section -->
|
|
|
<div class="discover-section" style="display: none;">
|
|
|
<div class="discover-section-header">
|
|
|
<h2 class="discover-section-title">💎 Forgotten Favorites</h2>
|
|
|
<p class="discover-section-subtitle">Rediscover tracks you used to love</p>
|
|
|
</div>
|
|
|
<div class="discover-playlist-container compact" id="personalized-forgotten-favorites">
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- Familiar Favorites Section -->
|
|
|
<div class="discover-section" style="display: none;">
|
|
|
<div class="discover-section-header">
|
|
|
<div>
|
|
|
<h2 class="discover-section-title">❤️ Familiar Favorites</h2>
|
|
|
<p class="discover-section-subtitle">Your reliable go-to tracks</p>
|
|
|
</div>
|
|
|
<div class="discover-section-actions">
|
|
|
<button class="action-button secondary"
|
|
|
onclick="openDownloadModalForDiscoverPlaylist('familiar_favorites', 'Familiar Favorites')"
|
|
|
title="Download missing tracks">
|
|
|
<span class="button-icon">↓</span>
|
|
|
<span class="button-text">Download</span>
|
|
|
</button>
|
|
|
<button class="action-button primary" id="familiar-favorites-sync-btn"
|
|
|
onclick="startDiscoverPlaylistSync('familiar_favorites', 'Familiar Favorites')"
|
|
|
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="familiar-favorites-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="familiar-favorites-sync-completed">0</span></span>
|
|
|
<span class="sync-stat">⏳ <span id="familiar-favorites-sync-pending">0</span></span>
|
|
|
<span class="sync-stat">✗ <span id="familiar-favorites-sync-failed">0</span></span>
|
|
|
<span class="sync-stat">(<span
|
|
|
id="familiar-favorites-sync-percentage">0</span>%)</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="discover-playlist-container compact" id="personalized-familiar-favorites">
|
|
|
<!-- 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 — 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>
|
|
|
|
|
|
<!-- 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="explorer-container">
|
|
|
<!-- Header (compact) -->
|
|
|
<div class="dashboard-header" style="margin-bottom: 12px;">
|
|
|
<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 inline -->
|
|
|
<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')">Albums</button>
|
|
|
<button class="explorer-mode-btn" data-mode="discographies"
|
|
|
onclick="explorerSetMode('discographies')">Discographies</button>
|
|
|
</div>
|
|
|
<button class="explorer-build-btn" id="explorer-build-btn"
|
|
|
onclick="explorerBuildTree()">Explore</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="explorer-picker-scroll" id="explorer-picker-scroll">
|
|
|
<!-- Populated by JS -->
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Action bar (sticky, appears after tree built) -->
|
|
|
<div class="explorer-action-bar" id="explorer-action-bar" style="display: none;">
|
|
|
<span class="explorer-selection-count" id="explorer-selection-count">0 albums selected</span>
|
|
|
<div class="explorer-action-buttons">
|
|
|
<button class="explorer-action-btn" onclick="explorerSelectAll()">Select All</button>
|
|
|
<button class="explorer-action-btn" onclick="explorerDeselectAll()">Deselect</button>
|
|
|
<button class="explorer-action-btn primary" onclick="explorerAddToWishlist()">
|
|
|
Add to Wishlist</button>
|
|
|
</div>
|
|
|
<span class="explorer-nav-hint">Scroll to zoom · Right-drag to pan · 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">−</button>
|
|
|
<button class="explorer-zoom-btn" onclick="explorerFitToView()" title="Fit to view">⬜</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="dashboard-header">
|
|
|
<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="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>
|
|
|
</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</h3>
|
|
|
|
|
|
<!-- Spotify Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title spotify-title">Spotify</h4>
|
|
|
<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>
|
|
|
<button class="auth-button disconnect-button"
|
|
|
onclick="clearSpotifyCacheAndFallback()"
|
|
|
title="Clear Spotify token cache and switch to your configured fallback metadata source">🗑️
|
|
|
Clear Cache & Use Fallback</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Tidal Playlist/Metadata Auth -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title tidal-title">Tidal (Playlists & Metadata)</h4>
|
|
|
<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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<!-- Qobuz Metadata/Enrichment Auth -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title qobuz-title">Qobuz (Metadata & Enrichment)</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- ListenBrainz Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title listenbrainz-title">ListenBrainz</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- AcoustID Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title acoustid-title">AcoustID Verification</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- Last.fm Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title lastfm-title">Last.fm</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- Genius Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title genius-title">Genius</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- iTunes Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title itunes-title">iTunes / Apple Music</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- Metadata Source Selection -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title" style="color: #e8e8e8;">Metadata Source</h4>
|
|
|
<div class="form-group">
|
|
|
<label>Fallback Source:</label>
|
|
|
<select id="metadata-fallback-source">
|
|
|
<option value="itunes">iTunes / Apple Music</option>
|
|
|
<option value="deezer">Deezer</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="callback-info">
|
|
|
<div class="callback-help">When Spotify is not connected, this source provides artist, album, and track metadata. Hydrabase requires a connection configured above.</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Hydrabase P2P Metadata -->
|
|
|
<div class="api-service-frame" data-stg="connections">
|
|
|
<h4 class="service-title" style="color: #00b4d8;">Hydrabase</h4>
|
|
|
<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>
|
|
|
|
|
|
<!-- 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://jellyfin.org/images/logo.svg" 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>
|
|
|
</div>
|
|
|
|
|
|
<!-- Plex Settings -->
|
|
|
<div class="server-config-container" id="plex-container">
|
|
|
<div class="form-group">
|
|
|
<label>Plex Server URL:</label>
|
|
|
<input type="url" id="plex-url" placeholder="http://localhost:32400">
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label>Plex Token:</label>
|
|
|
<input type="password" id="plex-token" placeholder="X-Plex-Token">
|
|
|
</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 class="detect-button" onclick="autoDetectPlex()">Auto-detect</button>
|
|
|
<button class="test-button" onclick="testConnection('plex')">Test</button>
|
|
|
</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="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>
|
|
|
|
|
|
<!-- 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>Slskd 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>Matched Transfer Dir (Plex Music Dir?):</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 Staging Dir:</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>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="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>
|
|
|
|
|
|
<!-- 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.
|
|
|
</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></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></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 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>
|
|
|
</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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- 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 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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- 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, $year, $quality (filename only)</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, $album, $albumtype, $year, $quality (filename only)</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<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)</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 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>
|
|
|
|
|
|
<!-- 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).</small>
|
|
|
</div>
|
|
|
<div class="form-group">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="lrclib-enabled" checked>
|
|
|
Generate .lrc lyrics files (LRClib)
|
|
|
</label>
|
|
|
</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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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">4 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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Qobuz -->
|
|
|
<div class="tag-service-group">
|
|
|
<div class="tag-service-header" onclick="toggleTagGroup(this)">
|
|
|
<span class="tag-group-arrow">▶</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">▶</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">▶</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>
|
|
|
|
|
|
<!-- General -->
|
|
|
<div class="tag-service-group">
|
|
|
<div class="tag-service-header" onclick="toggleTagGroup(this)">
|
|
|
<span class="tag-group-arrow">▶</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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- Security Settings -->
|
|
|
<div class="settings-group" data-stg="advanced">
|
|
|
<h3>🔒 Security</h3>
|
|
|
|
|
|
<div class="form-group" id="security-pin-setup" style="display: none;">
|
|
|
<label>Set Admin PIN:</label>
|
|
|
<div class="setting-help-text" style="margin-bottom: 8px;">
|
|
|
You need to set a PIN before enabling 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">
|
|
|
When enabled, a lock screen appears on every page load. PIN is verified against the admin account. Closing the browser 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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
|
|
<!-- 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 and albums
|
|
|
</label>
|
|
|
</div>
|
|
|
<div class="help-text">
|
|
|
Saves an M3U playlist file to the same folder as the downloaded tracks.
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Playlist Sync Settings -->
|
|
|
<div class="settings-group" data-stg="library">
|
|
|
<h3>Playlist Sync Settings</h3>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="create-backup" checked>
|
|
|
Create playlist backups before sync
|
|
|
</label>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Logging Information (Read-only) -->
|
|
|
<div class="settings-group" data-stg="advanced">
|
|
|
<h3>Logging Information</h3>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label>Log Level:</label>
|
|
|
<div class="readonly-field" id="log-level-display">DEBUG</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>
|
|
|
</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>
|
|
|
|
|
|
<!-- Save Button -->
|
|
|
<div class="settings-actions">
|
|
|
<button class="save-button" id="save-settings">Save Settings</button>
|
|
|
</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>
|
|
|
|
|
|
<!-- Stats Page -->
|
|
|
<div class="page" id="stats-page">
|
|
|
<div class="stats-container">
|
|
|
<div class="stats-header">
|
|
|
<div class="stats-header-title">
|
|
|
<img src="/static/trans2.png" alt="Stats" class="page-header-icon">
|
|
|
<h1 class="header-title">Listening Stats</h1>
|
|
|
</div>
|
|
|
<div class="stats-header-controls">
|
|
|
<div class="stats-time-range" id="stats-time-range">
|
|
|
<button class="stats-range-btn active" data-range="7d">7 Days</button>
|
|
|
<button class="stats-range-btn" data-range="30d">30 Days</button>
|
|
|
<button class="stats-range-btn" data-range="12m">12 Months</button>
|
|
|
<button class="stats-range-btn" data-range="all">All Time</button>
|
|
|
</div>
|
|
|
<div class="stats-sync-controls">
|
|
|
<span class="stats-last-synced" id="stats-last-synced"></span>
|
|
|
<button class="stats-sync-btn" id="stats-sync-btn" onclick="triggerStatsSync()" title="Sync now"><span class="stats-sync-icon">↻</span></button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Overview Cards -->
|
|
|
<div class="stats-overview" id="stats-overview">
|
|
|
<div class="stats-card">
|
|
|
<div class="stats-card-value" id="stats-total-plays">0</div>
|
|
|
<div class="stats-card-label">Total Plays</div>
|
|
|
</div>
|
|
|
<div class="stats-card">
|
|
|
<div class="stats-card-value" id="stats-listening-time">0h</div>
|
|
|
<div class="stats-card-label">Listening Time</div>
|
|
|
</div>
|
|
|
<div class="stats-card">
|
|
|
<div class="stats-card-value" id="stats-unique-artists">0</div>
|
|
|
<div class="stats-card-label">Artists</div>
|
|
|
</div>
|
|
|
<div class="stats-card">
|
|
|
<div class="stats-card-value" id="stats-unique-albums">0</div>
|
|
|
<div class="stats-card-label">Albums</div>
|
|
|
</div>
|
|
|
<div class="stats-card">
|
|
|
<div class="stats-card-value" id="stats-unique-tracks">0</div>
|
|
|
<div class="stats-card-label">Tracks</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Main Grid: Charts + Rankings -->
|
|
|
<div class="stats-main-grid">
|
|
|
<div class="stats-left-col">
|
|
|
<div class="stats-section-card">
|
|
|
<div class="stats-section-title">Listening Activity</div>
|
|
|
<div style="position:relative;height:220px;">
|
|
|
<canvas id="stats-timeline-chart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stats-section-card">
|
|
|
<div class="stats-section-title">Genre Breakdown</div>
|
|
|
<div class="stats-genre-chart-container">
|
|
|
<canvas id="stats-genre-chart"></canvas>
|
|
|
<div class="stats-genre-legend" id="stats-genre-legend"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stats-section-card">
|
|
|
<div class="stats-section-title">Recently Played</div>
|
|
|
<div class="stats-recent-list" id="stats-recent-plays"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stats-right-col">
|
|
|
<div class="stats-section-card">
|
|
|
<div class="stats-section-title">Top Artists</div>
|
|
|
<div class="stats-top-artists-visual" id="stats-top-artists-visual"></div>
|
|
|
<div class="stats-ranked-list" id="stats-top-artists"></div>
|
|
|
</div>
|
|
|
<div class="stats-section-card">
|
|
|
<div class="stats-section-title">Top Albums</div>
|
|
|
<div class="stats-ranked-list" id="stats-top-albums"></div>
|
|
|
</div>
|
|
|
<div class="stats-section-card">
|
|
|
<div class="stats-section-title">Top Tracks</div>
|
|
|
<div class="stats-ranked-list" id="stats-top-tracks"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Library Health -->
|
|
|
<div class="stats-section-card stats-full-width">
|
|
|
<div class="stats-section-title">Library Health</div>
|
|
|
<div class="stats-health-grid" id="stats-library-health">
|
|
|
<div class="stats-health-item">
|
|
|
<div class="stats-health-label">Format Breakdown</div>
|
|
|
<div class="stats-format-bar" id="stats-format-bar"></div>
|
|
|
</div>
|
|
|
<div class="stats-health-item">
|
|
|
<div class="stats-health-value" id="stats-unplayed">0</div>
|
|
|
<div class="stats-health-label">Unplayed Tracks</div>
|
|
|
</div>
|
|
|
<div class="stats-health-item">
|
|
|
<div class="stats-health-value" id="stats-total-duration">0h</div>
|
|
|
<div class="stats-health-label">Total Duration</div>
|
|
|
</div>
|
|
|
<div class="stats-health-item">
|
|
|
<div class="stats-health-value" id="stats-total-tracks-count">0</div>
|
|
|
<div class="stats-health-label">Total Tracks</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stats-enrichment" id="stats-enrichment-coverage"></div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Database Storage -->
|
|
|
<div class="stats-section-card stats-full-width">
|
|
|
<div class="stats-section-title">Database Storage</div>
|
|
|
<div class="stats-db-storage-wrap">
|
|
|
<div class="stats-db-chart-container">
|
|
|
<canvas id="stats-db-storage-chart"></canvas>
|
|
|
<div class="stats-db-total" id="stats-db-total"></div>
|
|
|
</div>
|
|
|
<div class="stats-db-legend" id="stats-db-legend"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Empty State -->
|
|
|
<div class="stats-empty hidden" id="stats-empty">
|
|
|
<div class="stats-empty-icon">📊</div>
|
|
|
<h3>No Listening Data Yet</h3>
|
|
|
<p>Enable "Listening Stats" in Settings to start tracking your listening activity from your media server.</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Import Page -->
|
|
|
<div class="page" id="import-page">
|
|
|
<div class="import-page-container">
|
|
|
<!-- Header with staging info -->
|
|
|
<div class="import-page-header">
|
|
|
<div class="import-page-title-row">
|
|
|
<h1 class="import-page-title"><img src="/static/import.png" class="page-header-icon" alt=""><span>Import Music</span></h1>
|
|
|
<button class="import-page-refresh-btn" onclick="importPageRefreshStaging()" title="Re-scan staging folder">
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M13.65 2.35A8 8 0 1 0 16 8h-2a6 6 0 1 1-1.76-4.24L10 6h6V0l-2.35 2.35z"/></svg>
|
|
|
Refresh
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="import-page-staging-bar" id="import-staging-bar">
|
|
|
<span class="import-staging-path" id="import-page-staging-path">Staging folder: loading...</span>
|
|
|
<span class="import-staging-stats" id="import-page-staging-stats"></span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Processing Queue -->
|
|
|
<div class="import-page-queue hidden" id="import-page-queue">
|
|
|
<div class="import-page-queue-header">
|
|
|
<span class="import-page-queue-title">Processing</span>
|
|
|
<button class="import-page-queue-clear" id="import-page-queue-clear" onclick="importPageClearFinishedJobs()">Clear finished</button>
|
|
|
</div>
|
|
|
<div class="import-page-queue-list" id="import-page-queue-list"></div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Tab Bar -->
|
|
|
<div class="import-page-tab-bar">
|
|
|
<button class="import-page-tab active" id="import-page-tab-album" onclick="importPageSwitchTab('album')">Albums</button>
|
|
|
<button class="import-page-tab" id="import-page-tab-singles" onclick="importPageSwitchTab('singles')">Singles</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Album Tab -->
|
|
|
<div class="import-page-tab-content active" id="import-page-album-content">
|
|
|
<!-- Search state -->
|
|
|
<div id="import-page-album-search-section">
|
|
|
<div class="import-page-suggestions" id="import-page-suggestions">
|
|
|
<div class="import-page-section-label">Suggested from your staging folder</div>
|
|
|
<div class="import-page-album-grid" id="import-page-suggestions-grid"></div>
|
|
|
</div>
|
|
|
<div class="import-page-search-bar">
|
|
|
<input type="text" id="import-page-album-search-input" class="import-page-search-input"
|
|
|
placeholder="Search for an album..." onkeydown="if(event.key==='Enter')importPageSearchAlbum()">
|
|
|
<button class="import-page-search-btn" onclick="importPageSearchAlbum()">Search</button>
|
|
|
<button class="import-page-clear-btn hidden" id="import-page-album-clear-btn"
|
|
|
onclick="importPageResetAlbumSearch()" title="Clear search">✕</button>
|
|
|
</div>
|
|
|
<div class="import-page-album-grid" id="import-page-album-results"></div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Match state (hidden initially) -->
|
|
|
<div id="import-page-album-match-section" class="hidden">
|
|
|
<div class="import-page-album-hero" id="import-page-album-hero"></div>
|
|
|
<div class="import-page-match-header">
|
|
|
<h3>Track Matching</h3>
|
|
|
<div class="import-page-match-actions">
|
|
|
<button class="import-page-secondary-btn" onclick="importPageAutoRematch()">Re-match Automatically</button>
|
|
|
<button class="import-page-back-btn" onclick="importPageResetAlbumSearch()">Back to Search</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="import-page-match-list" id="import-page-match-list"></div>
|
|
|
|
|
|
<!-- Unmatched file pool for drag-drop -->
|
|
|
<div class="import-page-unmatched-pool" id="import-page-unmatched-pool">
|
|
|
<div class="import-page-pool-label">Unmatched Files (<span id="import-page-unmatched-count">0</span>)</div>
|
|
|
<div class="import-page-pool-chips" id="import-page-pool-chips"></div>
|
|
|
</div>
|
|
|
|
|
|
<div class="import-page-match-footer">
|
|
|
<div class="import-page-match-stats" id="import-page-match-stats"></div>
|
|
|
<button class="import-page-process-btn" id="import-page-album-process-btn"
|
|
|
onclick="importPageProcessAlbum()">Process Album</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Singles Tab -->
|
|
|
<div class="import-page-tab-content" id="import-page-singles-content">
|
|
|
<div class="import-page-singles-header">
|
|
|
<div class="import-page-singles-actions">
|
|
|
<button class="import-page-secondary-btn" onclick="importPageSelectAllSingles()">
|
|
|
<span id="import-page-select-all-text">Select All</span>
|
|
|
</button>
|
|
|
<button class="import-page-process-btn" id="import-page-singles-process-btn"
|
|
|
onclick="importPageProcessSingles()" disabled>Process Selected (0)</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="import-page-singles-list" id="import-page-singles-list">
|
|
|
<div class="import-page-empty-state">Navigate to this page to scan your staging folder for audio files.</div>
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Issues Page -->
|
|
|
<div class="page" id="issues-page">
|
|
|
<div class="issues-container">
|
|
|
<div class="issues-header">
|
|
|
<div class="issues-header-left">
|
|
|
<h2 class="issues-title">Issues</h2>
|
|
|
<p class="issues-subtitle" id="issues-subtitle">Track and resolve library problems</p>
|
|
|
</div>
|
|
|
<div class="issues-header-right">
|
|
|
<div class="issues-filters" id="issues-filters">
|
|
|
<select id="issues-filter-status" class="issues-filter-select" onchange="loadIssuesPage()">
|
|
|
<option value="">All Status</option>
|
|
|
<option value="open" selected>Open</option>
|
|
|
<option value="in_progress">In Progress</option>
|
|
|
<option value="resolved">Resolved</option>
|
|
|
<option value="dismissed">Dismissed</option>
|
|
|
</select>
|
|
|
<select id="issues-filter-category" class="issues-filter-select" onchange="loadIssuesPage()">
|
|
|
<option value="">All Categories</option>
|
|
|
<optgroup label="Track Issues">
|
|
|
<option value="wrong_track">Wrong Track</option>
|
|
|
<option value="wrong_artist">Wrong Artist</option>
|
|
|
<option value="wrong_album">Wrong Album</option>
|
|
|
<option value="audio_quality">Audio Quality</option>
|
|
|
</optgroup>
|
|
|
<optgroup label="Album Issues">
|
|
|
<option value="wrong_cover">Wrong Cover Art</option>
|
|
|
<option value="duplicate_tracks">Duplicate Tracks</option>
|
|
|
<option value="missing_tracks">Missing Tracks</option>
|
|
|
<option value="incomplete_album">Incomplete Album</option>
|
|
|
</optgroup>
|
|
|
<optgroup label="Both">
|
|
|
<option value="wrong_metadata">Wrong Metadata</option>
|
|
|
<option value="other">Other</option>
|
|
|
</optgroup>
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="issues-stats" id="issues-stats"></div>
|
|
|
<div class="issues-list" id="issues-list">
|
|
|
<div class="issues-empty">Loading issues...</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Report Issue Modal -->
|
|
|
<div class="modal-overlay hidden" id="report-issue-overlay">
|
|
|
<div class="enhanced-bulk-modal report-issue-modal">
|
|
|
<div class="enhanced-bulk-modal-header">
|
|
|
<h3 id="report-issue-title">Report an Issue</h3>
|
|
|
<button class="enhanced-bulk-modal-close" onclick="closeReportIssueModal()">×</button>
|
|
|
</div>
|
|
|
<div class="enhanced-bulk-modal-body" id="report-issue-body">
|
|
|
<!-- Populated dynamically -->
|
|
|
</div>
|
|
|
<div class="enhanced-bulk-modal-footer">
|
|
|
<button class="enhanced-bulk-btn secondary" onclick="closeReportIssueModal()">Cancel</button>
|
|
|
<button class="enhanced-bulk-btn primary" id="report-issue-submit-btn" onclick="submitIssue()">Submit Issue</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Issue Detail Modal (Admin) -->
|
|
|
<div class="modal-overlay hidden" id="issue-detail-overlay">
|
|
|
<div class="enhanced-bulk-modal issue-detail-modal">
|
|
|
<div class="enhanced-bulk-modal-header">
|
|
|
<h3 id="issue-detail-title">Issue Details</h3>
|
|
|
<button class="enhanced-bulk-modal-close" onclick="closeIssueDetailModal()">×</button>
|
|
|
</div>
|
|
|
<div class="enhanced-bulk-modal-body" id="issue-detail-body">
|
|
|
<!-- Populated dynamically -->
|
|
|
</div>
|
|
|
<div class="enhanced-bulk-modal-footer" id="issue-detail-footer">
|
|
|
<button class="enhanced-bulk-btn secondary" onclick="closeIssueDetailModal()">Close</button>
|
|
|
</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>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
<!-- 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">
|
|
|
<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>
|
|
|
</div>
|
|
|
<div class="np-track-info">
|
|
|
<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">
|
|
|
<button class="np-action-btn" id="np-goto-artist" title="Go to Artist">
|
|
|
<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>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- Right: controls -->
|
|
|
<div class="np-right">
|
|
|
<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>
|
|
|
<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>
|
|
|
<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>
|
|
|
<!-- 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">
|
|
|
<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>
|
|
|
</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 Download to Spotify</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()">×</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 2.1 — 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()">×</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="wishlist-modal-btn wishlist-modal-btn-secondary"
|
|
|
onclick="closeAddToWishlistModal()">
|
|
|
Close
|
|
|
</button>
|
|
|
<button class="wishlist-modal-btn wishlist-modal-btn-download" id="wishlist-download-now-btn"
|
|
|
onclick="handleWishlistDownloadNow()">
|
|
|
Download Now
|
|
|
</button>
|
|
|
<button class="wishlist-modal-btn wishlist-modal-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()">×</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="watchlist-artist-config-content">
|
|
|
<div class="watchlist-artist-config-body">
|
|
|
<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>
|
|
|
|
|
|
<!-- 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="config-modal-btn config-modal-btn-secondary"
|
|
|
onclick="closeWatchlistArtistConfigModal()">
|
|
|
Cancel
|
|
|
</button>
|
|
|
<button class="config-modal-btn config-modal-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" id="watchlist-global-config-modal">
|
|
|
<div class="watchlist-artist-config-header" style="padding: 24px 28px;">
|
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
|
<span style="font-size: 28px;">⚙️</span>
|
|
|
<div>
|
|
|
<h2 style="color: #fff; margin: 0; font-size: 22px;">Global Watchlist Settings</h2>
|
|
|
<p style="color: #b3b3b3; margin: 4px 0 0; font-size: 13px;">
|
|
|
Override per-artist settings for all watchlist scans
|
|
|
</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<span class="watchlist-artist-config-close" onclick="closeWatchlistGlobalSettingsModal()">×</span>
|
|
|
</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="config-modal-btn config-modal-btn-secondary"
|
|
|
onclick="closeWatchlistGlobalSettingsModal()">
|
|
|
Cancel
|
|
|
</button>
|
|
|
<button class="config-modal-btn config-modal-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="retag-modal-overlay" id="retag-modal">
|
|
|
<div class="retag-modal-container">
|
|
|
<div class="retag-modal-header">
|
|
|
<div class="retag-modal-header-left">
|
|
|
<h2 class="retag-modal-title">Retag Tool</h2>
|
|
|
</div>
|
|
|
<div class="retag-header-actions">
|
|
|
<button class="retag-clear-all-btn" id="retag-clear-all-btn" onclick="clearAllRetagGroups(this)">Clear All</button>
|
|
|
<button class="retag-modal-close" onclick="closeRetagModal()">×</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="retag-batch-bar" id="retag-batch-bar" style="display: none;">
|
|
|
<span class="retag-batch-count" id="retag-batch-count">0 selected</span>
|
|
|
<button class="retag-batch-remove-btn" onclick="batchRemoveRetagGroups()">Remove Selected</button>
|
|
|
</div>
|
|
|
<div class="retag-modal-body" id="retag-modal-body">
|
|
|
<div class="retag-loading">Loading downloads...</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Retag Search Sub-Modal -->
|
|
|
<div class="retag-search-overlay" id="retag-search-modal">
|
|
|
<div class="retag-search-container">
|
|
|
<div class="retag-search-header">
|
|
|
<h3 class="retag-search-title" id="retag-search-title">Search for Correct Album</h3>
|
|
|
<button class="retag-search-close" onclick="closeRetagSearch()">×</button>
|
|
|
</div>
|
|
|
<div class="retag-search-input-section">
|
|
|
<input type="text" id="retag-search-input" placeholder="Search albums..." autocomplete="off">
|
|
|
</div>
|
|
|
<div class="retag-search-results" id="retag-search-results"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="tool-help-modal" id="tool-help-modal">
|
|
|
<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">×</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()">×</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>
|
|
|
</div>
|
|
|
<div class="library-history-list" id="library-history-list"></div>
|
|
|
<div class="library-history-pagination" id="library-history-pagination"></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()">×</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="clearMetadataCache()">Clear All</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button class="mcache-modal-close" onclick="closeMetadataCacheModal()">×</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">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>
|
|
|
</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()">×</button>
|
|
|
</div>
|
|
|
<div class="mcache-detail-body" id="mcache-detail-body">
|
|
|
<!-- Detail content populated by JS -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Library Maintenance Modal -->
|
|
|
<div class="repair-modal-overlay" id="repair-modal" style="display:none;" onclick="if(event.target===this)closeRepairModal()">
|
|
|
<div class="repair-modal">
|
|
|
<div class="repair-modal-header">
|
|
|
<div class="repair-modal-header-left">
|
|
|
<img src="/static/whisoul.png" alt="" class="repair-modal-logo" />
|
|
|
<div class="repair-modal-header-text">
|
|
|
<h2 class="repair-modal-title">Library Maintenance</h2>
|
|
|
<p class="repair-modal-subtitle">Scan, detect, and fix issues in your music library</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="repair-modal-header-actions">
|
|
|
<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>
|
|
|
<button class="repair-modal-close" onclick="closeRepairModal()">×</button>
|
|
|
</div>
|
|
|
</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;">
|
|
|
<!-- Summary dashboard -->
|
|
|
<div class="repair-findings-dashboard" id="repair-findings-dashboard"></div>
|
|
|
|
|
|
<!-- Toolbar: filters + bulk actions -->
|
|
|
<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;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="repair-bulk-btn fix" onclick="bulkFixFindings()">Fix Selected</button>
|
|
|
<button class="repair-bulk-btn" onclick="bulkRepairAction('dismiss')">Dismiss Selected</button>
|
|
|
<button class="repair-bulk-btn fix-all" 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>
|
|
|
</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">You can wait for the ban to expire (the app uses Apple Music in the meantime) or disconnect Spotify to clear the ban 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 Spotify
|
|
|
<span class="rate-limit-disconnect-sub">Clear ban, pause enrichment & switch to fallback source</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script src="{{ url_for('static', filename='vendor/socket.io.min.js') }}"></script>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
|
|
<!-- Floating helper toggle — always accessible above modals -->
|
|
|
<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') }}"></script>
|
|
|
<script src="{{ url_for('static', filename='helper.js') }}"></script>
|
|
|
<script src="{{ url_for('static', filename='particles.js') }}"></script>
|
|
|
<script src="{{ url_for('static', filename='worker-orbs.js') }}"></script>
|
|
|
</body>
|
|
|
|
|
|
</html> |