download bubbles for discoverp age
This commit is contained in:
parent
52c27ce2a9
commit
12c37fa61c
5 changed files with 1193 additions and 7 deletions
|
|
@ -7,8 +7,10 @@
|
|||
"Bash(grep:*)",
|
||||
"WebFetch(domain:python-plexapi.readthedocs.io)",
|
||||
"Bash(git restore:*)",
|
||||
"Bash(python3:*)"
|
||||
"Bash(python3:*)",
|
||||
"Bash(awk:*)",
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
173
web_server.py
173
web_server.py
|
|
@ -13586,6 +13586,179 @@ def test_database_access():
|
|||
"message": "Database access test failed"
|
||||
}), 500
|
||||
|
||||
# --- Discover Download Snapshot System ---
|
||||
|
||||
@app.route('/api/discover_downloads/snapshot', methods=['POST'])
|
||||
def save_discover_download_snapshot():
|
||||
"""
|
||||
Saves a snapshot of current discover download state for persistence across page refreshes.
|
||||
"""
|
||||
try:
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
data = request.json
|
||||
if not data or 'downloads' not in data:
|
||||
return jsonify({'success': False, 'error': 'No download data provided'}), 400
|
||||
|
||||
downloads = data['downloads']
|
||||
|
||||
# Create snapshot with timestamp
|
||||
snapshot = {
|
||||
'downloads': downloads,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'snapshot_id': datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
}
|
||||
|
||||
# Save to file
|
||||
snapshot_file = os.path.join(os.path.dirname(__file__), 'discover_download_snapshots.json')
|
||||
with open(snapshot_file, 'w') as f:
|
||||
json.dump(snapshot, f, indent=2)
|
||||
|
||||
download_count = len(downloads)
|
||||
print(f"📸 Saved discover download snapshot: {download_count} downloads")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Snapshot saved with {download_count} downloads',
|
||||
'timestamp': snapshot['timestamp']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error saving discover download snapshot: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/discover_downloads/hydrate', methods=['GET'])
|
||||
def hydrate_discover_downloads():
|
||||
"""
|
||||
Loads discover downloads with live status by cross-referencing snapshots with active processes.
|
||||
"""
|
||||
try:
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
snapshot_file = os.path.join(os.path.dirname(__file__), 'discover_download_snapshots.json')
|
||||
|
||||
# Load snapshot if it exists
|
||||
if not os.path.exists(snapshot_file):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'downloads': {},
|
||||
'message': 'No snapshots found'
|
||||
})
|
||||
|
||||
with open(snapshot_file, 'r') as f:
|
||||
snapshot_data = json.load(f)
|
||||
|
||||
saved_downloads = snapshot_data.get('downloads', {})
|
||||
snapshot_time = snapshot_data.get('timestamp', '')
|
||||
|
||||
# Clean up old snapshots (older than 48 hours)
|
||||
try:
|
||||
if snapshot_time:
|
||||
snapshot_dt = datetime.fromisoformat(snapshot_time.replace('Z', '+00:00'))
|
||||
cutoff = datetime.now() - timedelta(hours=48)
|
||||
if snapshot_dt < cutoff:
|
||||
print(f"🧹 Cleaning up old discover download snapshot from {snapshot_time}")
|
||||
os.remove(snapshot_file)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'downloads': {},
|
||||
'message': 'Old snapshot cleaned up'
|
||||
})
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"⚠️ Error checking discover snapshot age: {e}")
|
||||
|
||||
# Get current active download processes for live status
|
||||
current_processes = {}
|
||||
try:
|
||||
with tasks_lock:
|
||||
for batch_id, batch_data in download_batches.items():
|
||||
if batch_data.get('phase') not in ['complete', 'error', 'cancelled']:
|
||||
playlist_id = batch_data.get('playlist_id')
|
||||
if playlist_id:
|
||||
current_processes[playlist_id] = {
|
||||
'status': 'in_progress' if batch_data.get('phase') == 'downloading' else 'analyzing',
|
||||
'batch_id': batch_id,
|
||||
'phase': batch_data.get('phase')
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error fetching active processes for discover download hydration: {e}")
|
||||
|
||||
# If no active processes exist, the app likely restarted - clean up snapshots
|
||||
if not current_processes:
|
||||
print(f"🧹 No active processes found - app likely restarted, cleaning up discover download snapshot")
|
||||
try:
|
||||
os.remove(snapshot_file)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'downloads': {},
|
||||
'message': 'Snapshot cleaned up after app restart'
|
||||
})
|
||||
except OSError as e:
|
||||
print(f"⚠️ Error removing discover snapshot file: {e}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'downloads': {},
|
||||
'message': 'No active processes - returning empty downloads'
|
||||
})
|
||||
|
||||
# Update download statuses with live data
|
||||
hydrated_downloads = {}
|
||||
for playlist_id, download_data in saved_downloads.items():
|
||||
# Determine current live status
|
||||
if playlist_id in current_processes:
|
||||
process_info = current_processes[playlist_id]
|
||||
live_status = 'in_progress'
|
||||
print(f"🔄 Found active process for discover download {playlist_id}: {process_info['phase']}")
|
||||
else:
|
||||
# No active process - likely completed
|
||||
live_status = 'completed'
|
||||
print(f"✅ No active process for discover download {playlist_id} - marking as completed")
|
||||
|
||||
# Create updated download entry
|
||||
hydrated_downloads[playlist_id] = {
|
||||
'name': download_data.get('name'),
|
||||
'type': download_data.get('type'),
|
||||
'status': live_status,
|
||||
'virtualPlaylistId': playlist_id,
|
||||
'imageUrl': download_data.get('imageUrl'),
|
||||
'startTime': download_data.get('startTime', datetime.now().isoformat())
|
||||
}
|
||||
|
||||
download_count = len(hydrated_downloads)
|
||||
active_count = sum(1 for d in hydrated_downloads.values() if d['status'] == 'in_progress')
|
||||
completed_count = sum(1 for d in hydrated_downloads.values() if d['status'] == 'completed')
|
||||
|
||||
print(f"✅ Hydrated {download_count} discover downloads: {active_count} active, {completed_count} completed")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'downloads': hydrated_downloads,
|
||||
'stats': {
|
||||
'total_downloads': download_count,
|
||||
'active_downloads': active_count,
|
||||
'completed_downloads': completed_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error hydrating discover downloads: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
# --- Artist Bubble Snapshot System ---
|
||||
|
||||
@app.route('/api/artist_bubbles/snapshot', methods=['POST'])
|
||||
|
|
|
|||
|
|
@ -2814,6 +2814,18 @@
|
|||
</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>
|
||||
|
|
@ -365,7 +365,7 @@ function initializeWatchlist() {
|
|||
|
||||
function navigateToPage(pageId) {
|
||||
if (pageId === currentPage) return;
|
||||
|
||||
|
||||
// Update navigation buttons (only if there's a nav button for this page)
|
||||
document.querySelectorAll('.nav-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
|
|
@ -374,15 +374,33 @@ function navigateToPage(pageId) {
|
|||
if (navButton) {
|
||||
navButton.classList.add('active');
|
||||
}
|
||||
|
||||
|
||||
// Update pages
|
||||
document.querySelectorAll('.page').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${pageId}-page`).classList.add('active');
|
||||
|
||||
|
||||
currentPage = pageId;
|
||||
|
||||
|
||||
// Show/hide discover download sidebar based on page
|
||||
const downloadSidebar = document.getElementById('discover-download-sidebar');
|
||||
if (downloadSidebar) {
|
||||
if (pageId === 'discover') {
|
||||
// Show sidebar on discover page if there are active downloads
|
||||
const activeDownloads = Object.keys(discoverDownloads || {}).length;
|
||||
console.log(`📊 [NAVIGATE] Discover page - ${activeDownloads} active downloads`);
|
||||
if (activeDownloads > 0) {
|
||||
// Update the sidebar UI to render the bubbles
|
||||
console.log(`🔄 [NAVIGATE] Updating discover download bar UI`);
|
||||
updateDiscoverDownloadBar();
|
||||
}
|
||||
} else {
|
||||
// Always hide sidebar on other pages
|
||||
downloadSidebar.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Load page-specific data
|
||||
loadPageData(pageId);
|
||||
}
|
||||
|
|
@ -2190,7 +2208,10 @@ async function loadInitialData() {
|
|||
try {
|
||||
// Load artist bubble state first
|
||||
await hydrateArtistBubblesFromSnapshot();
|
||||
|
||||
|
||||
// Load discover download state
|
||||
await hydrateDiscoverDownloadsFromSnapshot();
|
||||
|
||||
// Load dashboard data by default
|
||||
await loadDashboardData();
|
||||
} catch (error) {
|
||||
|
|
@ -2359,12 +2380,95 @@ async function rehydrateDiscoverPlaylistModal(virtualPlaylistId, playlistName, b
|
|||
try {
|
||||
console.log(`💧 Rehydrating discover playlist modal: ${virtualPlaylistId} (${playlistName})`);
|
||||
|
||||
// Handle album downloads from Recent Releases
|
||||
if (virtualPlaylistId.startsWith('discover_album_')) {
|
||||
const albumId = virtualPlaylistId.replace('discover_album_', '');
|
||||
console.log(`💧 Album download - fetching album ${albumId}...`);
|
||||
|
||||
try {
|
||||
const albumResponse = await fetch(`/api/spotify/album/${albumId}`);
|
||||
if (!albumResponse.ok) {
|
||||
console.error(`❌ Failed to fetch album: ${albumResponse.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const albumData = await albumResponse.json();
|
||||
if (!albumData.tracks || albumData.tracks.length === 0) {
|
||||
console.error(`❌ No tracks in album`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert tracks to expected format
|
||||
const spotifyTracks = albumData.tracks.map(track => {
|
||||
let artists = track.artists || [];
|
||||
if (Array.isArray(artists)) {
|
||||
artists = artists.map(a => a.name || a);
|
||||
}
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: artists,
|
||||
album: {
|
||||
name: albumData.name || playlistName.split(' - ')[0],
|
||||
images: albumData.images || []
|
||||
},
|
||||
duration_ms: track.duration_ms || 0
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`✅ Retrieved ${spotifyTracks.length} tracks for album`);
|
||||
|
||||
// Create modal
|
||||
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
|
||||
|
||||
// Update process
|
||||
const process = activeDownloadProcesses[virtualPlaylistId];
|
||||
if (process) {
|
||||
process.status = 'running';
|
||||
process.batchId = batchId;
|
||||
const beginBtn = document.getElementById(`begin-analysis-btn-${virtualPlaylistId}`);
|
||||
const cancelBtn = document.getElementById(`cancel-all-btn-${virtualPlaylistId}`);
|
||||
if (beginBtn) beginBtn.style.display = 'none';
|
||||
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
||||
|
||||
// Hide modal for background rehydration
|
||||
if (process.modalElement) {
|
||||
process.modalElement.style.display = 'none';
|
||||
console.log(`🔍 Hiding rehydrated modal for background processing: ${playlistName}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Rehydrated album modal: ${playlistName}`);
|
||||
}
|
||||
return;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error fetching album:`, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine API endpoint based on playlist ID
|
||||
let apiEndpoint;
|
||||
if (virtualPlaylistId === 'discover_release_radar') {
|
||||
apiEndpoint = '/api/discover/release-radar';
|
||||
} else if (virtualPlaylistId === 'discover_discovery_weekly') {
|
||||
apiEndpoint = '/api/discover/discovery-weekly';
|
||||
} else if (virtualPlaylistId === 'discover_seasonal_playlist') {
|
||||
apiEndpoint = '/api/discover/seasonal-playlist';
|
||||
} else if (virtualPlaylistId === 'discover_popular_picks') {
|
||||
apiEndpoint = '/api/discover/popular-picks';
|
||||
} else if (virtualPlaylistId === 'discover_hidden_gems') {
|
||||
apiEndpoint = '/api/discover/hidden-gems';
|
||||
} else if (virtualPlaylistId === 'discover_discovery_shuffle') {
|
||||
apiEndpoint = '/api/discover/discovery-shuffle';
|
||||
} else if (virtualPlaylistId === 'discover_familiar_favorites') {
|
||||
apiEndpoint = '/api/discover/familiar-favorites';
|
||||
} else if (virtualPlaylistId === 'build_playlist_custom') {
|
||||
apiEndpoint = '/api/discover/build-playlist';
|
||||
} else if (virtualPlaylistId.startsWith('discover_lb_')) {
|
||||
console.log(`💧 ListenBrainz playlist - skipping (no automatic rehydration for ListenBrainz)`);
|
||||
return;
|
||||
} else {
|
||||
console.error(`❌ Unknown discover playlist type: ${virtualPlaylistId}`);
|
||||
return;
|
||||
|
|
@ -4210,6 +4314,23 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
|||
virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' :
|
||||
'YouTube';
|
||||
|
||||
// Store metadata for discover download sidebar (will be added when Begin Analysis is clicked)
|
||||
if (source === 'SoulSync' || virtualPlaylistId.startsWith('discover_lb_')) {
|
||||
// Extract image URL from first track's album cover
|
||||
let imageUrl = null;
|
||||
if (spotifyTracks && spotifyTracks.length > 0) {
|
||||
const firstTrack = spotifyTracks[0];
|
||||
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
||||
imageUrl = firstTrack.album.images[0].url;
|
||||
}
|
||||
}
|
||||
// Store in process for later use when Begin Analysis is clicked
|
||||
activeDownloadProcesses[virtualPlaylistId].discoverMetadata = {
|
||||
imageUrl: imageUrl,
|
||||
type: 'album'
|
||||
};
|
||||
}
|
||||
|
||||
const heroContext = {
|
||||
type: 'playlist',
|
||||
playlist: { name: playlistName, owner: source },
|
||||
|
|
@ -4494,6 +4615,13 @@ async function closeDownloadMissingModal(playlistId) {
|
|||
console.log(`✅ [MODAL CLOSE] Artist download cleanup completed for: ${playlistId}`);
|
||||
}
|
||||
|
||||
// Remove from discover download sidebar if this is a discover page download
|
||||
if (discoverDownloads && discoverDownloads[playlistId]) {
|
||||
console.log(`🧹 [MODAL CLOSE] Removing discover download bubble: ${playlistId}`);
|
||||
removeDiscoverDownload(playlistId);
|
||||
console.log(`✅ [MODAL CLOSE] Discover download bubble removed for: ${playlistId}`);
|
||||
}
|
||||
|
||||
// Automatic cleanup and server operations after successful downloads
|
||||
await handlePostDownloadAutomation(playlistId, process);
|
||||
|
||||
|
|
@ -4819,6 +4947,15 @@ async function startMissingTracksProcess(playlistId) {
|
|||
document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none';
|
||||
document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block';
|
||||
|
||||
// Add to discover download sidebar if this is a discover page download
|
||||
if (process.discoverMetadata) {
|
||||
const playlistName = process.playlist.name;
|
||||
const imageUrl = process.discoverMetadata.imageUrl;
|
||||
const type = process.discoverMetadata.type;
|
||||
addDiscoverDownload(playlistId, playlistName, type, imageUrl);
|
||||
console.log(`📥 [BEGIN ANALYSIS] Added discover download: ${playlistName}`);
|
||||
}
|
||||
|
||||
// Check if force download toggle is enabled
|
||||
const forceDownloadCheckbox = document.getElementById(`force-download-all-${playlistId}`);
|
||||
const forceDownloadAll = forceDownloadCheckbox ? forceDownloadCheckbox.checked : false;
|
||||
|
|
@ -26635,6 +26772,18 @@ async function startListenBrainzPlaylistSync(identifier, title, playlistId) {
|
|||
// Use the same sync function as all other discover playlists
|
||||
await startPlaylistSync(virtualPlaylistId);
|
||||
|
||||
// Extract image URL from first track for download bar bubble
|
||||
let imageUrl = null;
|
||||
if (spotifyTracks && spotifyTracks.length > 0) {
|
||||
const firstTrack = spotifyTracks[0];
|
||||
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
||||
imageUrl = firstTrack.album.images[0].url;
|
||||
}
|
||||
}
|
||||
|
||||
// Add to discover download bar
|
||||
addDiscoverDownload(virtualPlaylistId, title, 'listenbrainz', imageUrl);
|
||||
|
||||
// Start polling for progress updates (using discover playlist pattern)
|
||||
startListenBrainzSyncPolling(playlistId, virtualPlaylistId);
|
||||
|
||||
|
|
@ -27812,6 +27961,18 @@ async function startDiscoverPlaylistSync(playlistType, playlistName) {
|
|||
// Start sync using existing function
|
||||
await startPlaylistSync(virtualPlaylistId);
|
||||
|
||||
// Extract image URL from first track for download bar bubble
|
||||
let imageUrl = null;
|
||||
if (spotifyTracks && spotifyTracks.length > 0) {
|
||||
const firstTrack = spotifyTracks[0];
|
||||
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
||||
imageUrl = firstTrack.album.images[0].url;
|
||||
}
|
||||
}
|
||||
|
||||
// Add to discover download bar
|
||||
addDiscoverDownload(virtualPlaylistId, playlistName, playlistType, imageUrl);
|
||||
|
||||
// Start polling for progress updates
|
||||
startDiscoverSyncPolling(playlistType, virtualPlaylistId);
|
||||
}
|
||||
|
|
@ -27962,3 +28123,622 @@ async function openDownloadModalForRecentAlbum(albumIndex) {
|
|||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// DISCOVER DOWNLOAD BAR
|
||||
// ===============================
|
||||
|
||||
// Track discover page downloads
|
||||
let discoverDownloads = {}; // playlistId -> { name, type, status, virtualPlaylistId, startTime }
|
||||
|
||||
/**
|
||||
* Add a download to the discover download bar
|
||||
*/
|
||||
function addDiscoverDownload(playlistId, playlistName, playlistType, imageUrl = null) {
|
||||
console.log(`📥 [DOWNLOAD SIDEBAR] Adding discover download: ${playlistName} (${playlistId}) type: ${playlistType}, image: ${imageUrl}`);
|
||||
|
||||
// Check if download sidebar exists
|
||||
const downloadSidebar = document.getElementById('discover-download-sidebar');
|
||||
if (!downloadSidebar) {
|
||||
console.warn('⚠️ [DOWNLOAD SIDEBAR] Download sidebar element not found - user might not be on discover page');
|
||||
return;
|
||||
}
|
||||
|
||||
discoverDownloads[playlistId] = {
|
||||
name: playlistName,
|
||||
type: playlistType,
|
||||
status: 'in_progress',
|
||||
virtualPlaylistId: playlistId,
|
||||
imageUrl: imageUrl,
|
||||
startTime: new Date()
|
||||
};
|
||||
|
||||
console.log(`📊 [DOWNLOAD SIDEBAR] Active downloads:`, Object.keys(discoverDownloads));
|
||||
updateDiscoverDownloadBar();
|
||||
monitorDiscoverDownload(playlistId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor a discover download for completion
|
||||
*/
|
||||
function monitorDiscoverDownload(playlistId) {
|
||||
let notFoundCount = 0;
|
||||
const maxNotFoundAttempts = 5; // Give sync 10 seconds to start (5 checks * 2 seconds)
|
||||
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
// Check if download still exists
|
||||
if (!discoverDownloads[playlistId]) {
|
||||
clearInterval(checkInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check if there's an active download process (modal-based downloads)
|
||||
const activeProcess = activeDownloadProcesses[playlistId];
|
||||
if (activeProcess) {
|
||||
console.log(`📂 [DOWNLOAD BAR] Found active process for ${playlistId}, status: ${activeProcess.status}`);
|
||||
|
||||
if (activeProcess.status === 'complete') {
|
||||
console.log(`✅ [DOWNLOAD BAR] Process completed: ${discoverDownloads[playlistId].name}`);
|
||||
discoverDownloads[playlistId].status = 'completed';
|
||||
updateDiscoverDownloadBar();
|
||||
clearInterval(checkInterval);
|
||||
|
||||
// Auto-remove completed downloads after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (discoverDownloads[playlistId] && discoverDownloads[playlistId].status === 'completed') {
|
||||
removeDiscoverDownload(playlistId);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
return; // Continue monitoring
|
||||
}
|
||||
|
||||
// Check sync status API (for sync-based downloads)
|
||||
const response = await fetch(`/api/sync/status/${playlistId}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
notFoundCount = 0; // Reset counter if found
|
||||
|
||||
console.log(`🔄 [DOWNLOAD BAR] Sync status for ${playlistId}: ${data.status}`);
|
||||
|
||||
if (data.status === 'complete') {
|
||||
console.log(`✅ [DOWNLOAD BAR] Sync completed: ${discoverDownloads[playlistId].name}`);
|
||||
discoverDownloads[playlistId].status = 'completed';
|
||||
updateDiscoverDownloadBar();
|
||||
clearInterval(checkInterval);
|
||||
|
||||
// Auto-remove completed downloads after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (discoverDownloads[playlistId] && discoverDownloads[playlistId].status === 'completed') {
|
||||
removeDiscoverDownload(playlistId);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
notFoundCount++;
|
||||
console.log(`🔍 [DOWNLOAD BAR] Sync not found for ${playlistId} (attempt ${notFoundCount}/${maxNotFoundAttempts})`);
|
||||
|
||||
// Only remove after multiple attempts (give it time to start)
|
||||
if (notFoundCount >= maxNotFoundAttempts) {
|
||||
console.log(`⏹️ [DOWNLOAD BAR] Sync not found after ${maxNotFoundAttempts} attempts, removing`);
|
||||
clearInterval(checkInterval);
|
||||
removeDiscoverDownload(playlistId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [DOWNLOAD BAR] Error monitoring ${playlistId}:`, error);
|
||||
}
|
||||
}, 2000); // Check every 2 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a download from the bar
|
||||
*/
|
||||
function removeDiscoverDownload(playlistId) {
|
||||
console.log(`🗑️ Removing discover download: ${playlistId}`);
|
||||
delete discoverDownloads[playlistId];
|
||||
updateDiscoverDownloadBar();
|
||||
saveDiscoverDownloadSnapshot(); // Save state after removal
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the discover download sidebar UI
|
||||
*/
|
||||
function updateDiscoverDownloadBar() {
|
||||
const downloadSidebar = document.getElementById('discover-download-sidebar');
|
||||
const bubblesContainer = document.getElementById('discover-download-bubbles');
|
||||
const countElement = document.getElementById('discover-download-count');
|
||||
|
||||
console.log(`🔄 [DOWNLOAD SIDEBAR] Updating sidebar - found elements:`, {
|
||||
downloadSidebar: !!downloadSidebar,
|
||||
bubblesContainer: !!bubblesContainer,
|
||||
countElement: !!countElement
|
||||
});
|
||||
|
||||
if (!downloadSidebar || !bubblesContainer || !countElement) {
|
||||
console.warn('⚠️ [DOWNLOAD SIDEBAR] Missing elements, cannot update');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeDownloads = Object.keys(discoverDownloads);
|
||||
const count = activeDownloads.length;
|
||||
|
||||
console.log(`📊 [DOWNLOAD SIDEBAR] Updating with ${count} active downloads`);
|
||||
|
||||
// Update count
|
||||
countElement.textContent = count;
|
||||
|
||||
// Show/hide sidebar
|
||||
if (count === 0) {
|
||||
console.log(`👁️ [DOWNLOAD SIDEBAR] No downloads, hiding sidebar`);
|
||||
downloadSidebar.classList.add('hidden');
|
||||
return;
|
||||
} else {
|
||||
console.log(`👁️ [DOWNLOAD SIDEBAR] ${count} downloads, showing sidebar`);
|
||||
downloadSidebar.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update bubbles
|
||||
bubblesContainer.innerHTML = activeDownloads.map(playlistId => {
|
||||
const download = discoverDownloads[playlistId];
|
||||
const isCompleted = download.status === 'completed';
|
||||
const icon = isCompleted ? '✅' : '⏳';
|
||||
|
||||
// Use image if available, otherwise gradient background
|
||||
const imageUrl = download.imageUrl || '';
|
||||
const backgroundStyle = imageUrl ?
|
||||
`background-image: url('${imageUrl}');` :
|
||||
`background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);`;
|
||||
|
||||
return `
|
||||
<div class="discover-download-bubble">
|
||||
<div class="discover-download-bubble-card ${isCompleted ? 'completed' : ''}"
|
||||
onclick="openDiscoverDownloadModal('${playlistId}')"
|
||||
title="${escapeHtml(download.name)} - Click to view">
|
||||
<div class="discover-download-bubble-image" style="${backgroundStyle}"></div>
|
||||
<div class="discover-download-bubble-overlay"></div>
|
||||
<div class="discover-download-bubble-content">
|
||||
<span class="discover-download-bubble-icon">${icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="discover-download-bubble-name">${escapeHtml(download.name)}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
console.log(`📊 Updated discover download sidebar: ${count} active downloads`);
|
||||
|
||||
// Save snapshot after UI update
|
||||
saveDiscoverDownloadSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open download modal for a discover playlist
|
||||
*/
|
||||
async function openDiscoverDownloadModal(playlistId) {
|
||||
console.log(`📂 [DOWNLOAD BAR] Opening download modal for: ${playlistId}`);
|
||||
|
||||
// Check if there's an active download process with modal
|
||||
let process = activeDownloadProcesses[playlistId];
|
||||
|
||||
console.log(`📋 [DOWNLOAD BAR] Process found:`, {
|
||||
exists: !!process,
|
||||
hasModalElement: !!(process && process.modalElement),
|
||||
hasModalId: !!(process && process.modalId)
|
||||
});
|
||||
|
||||
if (process) {
|
||||
// Try modalElement first (album downloads)
|
||||
if (process.modalElement) {
|
||||
console.log(`✅ [DOWNLOAD BAR] Opening modal via modalElement`);
|
||||
process.modalElement.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
// Try modalId (sync downloads)
|
||||
if (process.modalId) {
|
||||
const modal = document.getElementById(process.modalId);
|
||||
if (modal) {
|
||||
console.log(`✅ [DOWNLOAD BAR] Opening modal via modalId: ${process.modalId}`);
|
||||
modal.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no process found, try to rehydrate from backend
|
||||
console.log(`💧 [DOWNLOAD BAR] No modal found, attempting to rehydrate from backend...`);
|
||||
const rehydrated = await rehydrateDiscoverDownloadModal(playlistId);
|
||||
|
||||
if (rehydrated) {
|
||||
console.log(`✅ [DOWNLOAD BAR] Successfully rehydrated modal, opening it...`);
|
||||
// Try again after rehydration
|
||||
process = activeDownloadProcesses[playlistId];
|
||||
if (process && process.modalElement) {
|
||||
process.modalElement.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: show toast
|
||||
const download = discoverDownloads[playlistId];
|
||||
if (download) {
|
||||
console.log(`ℹ️ [DOWNLOAD BAR] No modal found after rehydration attempt, showing toast`);
|
||||
showToast(`Download: ${download.name} - ${download.status}`, 'info');
|
||||
} else {
|
||||
console.warn(`⚠️ [DOWNLOAD BAR] No download or process found for: ${playlistId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize discover download sidebar on page load
|
||||
*/
|
||||
function initializeDiscoverDownloadBar() {
|
||||
console.log('🎵 Initializing discover download sidebar...');
|
||||
|
||||
// Start with sidebar hidden (will be shown if downloads exist after hydration)
|
||||
const downloadSidebar = document.getElementById('discover-download-sidebar');
|
||||
if (downloadSidebar) {
|
||||
downloadSidebar.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Discover Download Modal Rehydration ---
|
||||
|
||||
async function rehydrateDiscoverDownloadModal(playlistId) {
|
||||
/**
|
||||
* Rehydrates a discover download modal from backend process data.
|
||||
* Fetches tracks from backend API and recreates the modal (user-requested).
|
||||
*/
|
||||
try {
|
||||
console.log(`💧 [REHYDRATE] Attempting to rehydrate modal for: ${playlistId}`);
|
||||
|
||||
// Check if there's an active backend process for this playlist
|
||||
const batchResponse = await fetch(`/api/playlists/batch_info`);
|
||||
if (!batchResponse.ok) {
|
||||
console.log(`⚠️ [REHYDRATE] Failed to fetch batch info`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const batchData = await batchResponse.json();
|
||||
const batches = batchData.batches || [];
|
||||
|
||||
// Find the batch for this playlist
|
||||
const batch = batches.find(b => b.playlist_id === playlistId);
|
||||
|
||||
if (!batch) {
|
||||
console.log(`⚠️ [REHYDRATE] No active batch found for ${playlistId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`✅ [REHYDRATE] Found active batch for ${playlistId}:`, batch);
|
||||
|
||||
// Get the download metadata from discoverDownloads
|
||||
const downloadData = discoverDownloads[playlistId];
|
||||
if (!downloadData) {
|
||||
console.log(`⚠️ [REHYDRATE] No download metadata found for ${playlistId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle album downloads from Recent Releases
|
||||
if (playlistId.startsWith('discover_album_')) {
|
||||
const albumId = playlistId.replace('discover_album_', '');
|
||||
console.log(`💧 [REHYDRATE] Album download - fetching album ${albumId}...`);
|
||||
|
||||
try {
|
||||
const albumResponse = await fetch(`/api/spotify/album/${albumId}`);
|
||||
if (!albumResponse.ok) {
|
||||
console.error(`❌ [REHYDRATE] Failed to fetch album: ${albumResponse.status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const albumData = await albumResponse.json();
|
||||
if (!albumData.tracks || albumData.tracks.length === 0) {
|
||||
console.error(`❌ [REHYDRATE] No tracks in album`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert tracks to expected format
|
||||
const spotifyTracks = albumData.tracks.map(track => {
|
||||
let artists = track.artists || [];
|
||||
if (Array.isArray(artists)) {
|
||||
artists = artists.map(a => a.name || a);
|
||||
}
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: artists,
|
||||
album: {
|
||||
name: albumData.name || downloadData.name.split(' - ')[0],
|
||||
images: downloadData.imageUrl ? [{ url: downloadData.imageUrl }] : []
|
||||
},
|
||||
duration_ms: track.duration_ms || 0
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`✅ [REHYDRATE] Retrieved ${spotifyTracks.length} tracks for album`);
|
||||
|
||||
// Create modal
|
||||
await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);
|
||||
|
||||
// Update process
|
||||
const process = activeDownloadProcesses[playlistId];
|
||||
if (process) {
|
||||
process.status = 'running';
|
||||
process.batchId = batch.batch_id;
|
||||
const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
|
||||
const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
|
||||
if (beginBtn) beginBtn.style.display = 'none';
|
||||
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
||||
console.log(`✅ [REHYDRATE] Successfully rehydrated album modal`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [REHYDRATE] Error fetching album:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine API endpoint based on playlist ID
|
||||
let apiEndpoint;
|
||||
if (playlistId === 'discover_release_radar') {
|
||||
apiEndpoint = '/api/discover/release-radar';
|
||||
} else if (playlistId === 'discover_discovery_weekly') {
|
||||
apiEndpoint = '/api/discover/discovery-weekly';
|
||||
} else if (playlistId === 'discover_seasonal_playlist') {
|
||||
apiEndpoint = '/api/discover/seasonal-playlist';
|
||||
} else if (playlistId === 'discover_popular_picks') {
|
||||
apiEndpoint = '/api/discover/popular-picks';
|
||||
} else if (playlistId === 'discover_hidden_gems') {
|
||||
apiEndpoint = '/api/discover/hidden-gems';
|
||||
} else if (playlistId === 'discover_discovery_shuffle') {
|
||||
apiEndpoint = '/api/discover/discovery-shuffle';
|
||||
} else if (playlistId === 'discover_familiar_favorites') {
|
||||
apiEndpoint = '/api/discover/familiar-favorites';
|
||||
} else if (playlistId === 'build_playlist_custom') {
|
||||
apiEndpoint = '/api/discover/build-playlist';
|
||||
} else if (playlistId.startsWith('discover_lb_')) {
|
||||
// ListenBrainz playlist - fetch from cache
|
||||
const identifier = playlistId.replace('discover_lb_', '');
|
||||
const tracks = listenbrainzTracksCache[identifier];
|
||||
if (!tracks || tracks.length === 0) {
|
||||
console.log(`⚠️ [REHYDRATE] No ListenBrainz tracks in cache for ${identifier}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to Spotify format
|
||||
const spotifyTracks = tracks.map(track => ({
|
||||
id: null,
|
||||
name: track.track_name,
|
||||
artists: [track.artist_name],
|
||||
album: {
|
||||
name: track.album_name,
|
||||
images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
|
||||
},
|
||||
duration_ms: track.duration_ms || 0,
|
||||
mbid: track.mbid
|
||||
}));
|
||||
|
||||
// Create modal and update process
|
||||
await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);
|
||||
const process = activeDownloadProcesses[playlistId];
|
||||
if (process) {
|
||||
process.status = 'running';
|
||||
process.batchId = batch.batch_id;
|
||||
const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
|
||||
const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
|
||||
if (beginBtn) beginBtn.style.display = 'none';
|
||||
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
||||
console.log(`✅ [REHYDRATE] Successfully rehydrated ListenBrainz modal`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
console.error(`❌ [REHYDRATE] Unknown discover playlist type: ${playlistId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch tracks from API
|
||||
console.log(`📡 [REHYDRATE] Fetching tracks from ${apiEndpoint}...`);
|
||||
const response = await fetch(apiEndpoint);
|
||||
if (!response.ok) {
|
||||
console.error(`❌ [REHYDRATE] Failed to fetch tracks: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.tracks) {
|
||||
console.error(`❌ [REHYDRATE] Invalid track data:`, data);
|
||||
return false;
|
||||
}
|
||||
|
||||
const tracks = data.tracks;
|
||||
console.log(`✅ [REHYDRATE] Retrieved ${tracks.length} tracks`);
|
||||
|
||||
// Transform tracks to Spotify format
|
||||
const spotifyTracks = tracks.map(track => {
|
||||
let spotifyTrack;
|
||||
if (track.track_data_json) {
|
||||
spotifyTrack = track.track_data_json;
|
||||
} else {
|
||||
spotifyTrack = {
|
||||
id: track.spotify_track_id,
|
||||
name: track.track_name,
|
||||
artists: [{ name: track.artist_name }],
|
||||
album: {
|
||||
name: track.album_name,
|
||||
images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
|
||||
},
|
||||
duration_ms: track.duration_ms || 0
|
||||
};
|
||||
}
|
||||
if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
|
||||
spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
|
||||
}
|
||||
return spotifyTrack;
|
||||
});
|
||||
|
||||
// Create the modal
|
||||
await openDownloadMissingModalForYouTube(playlistId, downloadData.name, spotifyTracks);
|
||||
|
||||
// Update process with batch info
|
||||
const process = activeDownloadProcesses[playlistId];
|
||||
if (process) {
|
||||
process.status = 'running';
|
||||
process.batchId = batch.batch_id;
|
||||
|
||||
// Update button states
|
||||
const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
|
||||
const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
|
||||
if (beginBtn) beginBtn.style.display = 'none';
|
||||
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
||||
|
||||
// Don't hide the modal - user clicked to open it
|
||||
console.log(`✅ [REHYDRATE] Successfully rehydrated modal for ${downloadData.name}`);
|
||||
return true;
|
||||
} else {
|
||||
console.error(`❌ [REHYDRATE] Failed to find rehydrated process for ${playlistId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [REHYDRATE] Error rehydrating discover download modal:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Discover Download Snapshot System ---
|
||||
|
||||
let discoverSnapshotSaveTimeout = null; // Debounce snapshot saves
|
||||
|
||||
async function saveDiscoverDownloadSnapshot() {
|
||||
/**
|
||||
* Saves current discoverDownloads state to backend for persistence.
|
||||
* Debounced to prevent excessive backend calls.
|
||||
*/
|
||||
|
||||
// Clear any existing timeout
|
||||
if (discoverSnapshotSaveTimeout) {
|
||||
clearTimeout(discoverSnapshotSaveTimeout);
|
||||
}
|
||||
|
||||
// Debounce the actual save
|
||||
discoverSnapshotSaveTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const downloadCount = Object.keys(discoverDownloads).length;
|
||||
|
||||
// Don't save empty state
|
||||
if (downloadCount === 0) {
|
||||
console.log('📸 Skipping discover snapshot save - no downloads to save');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📸 Saving discover download snapshot: ${downloadCount} downloads`);
|
||||
|
||||
// Prepare snapshot data (clean format)
|
||||
const cleanDownloads = {};
|
||||
for (const [playlistId, downloadData] of Object.entries(discoverDownloads)) {
|
||||
cleanDownloads[playlistId] = {
|
||||
name: downloadData.name,
|
||||
type: downloadData.type,
|
||||
status: downloadData.status,
|
||||
virtualPlaylistId: downloadData.virtualPlaylistId,
|
||||
imageUrl: downloadData.imageUrl,
|
||||
startTime: downloadData.startTime instanceof Date ? downloadData.startTime.toISOString() : downloadData.startTime
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch('/api/discover_downloads/snapshot', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
downloads: cleanDownloads
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
console.log(`✅ Discover download snapshot saved: ${downloadCount} downloads`);
|
||||
} else {
|
||||
console.error('❌ Failed to save discover download snapshot:', data.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error saving discover download snapshot:', error);
|
||||
}
|
||||
}, 1000); // 1 second debounce
|
||||
}
|
||||
|
||||
async function hydrateDiscoverDownloadsFromSnapshot() {
|
||||
/**
|
||||
* Hydrates discover downloads from backend snapshot with live status.
|
||||
* Called on page load to restore download state.
|
||||
*/
|
||||
try {
|
||||
console.log('🔄 Loading discover download snapshot from backend...');
|
||||
|
||||
const response = await fetch('/api/discover_downloads/hydrate');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
console.error('❌ Failed to load discover download snapshot:', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const downloads = data.downloads || {};
|
||||
const stats = data.stats || {};
|
||||
|
||||
console.log(`🔄 Loaded discover snapshot: ${stats.total_downloads || 0} downloads, ${stats.active_downloads || 0} active, ${stats.completed_downloads || 0} completed`);
|
||||
|
||||
if (Object.keys(downloads).length === 0) {
|
||||
console.log('ℹ️ No discover downloads to hydrate');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing state
|
||||
discoverDownloads = {};
|
||||
|
||||
// Restore discoverDownloads with hydrated data
|
||||
for (const [playlistId, downloadData] of Object.entries(downloads)) {
|
||||
discoverDownloads[playlistId] = {
|
||||
name: downloadData.name,
|
||||
type: downloadData.type,
|
||||
status: downloadData.status, // Live status from backend
|
||||
virtualPlaylistId: downloadData.virtualPlaylistId,
|
||||
imageUrl: downloadData.imageUrl,
|
||||
startTime: new Date(downloadData.startTime)
|
||||
};
|
||||
|
||||
console.log(`🔄 Hydrated download: ${downloadData.name} (${downloadData.status})`);
|
||||
|
||||
// Start monitoring for any in-progress downloads
|
||||
if (downloadData.status === 'in_progress') {
|
||||
console.log(`📡 Starting monitoring for: ${downloadData.name}`);
|
||||
monitorDiscoverDownload(playlistId);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't update UI here - it will be updated when user navigates to discover page
|
||||
// This allows hydration to work even if page loads on a different tab
|
||||
|
||||
const totalDownloads = Object.keys(discoverDownloads).length;
|
||||
console.log(`✅ Successfully hydrated ${totalDownloads} discover downloads (UI will update on discover page navigation)`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error hydrating discover downloads from snapshot:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeDiscoverDownloadBar);
|
||||
} else {
|
||||
initializeDiscoverDownloadBar();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17090,6 +17090,225 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
DISCOVER DOWNLOAD BAR
|
||||
=============================== */
|
||||
|
||||
/* Fixed bottom download bar */
|
||||
/* ===============================
|
||||
DISCOVER DOWNLOAD SIDEBAR
|
||||
Right sidebar for active downloads
|
||||
=============================== */
|
||||
|
||||
.discover-download-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 140px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(270deg,
|
||||
rgba(18, 18, 18, 0.98) 0%,
|
||||
rgba(12, 12, 12, 0.99) 100%);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 20px 12px;
|
||||
z-index: 9999;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.3s ease;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Hidden state */
|
||||
.discover-download-sidebar.hidden {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Sidebar header */
|
||||
.discover-download-sidebar-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.discover-download-sidebar-icon {
|
||||
font-size: 20px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.7; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.discover-download-sidebar-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discover-download-sidebar-count {
|
||||
background: linear-gradient(135deg, #1db954 0%, #1ed760 100%);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Download bubbles container */
|
||||
.discover-download-bubbles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Individual download bubble - 100x100px circular */
|
||||
.discover-download-bubble {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.discover-download-bubble-card {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(26, 26, 26, 0.95) 0%,
|
||||
rgba(18, 18, 18, 0.98) 100%);
|
||||
border: 2px solid rgba(29, 185, 84, 0.3);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(29, 185, 84, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discover-download-bubble-card:hover {
|
||||
transform: scale(1.08);
|
||||
border-color: rgba(29, 185, 84, 0.5);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(29, 185, 84, 0.2),
|
||||
0 0 15px rgba(29, 185, 84, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.discover-download-bubble-card.completed {
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.discover-download-bubble-card.completed:hover {
|
||||
border-color: rgba(34, 197, 94, 0.6);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(34, 197, 94, 0.3),
|
||||
0 0 15px rgba(34, 197, 94, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Bubble card background image */
|
||||
.discover-download-bubble-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Bubble card overlay */
|
||||
.discover-download-bubble-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(0, 0, 0, 0.2) 0%,
|
||||
rgba(0, 0, 0, 0.5) 100%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Bubble card content (icon/status) */
|
||||
.discover-download-bubble-content {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.discover-download-bubble-icon {
|
||||
font-size: 28px;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
|
||||
/* Bubble name (below the card) */
|
||||
.discover-download-bubble-name {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Bubble status text (optional, below name) */
|
||||
.discover-download-bubble-status {
|
||||
font-size: 8px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.discover-download-sidebar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.discover-download-sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.discover-download-sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.discover-download-sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
BUILD A PLAYLIST STYLES
|
||||
=============================== */
|
||||
|
|
|
|||
Loading…
Reference in a new issue