|
|
<!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>
|
|
|
<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">
|
|
|
<h1 class="app-name">SoulSync</h1>
|
|
|
<p class="app-subtitle">Music Sync & Manager</p>
|
|
|
</div>
|
|
|
|
|
|
<!-- Navigation Section -->
|
|
|
<nav class="sidebar-nav">
|
|
|
<button class="nav-button active" data-page="dashboard">
|
|
|
<span class="nav-icon">📊</span>
|
|
|
<span class="nav-text">Dashboard</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="sync">
|
|
|
<span class="nav-icon">🔄</span>
|
|
|
<span class="nav-text">Sync</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="downloads">
|
|
|
<span class="nav-icon">📥</span>
|
|
|
<span class="nav-text">Search</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="discover">
|
|
|
<span class="nav-icon">🎧</span>
|
|
|
<span class="nav-text">Discover</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="artists">
|
|
|
<span class="nav-icon">🎵</span>
|
|
|
<span class="nav-text">Artists</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="library">
|
|
|
<span class="nav-icon">📚</span>
|
|
|
<span class="nav-text">Library</span>
|
|
|
</button>
|
|
|
<button class="nav-button" data-page="settings">
|
|
|
<span class="nav-icon">⚙️</span>
|
|
|
<span class="nav-text">Settings</span>
|
|
|
</button>
|
|
|
</nav>
|
|
|
|
|
|
<!-- Spacer -->
|
|
|
<div class="sidebar-spacer"></div>
|
|
|
|
|
|
<!-- Media Player Section -->
|
|
|
<div class="media-player" id="media-player">
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- Header (always visible) -->
|
|
|
<div class="media-header">
|
|
|
<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>
|
|
|
<button class="play-button" id="play-button" disabled>▷</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Expanded Content (hidden initially) -->
|
|
|
<div class="media-expanded hidden" id="media-expanded">
|
|
|
<div class="album-name" id="album-name">Unknown Album</div>
|
|
|
|
|
|
<!-- Progress Bar and Time Display -->
|
|
|
<div class="progress-section">
|
|
|
<div class="time-display">
|
|
|
<span class="current-time" id="current-time">0:00</span>
|
|
|
<span class="time-separator">/</span>
|
|
|
<span class="total-time" id="total-time">0:00</span>
|
|
|
</div>
|
|
|
<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>
|
|
|
|
|
|
<div class="media-controls">
|
|
|
<div class="volume-control">
|
|
|
<span class="volume-icon">🔊</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>⏹</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- No Track Message -->
|
|
|
<div class="no-track-message" id="no-track-message">
|
|
|
Start playing music to see controls
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Crypto Donation Section -->
|
|
|
<div class="crypto-donation">
|
|
|
<div class="donation-header">
|
|
|
<span class="donation-title">Support Development</span>
|
|
|
<button class="toggle-button" id="donation-toggle">Show</button>
|
|
|
</div>
|
|
|
<div class="donation-addresses hidden" id="donation-addresses">
|
|
|
<div class="donation-item" onclick="openKofi()">
|
|
|
<span class="donation-name">Ko-fi</span>
|
|
|
<span class="donation-link">Click to open</span>
|
|
|
</div>
|
|
|
<div class="donation-item" onclick="copyAddress('3JVWrRSkozAQSmw5DXYVxYKsM9bndPTqdS', 'Bitcoin')">
|
|
|
<span class="donation-name">Bitcoin</span>
|
|
|
<span class="donation-address">3JVWrR...dPTqdS</span>
|
|
|
</div>
|
|
|
<div class="donation-item"
|
|
|
onclick="copyAddress('0x343fC48c2cd1C6332b0df9a58F86e6520a026AC5', 'Ethereum')">
|
|
|
<span class="donation-name">Ethereum</span>
|
|
|
<span class="donation-address">0x343f...026AC5</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Version Section -->
|
|
|
<div class="version-section">
|
|
|
<button class="version-button" onclick="showVersionInfo()">v1.6</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">Soulseek</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Main Content Area -->
|
|
|
<div class="main-content">
|
|
|
<!-- 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">System Dashboard</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>
|
|
|
<button class="import-button" id="import-button" onclick="openImportModal()"
|
|
|
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">👁️ Watchlist
|
|
|
(0)</button>
|
|
|
<button class="header-button wishlist-button" id="wishlist-button">🎵 Wishlist (0)</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">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>
|
|
|
|
|
|
<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">
|
|
|
<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="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>
|
|
|
</div>
|
|
|
|
|
|
<div class="dashboard-section">
|
|
|
<h3 class="section-title">Recent Activity</h3>
|
|
|
<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">
|
|
|
<h2 class="sync-title">Playlist Sync</h2>
|
|
|
<p class="sync-subtitle">Synchronize your Spotify, Tidal, and YouTube playlists with your media
|
|
|
server</p>
|
|
|
</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 active" data-tab="spotify">
|
|
|
<span class="tab-icon spotify-icon"></span> Spotify
|
|
|
</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="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>
|
|
|
</div>
|
|
|
|
|
|
<!-- Spotify Tab Content -->
|
|
|
<div class="sync-tab-content active" 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>
|
|
|
|
|
|
<!-- 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="playlist-scroll-container" id="youtube-playlist-container">
|
|
|
<div class="playlist-placeholder">Parsed YouTube playlists will appear here.</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Beatport Tab Content -->
|
|
|
<div class="sync-tab-content" id="beatport-tab-content">
|
|
|
<!-- Beatport Nested Tabs -->
|
|
|
<div class="beatport-tabs">
|
|
|
<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>
|
|
|
|
|
|
<!-- 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>
|
|
|
</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">
|
|
|
|
|
|
<!-- ======================================================= -->
|
|
|
<!-- == 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">🎵 Music Downloads</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">
|
|
|
|
|
|
<!-- 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>
|
|
|
</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">🎵 Discover Artists</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">
|
|
|
<div class="artist-detail-header">
|
|
|
<div class="artist-detail-header-left">
|
|
|
<button class="artist-detail-back-button" id="artist-detail-back-button">
|
|
|
<span class="back-icon">←</span>
|
|
|
<span>Back to Results</span>
|
|
|
</button>
|
|
|
|
|
|
<div class="artist-detail-info">
|
|
|
<div class="artist-detail-image" id="search-artist-detail-image"></div>
|
|
|
<div class="artist-detail-text">
|
|
|
<h2 class="artist-detail-name" id="search-artist-detail-name">Artist Name</h2>
|
|
|
<p class="artist-detail-genres" id="search-artist-detail-genres">Genres</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<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>
|
|
|
</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>
|
|
|
|
|
|
<!-- 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">📚 Music Library</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>
|
|
|
</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>
|
|
|
</div>
|
|
|
|
|
|
<!-- Artist Hero Section -->
|
|
|
<div class="artist-hero-section" id="artist-hero-section">
|
|
|
<div class="artist-hero-content">
|
|
|
<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>
|
|
|
|
|
|
<div class="artist-info">
|
|
|
<h1 class="artist-name" id="artist-detail-name">Artist Name</h1>
|
|
|
<div class="artist-genres-container" id="artist-genres"></div>
|
|
|
|
|
|
<div class="collection-overview">
|
|
|
<div class="collection-category">
|
|
|
<div class="category-header">
|
|
|
<span class="category-label">Albums</span>
|
|
|
<span class="category-stats" id="albums-stats">0 owned, 0 missing</span>
|
|
|
</div>
|
|
|
<div class="completion-section">
|
|
|
<div class="completion-bar">
|
|
|
<div class="completion-fill" id="albums-completion-fill" style="width: 0%">
|
|
|
</div>
|
|
|
</div>
|
|
|
<span class="completion-text" id="albums-completion-text">0%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="collection-category">
|
|
|
<div class="category-header">
|
|
|
<span class="category-label">EPs</span>
|
|
|
<span class="category-stats" id="eps-stats">0 owned, 0 missing</span>
|
|
|
</div>
|
|
|
<div class="completion-section">
|
|
|
<div class="completion-bar">
|
|
|
<div class="completion-fill" id="eps-completion-fill" style="width: 0%">
|
|
|
</div>
|
|
|
</div>
|
|
|
<span class="completion-text" id="eps-completion-text">0%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="collection-category">
|
|
|
<div class="category-header">
|
|
|
<span class="category-label">Singles</span>
|
|
|
<span class="category-stats" id="singles-stats">0 owned, 0 missing</span>
|
|
|
</div>
|
|
|
<div class="completion-section">
|
|
|
<div class="completion-bar">
|
|
|
<div class="completion-fill" id="singles-completion-fill" style="width: 0%">
|
|
|
</div>
|
|
|
</div>
|
|
|
<span class="completion-text" id="singles-completion-text">0%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</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 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>
|
|
|
</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>
|
|
|
</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</h2>
|
|
|
<p class="discover-section-subtitle">Create a custom playlist from your favorite artists</p>
|
|
|
</div>
|
|
|
<div class="build-playlist-container">
|
|
|
<!-- Artist Search -->
|
|
|
<div class="build-playlist-search-section">
|
|
|
<input type="text" id="build-playlist-search"
|
|
|
placeholder="Search for artists (1-5 artists)..."
|
|
|
oninput="searchBuildPlaylistArtists()" />
|
|
|
<div id="build-playlist-search-results" class="build-playlist-search-results"></div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Selected Artists -->
|
|
|
<div class="build-playlist-selected-section">
|
|
|
<h3>Selected Artists:</h3>
|
|
|
<div id="build-playlist-selected-artists" class="build-playlist-selected-artists">
|
|
|
<div class="build-playlist-no-selection">No artists selected. Search and select 1-5
|
|
|
artists.</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Generate Button -->
|
|
|
<div class="build-playlist-actions">
|
|
|
<button id="build-playlist-generate-btn" class="build-playlist-generate-btn"
|
|
|
onclick="generateBuildPlaylist()" disabled style="opacity: 0.5;">
|
|
|
Generate Playlist (50 tracks)
|
|
|
</button>
|
|
|
<div id="build-playlist-loading" class="build-playlist-loading" style="display: none;">
|
|
|
<div class="loading-spinner"></div>
|
|
|
<p>Building your playlist...</p>
|
|
|
</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">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>
|
|
|
|
|
|
<!-- Settings Page -->
|
|
|
<div class="page" id="settings-page">
|
|
|
<div class="page-header">
|
|
|
<h2>Settings</h2>
|
|
|
<button class="save-button" onclick="document.getElementById('save-settings').click()">💾 Save
|
|
|
Settings</button>
|
|
|
</div>
|
|
|
<div class="settings-content">
|
|
|
<!-- Two Column Layout -->
|
|
|
<div class="settings-columns">
|
|
|
<!-- Left Column - API Configuration -->
|
|
|
<div class="settings-left-column">
|
|
|
<!-- API Configuration -->
|
|
|
<div class="settings-group">
|
|
|
<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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Tidal Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title tidal-title">Tidal</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>
|
|
|
|
|
|
<!-- Soulseek Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title soulseek-title">Soulseek</h4>
|
|
|
<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 style="color: #888; display: block; margin-top: 5px;">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 style="color: #888; display: block; margin-top: 5px;">Extra time to wait
|
|
|
for late results (5-60 seconds)</small>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- ListenBrainz Settings -->
|
|
|
<div class="api-service-frame">
|
|
|
<h4 class="service-title listenbrainz-title">ListenBrainz</h4>
|
|
|
<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>
|
|
|
</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>
|
|
|
</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('soulseek')">Test
|
|
|
Soulseek</button>
|
|
|
<button class="test-button" onclick="testConnection('listenbrainz')">Test
|
|
|
ListenBrainz</button>
|
|
|
<button class="test-button" onclick="testConnection('acoustid')">Test
|
|
|
AcoustID</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Server Connections -->
|
|
|
<div class="settings-group">
|
|
|
<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" 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-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">
|
|
|
<h3>Download Settings</h3>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label>Slskd Download Dir:</label>
|
|
|
<div class="path-input-group">
|
|
|
<input type="text" id="download-path" placeholder="./downloads">
|
|
|
<button class="browse-button" onclick="browsePath('download')">Browse</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">
|
|
|
<button class="browse-button" onclick="browsePath('transfer')">Browse</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">
|
|
|
<button class="browse-button" onclick="browsePath('staging')">Browse</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="hybrid">Hybrid (Try both with 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>
|
|
|
|
|
|
<!-- Hybrid Mode Settings (shown only when hybrid is selected) -->
|
|
|
<div id="hybrid-settings-container" style="display: none;">
|
|
|
<div class="form-group">
|
|
|
<label>Primary Source (Hybrid Mode):</label>
|
|
|
<select id="hybrid-primary-source" class="form-select">
|
|
|
<option value="soulseek">Try Soulseek First</option>
|
|
|
<option value="youtube">Try YouTube First</option>
|
|
|
</select>
|
|
|
<div class="setting-help-text">
|
|
|
Which source to try first when hybrid mode is enabled.
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label>YouTube Min Confidence:</label>
|
|
|
<input type="number" id="youtube-min-confidence" min="0.5" max="1.0" step="0.05"
|
|
|
value="0.65" placeholder="0.65">
|
|
|
<div class="setting-help-text">
|
|
|
Minimum match confidence for YouTube downloads (0.5-1.0). Higher = stricter
|
|
|
matching.
|
|
|
</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>
|
|
|
|
|
|
<!-- Discovery Settings -->
|
|
|
<div class="settings-group">
|
|
|
<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>
|
|
|
|
|
|
<!-- Quality Profile Settings -->
|
|
|
<div class="settings-group">
|
|
|
<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>
|
|
|
|
|
|
<!-- MP3 320 Quality -->
|
|
|
<div class="quality-tier">
|
|
|
<div class="quality-tier-header">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="quality-mp3_320-enabled" checked
|
|
|
onchange="toggleQuality('mp3_320')">
|
|
|
<span class="quality-tier-name">MP3 320 kbps</span>
|
|
|
</label>
|
|
|
<span class="quality-tier-priority" id="priority-mp3_320">Priority: 2</span>
|
|
|
</div>
|
|
|
<div class="quality-tier-sliders" id="sliders-mp3_320">
|
|
|
<div class="slider-group">
|
|
|
<label>Bitrate Range:</label>
|
|
|
<div class="dual-slider-container">
|
|
|
<input type="range" class="range-slider range-slider-min"
|
|
|
id="mp3_320-min" min="0" max="500" value="280" step="10"
|
|
|
oninput="updateQualityRange('mp3_320')">
|
|
|
<input type="range" class="range-slider range-slider-max"
|
|
|
id="mp3_320-max" min="0" max="500" value="500" step="10"
|
|
|
oninput="updateQualityRange('mp3_320')">
|
|
|
<div class="range-slider-track"></div>
|
|
|
</div>
|
|
|
<div class="slider-values">
|
|
|
<span id="mp3_320-min-value">280 kbps</span>
|
|
|
<span>-</span>
|
|
|
<span id="mp3_320-max-value">500 kbps</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- MP3 256 Quality -->
|
|
|
<div class="quality-tier">
|
|
|
<div class="quality-tier-header">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="quality-mp3_256-enabled" checked
|
|
|
onchange="toggleQuality('mp3_256')">
|
|
|
<span class="quality-tier-name">MP3 256 kbps</span>
|
|
|
</label>
|
|
|
<span class="quality-tier-priority" id="priority-mp3_256">Priority: 3</span>
|
|
|
</div>
|
|
|
<div class="quality-tier-sliders" id="sliders-mp3_256">
|
|
|
<div class="slider-group">
|
|
|
<label>Bitrate Range:</label>
|
|
|
<div class="dual-slider-container">
|
|
|
<input type="range" class="range-slider range-slider-min"
|
|
|
id="mp3_256-min" min="0" max="400" value="200" step="10"
|
|
|
oninput="updateQualityRange('mp3_256')">
|
|
|
<input type="range" class="range-slider range-slider-max"
|
|
|
id="mp3_256-max" min="0" max="400" value="400" step="10"
|
|
|
oninput="updateQualityRange('mp3_256')">
|
|
|
<div class="range-slider-track"></div>
|
|
|
</div>
|
|
|
<div class="slider-values">
|
|
|
<span id="mp3_256-min-value">200 kbps</span>
|
|
|
<span>-</span>
|
|
|
<span id="mp3_256-max-value">400 kbps</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- MP3 192 Quality -->
|
|
|
<div class="quality-tier">
|
|
|
<div class="quality-tier-header">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="quality-mp3_192-enabled"
|
|
|
onchange="toggleQuality('mp3_192')">
|
|
|
<span class="quality-tier-name">MP3 192 kbps</span>
|
|
|
</label>
|
|
|
<span class="quality-tier-priority" id="priority-mp3_192">Priority: 4</span>
|
|
|
</div>
|
|
|
<div class="quality-tier-sliders disabled" id="sliders-mp3_192">
|
|
|
<div class="slider-group">
|
|
|
<label>Bitrate Range:</label>
|
|
|
<div class="dual-slider-container">
|
|
|
<input type="range" class="range-slider range-slider-min"
|
|
|
id="mp3_192-min" min="0" max="300" value="150" step="10"
|
|
|
oninput="updateQualityRange('mp3_192')">
|
|
|
<input type="range" class="range-slider range-slider-max"
|
|
|
id="mp3_192-max" min="0" max="300" value="300" step="10"
|
|
|
oninput="updateQualityRange('mp3_192')">
|
|
|
<div class="range-slider-track"></div>
|
|
|
</div>
|
|
|
<div class="slider-values">
|
|
|
<span id="mp3_192-min-value">150 kbps</span>
|
|
|
<span>-</span>
|
|
|
<span id="mp3_192-max-value">300 kbps</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Fallback Option -->
|
|
|
<div class="form-group">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="quality-fallback-enabled" checked>
|
|
|
Allow fallback to any quality if preferred qualities unavailable
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div class="help-text">
|
|
|
<strong>How it works:</strong> Downloads try each enabled quality in priority order
|
|
|
(1 = highest).
|
|
|
MIN bitrate catches fake/transcoded files (e.g., FLAC below 500 kbps is likely a
|
|
|
re-encoded MP3). MAX bitrate limits hi-res files if you want to save space.
|
|
|
When track duration is unavailable, a generous file-size safety net is used instead.
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Third Column - Database, Metadata, Playlist Sync, Logging -->
|
|
|
<div class="settings-third-column">
|
|
|
<!-- Database Settings -->
|
|
|
<div class="settings-group">
|
|
|
<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>
|
|
|
|
|
|
<!-- Metadata Enhancement Settings -->
|
|
|
<div class="settings-group">
|
|
|
<h3>🎵 Metadata Enhancement</h3>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="metadata-enabled" checked>
|
|
|
Enable metadata enhancement with Spotify data
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label class="checkbox-label">
|
|
|
<input type="checkbox" id="embed-album-art" checked>
|
|
|
Embed high-quality album art from Spotify
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label>Supported Formats:</label>
|
|
|
<div class="supported-formats">MP3, FLAC, MP4/M4A, OGG</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- File Organization Settings -->
|
|
|
<div class="settings-group">
|
|
|
<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 style="color: #888;">Variables: $albumartist, $artist, $album, $title,
|
|
|
$track, $year</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label>Single Path Template:</label>
|
|
|
<input type="text" id="template-single-path"
|
|
|
placeholder="$artist/$artist - $title/$title">
|
|
|
<small style="color: #888;">Variables: $artist, $title, $album, $year</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label>Playlist Path Template:</label>
|
|
|
<input type="text" id="template-playlist-path"
|
|
|
placeholder="$playlist/$artist - $title">
|
|
|
<small style="color: #888;">Variables: $playlist, $artist, $title, $year</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<button class="test-button" onclick="resetFileOrganizationTemplates()"
|
|
|
style="background: #666;">
|
|
|
🔄 Reset to Defaults
|
|
|
</button>
|
|
|
<small style="color: #888; display: block; margin-top: 8px;">
|
|
|
Restores original path structure. Your downloads will be organized like before.
|
|
|
</small>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Playlist Sync Settings -->
|
|
|
<div class="settings-group">
|
|
|
<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">
|
|
|
<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>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Save Button -->
|
|
|
<div class="settings-actions">
|
|
|
<button class="save-button" id="save-settings">💾 Save Settings</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
|
|
<!-- 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 1.2 - Enhanced Search & Matching Engine Overhaul</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>
|
|
|
|
|
|
<!-- Import Modal -->
|
|
|
<div class="modal-overlay hidden" id="import-modal-overlay" onclick="if(event.target===this)closeImportModal()">
|
|
|
<div class="import-modal" id="import-modal">
|
|
|
<div class="import-modal-header">
|
|
|
<h2 class="import-modal-title">Import Music</h2>
|
|
|
<div class="import-tab-bar">
|
|
|
<button class="import-tab-btn active" id="import-tab-album-btn"
|
|
|
onclick="switchImportTab('album')">Album</button>
|
|
|
<button class="import-tab-btn" id="import-tab-singles-btn"
|
|
|
onclick="switchImportTab('singles')">Singles</button>
|
|
|
</div>
|
|
|
<span class="import-modal-close" onclick="closeImportModal()">×</span>
|
|
|
</div>
|
|
|
<div class="import-modal-body">
|
|
|
<!-- Album Tab -->
|
|
|
<div class="import-tab-content active" id="import-tab-album">
|
|
|
<!-- Search state -->
|
|
|
<div id="import-album-search-section">
|
|
|
<div class="import-suggestions-section" id="import-suggestions-section">
|
|
|
<div class="import-suggestions-label">Suggested from your staging folder</div>
|
|
|
<div class="import-album-grid" id="import-suggestions-grid"></div>
|
|
|
</div>
|
|
|
<div class="import-search-bar">
|
|
|
<input type="text" id="import-album-search-input" placeholder="Search for an album..."
|
|
|
onkeydown="if(event.key==='Enter')searchImportAlbum()">
|
|
|
<button class="import-search-btn" onclick="searchImportAlbum()">Search</button>
|
|
|
<button class="import-clear-btn hidden" id="import-album-clear-btn"
|
|
|
onclick="clearImportAlbumSearch()" title="Clear search">✕</button>
|
|
|
</div>
|
|
|
<div class="import-album-grid" id="import-album-results"></div>
|
|
|
</div>
|
|
|
<!-- Match state (hidden initially) -->
|
|
|
<div id="import-album-match-section" class="hidden">
|
|
|
<div class="import-album-hero" id="import-album-hero"></div>
|
|
|
<div class="import-match-header">
|
|
|
<h3>Track Matching</h3>
|
|
|
<button class="import-back-btn" onclick="resetImportAlbumSearch()">← Back to Search</button>
|
|
|
</div>
|
|
|
<div class="import-match-list" id="import-match-list"></div>
|
|
|
<div class="import-match-footer">
|
|
|
<div class="import-match-stats" id="import-match-stats"></div>
|
|
|
<button class="import-process-btn" id="import-album-process-btn"
|
|
|
onclick="processImportAlbum()">Process Album</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- Progress state -->
|
|
|
<div id="import-album-progress-section" class="hidden">
|
|
|
<div class="import-progress-container">
|
|
|
<div class="import-progress-text" id="import-album-progress-text">Processing...</div>
|
|
|
<div class="import-progress-bar" id="import-album-progress-bar">
|
|
|
<div class="import-progress-fill" id="import-album-progress-fill"></div>
|
|
|
</div>
|
|
|
<div class="import-progress-result" id="import-album-progress-result"></div>
|
|
|
<div class="import-progress-actions hidden" id="import-album-progress-actions">
|
|
|
<button class="import-secondary-btn" onclick="resetImportAlbumSearch()">Import
|
|
|
Another</button>
|
|
|
<button class="import-process-btn" onclick="closeImportModal()">Done</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- Singles Tab -->
|
|
|
<div class="import-tab-content" id="import-tab-singles">
|
|
|
<div id="import-singles-controls">
|
|
|
<div class="import-singles-header">
|
|
|
<div class="import-singles-info">
|
|
|
<span id="import-staging-path-display"></span>
|
|
|
</div>
|
|
|
<div class="import-singles-actions">
|
|
|
<button class="import-action-btn" onclick="loadStagingFiles()">Refresh</button>
|
|
|
<button class="import-action-btn" onclick="selectAllStagingFiles()">Select All</button>
|
|
|
<button class="import-process-btn" id="import-singles-process-btn"
|
|
|
onclick="processImportSingles()" disabled>Process Selected (0)</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="import-singles-list" id="import-singles-list">
|
|
|
<div class="import-singles-empty">Click "Refresh" to scan staging folder for audio files</div>
|
|
|
</div>
|
|
|
<!-- Singles Progress -->
|
|
|
<div id="import-singles-progress-section" class="hidden">
|
|
|
<div class="import-progress-container">
|
|
|
<div class="import-progress-text" id="import-singles-progress-text">Processing...</div>
|
|
|
<div class="import-progress-bar" id="import-singles-progress-bar">
|
|
|
<div class="import-progress-fill" id="import-singles-progress-fill"></div>
|
|
|
</div>
|
|
|
<div class="import-progress-result" id="import-singles-progress-result"></div>
|
|
|
<div class="import-progress-actions hidden" id="import-singles-progress-actions">
|
|
|
<button class="import-secondary-btn" onclick="resetImportSingles()">Import More</button>
|
|
|
<button class="import-process-btn" onclick="closeImportModal()">Done</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</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-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>
|
|
|
</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>
|
|
|
|
|
|
<!-- 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 -->
|
|
|
<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>
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
|
|
</body>
|
|
|
|
|
|
</html> |