discover progress
This commit is contained in:
parent
7f1b8cf3a8
commit
65cbf6ce28
5 changed files with 649 additions and 36 deletions
|
|
@ -1037,27 +1037,66 @@ class WatchlistScanner:
|
|||
albums_by_artist[artist].append(album)
|
||||
|
||||
# Get tracks from each album, grouped by artist
|
||||
# Also add these tracks to discovery pool for fast lookup
|
||||
artist_tracks = {}
|
||||
artist_track_data = {} # Store full track data for discovery pool
|
||||
|
||||
for artist, albums in albums_by_artist.items():
|
||||
artist_tracks[artist] = []
|
||||
artist_track_data[artist] = []
|
||||
|
||||
for album in albums:
|
||||
try:
|
||||
album_data = self.spotify_client.get_album(album['album_spotify_id'])
|
||||
if album_data and 'tracks' in album_data:
|
||||
for track in album_data['tracks']['items']:
|
||||
artist_tracks[artist].append(track['id'])
|
||||
track_id = track['id']
|
||||
artist_tracks[artist].append(track_id)
|
||||
|
||||
# Store full track data for discovery pool
|
||||
full_track = {
|
||||
'id': track_id,
|
||||
'name': track['name'],
|
||||
'artists': track.get('artists', []),
|
||||
'album': album_data,
|
||||
'duration_ms': track.get('duration_ms', 0)
|
||||
}
|
||||
artist_track_data[artist].append(full_track)
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Balance by artist - max 6 tracks per artist
|
||||
balanced_tracks = []
|
||||
balanced_track_data = []
|
||||
|
||||
for artist, tracks in artist_tracks.items():
|
||||
random.shuffle(tracks)
|
||||
balanced_tracks.extend(tracks[:6]) # Max 6 per artist
|
||||
# Shuffle and get indices
|
||||
indices = list(range(len(tracks)))
|
||||
random.shuffle(indices)
|
||||
selected_indices = indices[:6]
|
||||
|
||||
# Add selected tracks
|
||||
for idx in selected_indices:
|
||||
balanced_tracks.append(tracks[idx])
|
||||
balanced_track_data.append(artist_track_data[artist][idx])
|
||||
|
||||
# Shuffle and limit to 50
|
||||
random.shuffle(balanced_tracks)
|
||||
release_radar_tracks = balanced_tracks[:50]
|
||||
combined = list(zip(balanced_tracks, balanced_track_data))
|
||||
random.shuffle(combined)
|
||||
combined = combined[:50]
|
||||
|
||||
release_radar_tracks = [track_id for track_id, _ in combined]
|
||||
release_radar_track_data = [track_data for _, track_data in combined]
|
||||
|
||||
# Add Release Radar tracks to discovery pool so they're available for fast lookup
|
||||
logger.info(f"Adding {len(release_radar_track_data)} Release Radar tracks to discovery pool...")
|
||||
for track_data in release_radar_track_data:
|
||||
try:
|
||||
self.database.add_to_discovery_pool(track_data, is_new_release=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to add track {track_data['name']} to discovery pool: {e}")
|
||||
continue
|
||||
|
||||
self.database.save_curated_playlist('release_radar', release_radar_tracks)
|
||||
logger.info(f"Release Radar curated: {len(release_radar_tracks)} tracks")
|
||||
|
|
|
|||
|
|
@ -11613,7 +11613,7 @@ def get_playlist_tracks(playlist_id):
|
|||
full_playlist = spotify_client.get_playlist_by_id(playlist_id)
|
||||
if not full_playlist:
|
||||
return jsonify({})
|
||||
|
||||
|
||||
# Convert playlist to dict manually since core class doesn't have to_dict method
|
||||
playlist_dict = {
|
||||
'id': full_playlist.id,
|
||||
|
|
@ -11631,6 +11631,33 @@ def get_playlist_tracks(playlist_id):
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/spotify/album/<album_id>', methods=['GET'])
|
||||
def get_album_tracks(album_id):
|
||||
"""Fetches full track details for a specific album."""
|
||||
if not spotify_client or not spotify_client.is_authenticated():
|
||||
return jsonify({"error": "Spotify not authenticated."}), 401
|
||||
try:
|
||||
album_data = spotify_client.get_album(album_id)
|
||||
if not album_data:
|
||||
return jsonify({"error": "Album not found"}), 404
|
||||
|
||||
# Extract tracks from album data
|
||||
tracks = album_data.get('tracks', {}).get('items', [])
|
||||
|
||||
# Format response
|
||||
album_dict = {
|
||||
'id': album_data['id'],
|
||||
'name': album_data['name'],
|
||||
'artists': album_data.get('artists', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'total_tracks': album_data.get('total_tracks', 0),
|
||||
'images': album_data.get('images', []),
|
||||
'tracks': tracks
|
||||
}
|
||||
return jsonify(album_dict)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/spotify/search_tracks', methods=['GET'])
|
||||
def search_spotify_tracks():
|
||||
|
|
@ -14477,33 +14504,32 @@ def get_discover_release_radar():
|
|||
curated_track_ids = database.get_curated_playlist('release_radar')
|
||||
|
||||
if curated_track_ids:
|
||||
# Fetch track data directly from Spotify (release radar tracks may not be in discovery pool)
|
||||
# Use curated selection - fetch track data from discovery pool (same as Discovery Weekly)
|
||||
discovery_tracks = database.get_discovery_pool_tracks(limit=5000, new_releases_only=False)
|
||||
tracks_by_id = {track.spotify_track_id: track for track in discovery_tracks}
|
||||
|
||||
selected_tracks = []
|
||||
|
||||
for track_id in curated_track_ids:
|
||||
try:
|
||||
# Get track data from Spotify
|
||||
track_data = spotify_client.get_track_details(track_id)
|
||||
if track_id in tracks_by_id:
|
||||
track = tracks_by_id[track_id]
|
||||
|
||||
if track_data:
|
||||
# Get album cover from raw_data (enhanced data doesn't include images)
|
||||
album_cover = None
|
||||
if track_data.get('raw_data') and track_data['raw_data'].get('album', {}).get('images'):
|
||||
album_cover = track_data['raw_data']['album']['images'][0]['url']
|
||||
# Parse track_data_json if it's a string
|
||||
track_data = track.track_data_json
|
||||
if isinstance(track_data, str):
|
||||
try:
|
||||
track_data = json.loads(track_data)
|
||||
except:
|
||||
track_data = None
|
||||
|
||||
selected_tracks.append({
|
||||
"spotify_track_id": track_data['id'],
|
||||
"track_name": track_data['name'],
|
||||
"artist_name": track_data.get('primary_artist', 'Unknown'),
|
||||
"album_name": track_data['album']['name'] if track_data.get('album') else 'Unknown',
|
||||
"album_cover_url": album_cover,
|
||||
"duration_ms": track_data.get('duration_ms', 0),
|
||||
"track_data_json": track_data.get('raw_data', track_data)
|
||||
})
|
||||
except Exception as track_error:
|
||||
# Skip tracks that fail to fetch
|
||||
print(f"Error fetching track {track_id}: {track_error}")
|
||||
continue
|
||||
selected_tracks.append({
|
||||
"spotify_track_id": track.spotify_track_id,
|
||||
"track_name": track.track_name,
|
||||
"artist_name": track.artist_name,
|
||||
"album_name": track.album_name,
|
||||
"album_cover_url": track.album_cover_url,
|
||||
"duration_ms": track.duration_ms,
|
||||
"track_data_json": track_data # Now properly parsed
|
||||
})
|
||||
|
||||
return jsonify({"success": True, "tracks": selected_tracks})
|
||||
|
||||
|
|
@ -14534,6 +14560,15 @@ def get_discover_weekly():
|
|||
for track_id in curated_track_ids:
|
||||
if track_id in tracks_by_id:
|
||||
track = tracks_by_id[track_id]
|
||||
|
||||
# Parse track_data_json if it's a string
|
||||
track_data = track.track_data_json
|
||||
if isinstance(track_data, str):
|
||||
try:
|
||||
track_data = json.loads(track_data)
|
||||
except:
|
||||
track_data = None
|
||||
|
||||
selected_tracks.append({
|
||||
"spotify_track_id": track.spotify_track_id,
|
||||
"track_name": track.track_name,
|
||||
|
|
@ -14541,7 +14576,7 @@ def get_discover_weekly():
|
|||
"album_name": track.album_name,
|
||||
"album_cover_url": track.album_cover_url,
|
||||
"duration_ms": track.duration_ms,
|
||||
"track_data_json": track.track_data_json
|
||||
"track_data_json": track_data # Now properly parsed
|
||||
})
|
||||
|
||||
return jsonify({"success": True, "tracks": selected_tracks})
|
||||
|
|
|
|||
|
|
@ -1668,8 +1668,37 @@
|
|||
<!-- Release Radar Section -->
|
||||
<div class="discover-section">
|
||||
<div class="discover-section-header">
|
||||
<h2 class="discover-section-title">Release Radar</h2>
|
||||
<p class="discover-section-subtitle">Fresh tracks from new releases</p>
|
||||
<div>
|
||||
<h2 class="discover-section-title">Release Radar</h2>
|
||||
<p class="discover-section-subtitle">Fresh tracks from new releases</p>
|
||||
</div>
|
||||
<div class="discover-section-actions">
|
||||
<button class="action-button secondary" onclick="openDownloadModalForDiscoverPlaylist('release_radar', 'Release Radar')" 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', 'Release Radar')" 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 -->
|
||||
|
|
@ -1683,8 +1712,37 @@
|
|||
<!-- Discovery Weekly Section -->
|
||||
<div class="discover-section">
|
||||
<div class="discover-section-header">
|
||||
<h2 class="discover-section-title">Discovery Weekly</h2>
|
||||
<p class="discover-section-subtitle">A personalized playlist just for you</p>
|
||||
<div>
|
||||
<h2 class="discover-section-title">Discovery Weekly</h2>
|
||||
<p class="discover-section-subtitle">A personalized playlist just for you</p>
|
||||
</div>
|
||||
<div class="discover-section-actions">
|
||||
<button class="action-button secondary" onclick="openDownloadModalForDiscoverPlaylist('discovery_weekly', 'Discovery Weekly')" 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', 'Discovery Weekly')" 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 -->
|
||||
|
|
|
|||
|
|
@ -24213,6 +24213,11 @@ let discoverHeroIndex = 0;
|
|||
let discoverHeroArtists = [];
|
||||
let discoverHeroInterval = null;
|
||||
|
||||
// Store discover playlist tracks for download/sync functionality
|
||||
let discoverReleaseRadarTracks = [];
|
||||
let discoverWeeklyTracks = [];
|
||||
let discoverRecentAlbums = [];
|
||||
|
||||
async function loadDiscoverPage() {
|
||||
console.log('Loading discover page...');
|
||||
|
||||
|
|
@ -24224,6 +24229,71 @@ async function loadDiscoverPage() {
|
|||
loadDiscoverWeekly(),
|
||||
loadMoreForYou()
|
||||
]);
|
||||
|
||||
// Check for active syncs after page load
|
||||
checkForActiveDiscoverSyncs();
|
||||
}
|
||||
|
||||
async function checkForActiveDiscoverSyncs() {
|
||||
// Check if Release Radar sync is active
|
||||
try {
|
||||
const releaseRadarResponse = await fetch('/api/sync/status/discover_release_radar');
|
||||
if (releaseRadarResponse.ok) {
|
||||
const data = await releaseRadarResponse.json();
|
||||
if (data.status === 'syncing' || data.status === 'starting') {
|
||||
console.log('🔄 Resuming Release Radar sync polling after page refresh');
|
||||
|
||||
// Show status display
|
||||
const statusDisplay = document.getElementById('release-radar-sync-status');
|
||||
if (statusDisplay) {
|
||||
statusDisplay.style.display = 'block';
|
||||
}
|
||||
|
||||
// Disable button
|
||||
const syncButton = document.getElementById('release-radar-sync-btn');
|
||||
if (syncButton) {
|
||||
syncButton.disabled = true;
|
||||
syncButton.style.opacity = '0.5';
|
||||
syncButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
|
||||
// Resume polling
|
||||
startDiscoverSyncPolling('release_radar', 'discover_release_radar');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Sync not active, ignore
|
||||
}
|
||||
|
||||
// Check if Discovery Weekly sync is active
|
||||
try {
|
||||
const discoveryWeeklyResponse = await fetch('/api/sync/status/discover_discovery_weekly');
|
||||
if (discoveryWeeklyResponse.ok) {
|
||||
const data = await discoveryWeeklyResponse.json();
|
||||
if (data.status === 'syncing' || data.status === 'starting') {
|
||||
console.log('🔄 Resuming Discovery Weekly sync polling after page refresh');
|
||||
|
||||
// Show status display
|
||||
const statusDisplay = document.getElementById('discovery-weekly-sync-status');
|
||||
if (statusDisplay) {
|
||||
statusDisplay.style.display = 'block';
|
||||
}
|
||||
|
||||
// Disable button
|
||||
const syncButton = document.getElementById('discovery-weekly-sync-btn');
|
||||
if (syncButton) {
|
||||
syncButton.disabled = true;
|
||||
syncButton.style.opacity = '0.5';
|
||||
syncButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
|
||||
// Resume polling
|
||||
startDiscoverSyncPolling('discovery_weekly', 'discover_discovery_weekly');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Sync not active, ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiscoverHero() {
|
||||
|
|
@ -24320,12 +24390,15 @@ async function loadDiscoverRecentReleases() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Store albums for download functionality
|
||||
discoverRecentAlbums = data.albums;
|
||||
|
||||
// Build carousel HTML
|
||||
let html = '';
|
||||
data.albums.forEach(album => {
|
||||
data.albums.forEach((album, index) => {
|
||||
const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
|
||||
html += `
|
||||
<div class="discover-card">
|
||||
<div class="discover-card" onclick="openDownloadModalForRecentAlbum(${index})" style="cursor: pointer;">
|
||||
<div class="discover-card-image">
|
||||
<img src="${coverUrl}" alt="${album.album_name}">
|
||||
</div>
|
||||
|
|
@ -24367,6 +24440,9 @@ async function loadDiscoverReleaseRadar() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Store tracks for download/sync functionality
|
||||
discoverReleaseRadarTracks = data.tracks;
|
||||
|
||||
// Build compact playlist HTML
|
||||
let html = '<div class="discover-playlist-tracks-compact">';
|
||||
data.tracks.forEach((track, index) => {
|
||||
|
|
@ -24421,6 +24497,9 @@ async function loadDiscoverWeekly() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Store tracks for download/sync functionality
|
||||
discoverWeeklyTracks = data.tracks;
|
||||
|
||||
// Build compact playlist HTML
|
||||
let html = '<div class="discover-playlist-tracks-compact">';
|
||||
data.tracks.forEach((track, index) => {
|
||||
|
|
@ -24563,3 +24642,286 @@ async function loadMoreForYou() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// DISCOVER PLAYLIST ACTIONS
|
||||
// ===============================
|
||||
|
||||
async function openDownloadModalForDiscoverPlaylist(playlistType, playlistName) {
|
||||
console.log(`📥 Opening Download Missing Tracks modal for ${playlistName}`);
|
||||
|
||||
try {
|
||||
// Get tracks based on playlist type
|
||||
let tracks = [];
|
||||
if (playlistType === 'release_radar') {
|
||||
tracks = discoverReleaseRadarTracks;
|
||||
} else if (playlistType === 'discovery_weekly') {
|
||||
tracks = discoverWeeklyTracks;
|
||||
}
|
||||
|
||||
if (!tracks || tracks.length === 0) {
|
||||
showToast(`No tracks available for ${playlistName}`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert discover tracks to format expected by download modal
|
||||
const spotifyTracks = tracks.map(track => {
|
||||
let spotifyTrack;
|
||||
|
||||
// Use track_data_json if available, otherwise construct from track data
|
||||
if (track.track_data_json) {
|
||||
spotifyTrack = track.track_data_json;
|
||||
} else {
|
||||
// Fallback: construct track object from available data
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize artists to array of strings for modal compatibility
|
||||
if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
|
||||
spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
|
||||
}
|
||||
|
||||
return spotifyTrack;
|
||||
});
|
||||
|
||||
// Create virtual playlist ID
|
||||
const virtualPlaylistId = `discover_${playlistType}`;
|
||||
|
||||
// Use existing modal system (same as YouTube/Tidal playlists)
|
||||
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error opening download modal for discover playlist:', error);
|
||||
showToast(`Failed to open download modal: ${error.message}`, 'error');
|
||||
hideLoadingOverlay(); // Ensure overlay is hidden on error
|
||||
}
|
||||
}
|
||||
|
||||
async function startDiscoverPlaylistSync(playlistType, playlistName) {
|
||||
console.log(`🔄 Starting sync for ${playlistName}`);
|
||||
|
||||
// Get tracks based on playlist type
|
||||
let tracks = [];
|
||||
if (playlistType === 'release_radar') {
|
||||
tracks = discoverReleaseRadarTracks;
|
||||
} else if (playlistType === 'discovery_weekly') {
|
||||
tracks = discoverWeeklyTracks;
|
||||
}
|
||||
|
||||
if (!tracks || tracks.length === 0) {
|
||||
showToast(`No tracks available for ${playlistName}`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to format expected by sync API
|
||||
const spotifyTracks = tracks.map(track => {
|
||||
let spotifyTrack;
|
||||
|
||||
// Use track_data_json if available
|
||||
if (track.track_data_json) {
|
||||
spotifyTrack = track.track_data_json;
|
||||
} else {
|
||||
// Fallback: construct track object
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize artists to array of strings for sync compatibility
|
||||
if (spotifyTrack.artists && Array.isArray(spotifyTrack.artists)) {
|
||||
spotifyTrack.artists = spotifyTrack.artists.map(a => a.name || a);
|
||||
}
|
||||
|
||||
return spotifyTrack;
|
||||
});
|
||||
|
||||
// Create virtual playlist ID
|
||||
const virtualPlaylistId = `discover_${playlistType}`;
|
||||
|
||||
// Store in cache for sync function
|
||||
playlistTrackCache[virtualPlaylistId] = spotifyTracks;
|
||||
|
||||
// Create virtual playlist object
|
||||
const virtualPlaylist = {
|
||||
id: virtualPlaylistId,
|
||||
name: playlistName,
|
||||
track_count: spotifyTracks.length
|
||||
};
|
||||
|
||||
// Add to spotify playlists array if not already there
|
||||
if (!spotifyPlaylists.find(p => p.id === virtualPlaylistId)) {
|
||||
spotifyPlaylists.push(virtualPlaylist);
|
||||
}
|
||||
|
||||
// Show sync status display
|
||||
const statusId = playlistType === 'release_radar' ? 'release-radar-sync-status' : 'discovery-weekly-sync-status';
|
||||
const statusDisplay = document.getElementById(statusId);
|
||||
if (statusDisplay) {
|
||||
statusDisplay.style.display = 'block';
|
||||
}
|
||||
|
||||
// Disable sync button to prevent duplicate syncs
|
||||
const buttonId = playlistType === 'release_radar' ? 'release-radar-sync-btn' : 'discovery-weekly-sync-btn';
|
||||
const syncButton = document.getElementById(buttonId);
|
||||
if (syncButton) {
|
||||
syncButton.disabled = true;
|
||||
syncButton.style.opacity = '0.5';
|
||||
syncButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
|
||||
// Start sync using existing function
|
||||
await startPlaylistSync(virtualPlaylistId);
|
||||
|
||||
// Start polling for progress updates
|
||||
startDiscoverSyncPolling(playlistType, virtualPlaylistId);
|
||||
}
|
||||
|
||||
// Track active discover sync pollers
|
||||
const discoverSyncPollers = {};
|
||||
|
||||
function startDiscoverSyncPolling(playlistType, virtualPlaylistId) {
|
||||
// Stop any existing poller for this playlist type
|
||||
if (discoverSyncPollers[playlistType]) {
|
||||
clearInterval(discoverSyncPollers[playlistType]);
|
||||
}
|
||||
|
||||
console.log(`🔄 Starting sync polling for ${playlistType} (${virtualPlaylistId})`);
|
||||
|
||||
// Poll every 500ms for progress updates
|
||||
discoverSyncPollers[playlistType] = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/sync/status/${virtualPlaylistId}`);
|
||||
if (!response.ok) {
|
||||
console.log(`⚠️ Sync status response not OK: ${response.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`📊 Sync status for ${playlistType}:`, data);
|
||||
|
||||
// Update UI with progress (data structure: {status: ..., progress: {...}})
|
||||
const prefix = playlistType === 'release_radar' ? 'release-radar' : 'discovery-weekly';
|
||||
const progress = data.progress || {};
|
||||
|
||||
const totalEl = document.getElementById(`${prefix}-sync-total`);
|
||||
const matchedEl = document.getElementById(`${prefix}-sync-matched`);
|
||||
const failedEl = document.getElementById(`${prefix}-sync-failed`);
|
||||
const percentageEl = document.getElementById(`${prefix}-sync-percentage`);
|
||||
|
||||
const total = progress.total_tracks || 0;
|
||||
const matched = progress.matched_tracks || 0;
|
||||
const failed = progress.failed_tracks || 0;
|
||||
const processed = matched + failed;
|
||||
const completionPercentage = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
|
||||
if (totalEl) totalEl.textContent = total;
|
||||
if (matchedEl) matchedEl.textContent = matched;
|
||||
if (failedEl) failedEl.textContent = failed;
|
||||
if (percentageEl) percentageEl.textContent = completionPercentage;
|
||||
|
||||
// If complete, stop polling and hide status after delay
|
||||
if (data.status === 'finished') {
|
||||
console.log(`✅ Sync complete for ${playlistType}`);
|
||||
clearInterval(discoverSyncPollers[playlistType]);
|
||||
delete discoverSyncPollers[playlistType];
|
||||
|
||||
// Re-enable sync button
|
||||
const buttonId = playlistType === 'release_radar' ? 'release-radar-sync-btn' : 'discovery-weekly-sync-btn';
|
||||
const syncButton = document.getElementById(buttonId);
|
||||
if (syncButton) {
|
||||
syncButton.disabled = false;
|
||||
syncButton.style.opacity = '1';
|
||||
syncButton.style.cursor = 'pointer';
|
||||
}
|
||||
|
||||
// Show completion toast
|
||||
showToast(`${playlistType === 'release_radar' ? 'Release Radar' : 'Discovery Weekly'} sync complete!`, 'success');
|
||||
|
||||
// Hide status display after 3 seconds
|
||||
setTimeout(() => {
|
||||
const statusDisplay = document.getElementById(`${prefix}-sync-status`);
|
||||
if (statusDisplay) {
|
||||
statusDisplay.style.display = 'none';
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error polling sync status for ${playlistType}:`, error);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function openDownloadModalForRecentAlbum(albumIndex) {
|
||||
const album = discoverRecentAlbums[albumIndex];
|
||||
if (!album) {
|
||||
showToast('Album data not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📥 Opening Download Missing Tracks modal for album: ${album.album_name}`);
|
||||
showLoadingOverlay(`Loading tracks for ${album.album_name}...`);
|
||||
|
||||
try {
|
||||
// Fetch album tracks from Spotify API via backend
|
||||
const response = await fetch(`/api/spotify/album/${album.album_spotify_id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch album tracks');
|
||||
}
|
||||
|
||||
const albumData = await response.json();
|
||||
if (!albumData.tracks || albumData.tracks.length === 0) {
|
||||
throw new Error('No tracks found in album');
|
||||
}
|
||||
|
||||
// Convert to expected format
|
||||
const spotifyTracks = albumData.tracks.map(track => {
|
||||
// Normalize artists to array of strings
|
||||
let artists = track.artists || [{ name: album.artist_name }];
|
||||
if (Array.isArray(artists)) {
|
||||
artists = artists.map(a => a.name || a);
|
||||
}
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: artists,
|
||||
album: {
|
||||
name: album.album_name,
|
||||
images: album.album_cover_url ? [{ url: album.album_cover_url }] : []
|
||||
},
|
||||
duration_ms: track.duration_ms || 0
|
||||
};
|
||||
});
|
||||
|
||||
// Create virtual playlist ID
|
||||
const virtualPlaylistId = `discover_album_${album.album_spotify_id}`;
|
||||
const playlistName = `${album.album_name} - ${album.artist_name}`;
|
||||
|
||||
// Open download modal
|
||||
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
|
||||
|
||||
hideLoadingOverlay();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error opening album download modal:', error);
|
||||
showToast(`Failed to load album: ${error.message}`, 'error');
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16256,6 +16256,10 @@ body {
|
|||
|
||||
.discover-section-header {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.discover-section-title {
|
||||
|
|
@ -16271,6 +16275,121 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.discover-section-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.discover-section-actions .action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.discover-section-actions .action-button .button-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.discover-section-actions .action-button.primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.discover-section-actions .action-button.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.discover-section-actions .action-button.secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.discover-section-actions .action-button.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Sync Status Display */
|
||||
.discover-sync-status {
|
||||
margin: 16px 20px 20px 20px;
|
||||
padding: 16px 20px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 8px;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sync-status-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sync-status-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sync-icon {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
animation: spin 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sync-status-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.sync-stat {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sync-separator {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Carousel Styles */
|
||||
.discover-carousel {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue