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)
|
albums_by_artist[artist].append(album)
|
||||||
|
|
||||||
# Get tracks from each album, grouped by artist
|
# Get tracks from each album, grouped by artist
|
||||||
|
# Also add these tracks to discovery pool for fast lookup
|
||||||
artist_tracks = {}
|
artist_tracks = {}
|
||||||
|
artist_track_data = {} # Store full track data for discovery pool
|
||||||
|
|
||||||
for artist, albums in albums_by_artist.items():
|
for artist, albums in albums_by_artist.items():
|
||||||
artist_tracks[artist] = []
|
artist_tracks[artist] = []
|
||||||
|
artist_track_data[artist] = []
|
||||||
|
|
||||||
for album in albums:
|
for album in albums:
|
||||||
try:
|
try:
|
||||||
album_data = self.spotify_client.get_album(album['album_spotify_id'])
|
album_data = self.spotify_client.get_album(album['album_spotify_id'])
|
||||||
if album_data and 'tracks' in album_data:
|
if album_data and 'tracks' in album_data:
|
||||||
for track in album_data['tracks']['items']:
|
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:
|
except Exception as e:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Balance by artist - max 6 tracks per artist
|
# Balance by artist - max 6 tracks per artist
|
||||||
balanced_tracks = []
|
balanced_tracks = []
|
||||||
|
balanced_track_data = []
|
||||||
|
|
||||||
for artist, tracks in artist_tracks.items():
|
for artist, tracks in artist_tracks.items():
|
||||||
random.shuffle(tracks)
|
# Shuffle and get indices
|
||||||
balanced_tracks.extend(tracks[:6]) # Max 6 per artist
|
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
|
# Shuffle and limit to 50
|
||||||
random.shuffle(balanced_tracks)
|
combined = list(zip(balanced_tracks, balanced_track_data))
|
||||||
release_radar_tracks = balanced_tracks[:50]
|
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)
|
self.database.save_curated_playlist('release_radar', release_radar_tracks)
|
||||||
logger.info(f"Release Radar curated: {len(release_radar_tracks)} tracks")
|
logger.info(f"Release Radar curated: {len(release_radar_tracks)} tracks")
|
||||||
|
|
|
||||||
|
|
@ -11631,6 +11631,33 @@ def get_playlist_tracks(playlist_id):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
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'])
|
@app.route('/api/spotify/search_tracks', methods=['GET'])
|
||||||
def search_spotify_tracks():
|
def search_spotify_tracks():
|
||||||
|
|
@ -14477,33 +14504,32 @@ def get_discover_release_radar():
|
||||||
curated_track_ids = database.get_curated_playlist('release_radar')
|
curated_track_ids = database.get_curated_playlist('release_radar')
|
||||||
|
|
||||||
if curated_track_ids:
|
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 = []
|
selected_tracks = []
|
||||||
|
|
||||||
for track_id in curated_track_ids:
|
for track_id in curated_track_ids:
|
||||||
try:
|
if track_id in tracks_by_id:
|
||||||
# Get track data from Spotify
|
track = tracks_by_id[track_id]
|
||||||
track_data = spotify_client.get_track_details(track_id)
|
|
||||||
|
|
||||||
if track_data:
|
# Parse track_data_json if it's a string
|
||||||
# Get album cover from raw_data (enhanced data doesn't include images)
|
track_data = track.track_data_json
|
||||||
album_cover = None
|
if isinstance(track_data, str):
|
||||||
if track_data.get('raw_data') and track_data['raw_data'].get('album', {}).get('images'):
|
try:
|
||||||
album_cover = track_data['raw_data']['album']['images'][0]['url']
|
track_data = json.loads(track_data)
|
||||||
|
except:
|
||||||
|
track_data = None
|
||||||
|
|
||||||
selected_tracks.append({
|
selected_tracks.append({
|
||||||
"spotify_track_id": track_data['id'],
|
"spotify_track_id": track.spotify_track_id,
|
||||||
"track_name": track_data['name'],
|
"track_name": track.track_name,
|
||||||
"artist_name": track_data.get('primary_artist', 'Unknown'),
|
"artist_name": track.artist_name,
|
||||||
"album_name": track_data['album']['name'] if track_data.get('album') else 'Unknown',
|
"album_name": track.album_name,
|
||||||
"album_cover_url": album_cover,
|
"album_cover_url": track.album_cover_url,
|
||||||
"duration_ms": track_data.get('duration_ms', 0),
|
"duration_ms": track.duration_ms,
|
||||||
"track_data_json": track_data.get('raw_data', track_data)
|
"track_data_json": track_data # Now properly parsed
|
||||||
})
|
})
|
||||||
except Exception as track_error:
|
|
||||||
# Skip tracks that fail to fetch
|
|
||||||
print(f"Error fetching track {track_id}: {track_error}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
return jsonify({"success": True, "tracks": selected_tracks})
|
return jsonify({"success": True, "tracks": selected_tracks})
|
||||||
|
|
||||||
|
|
@ -14534,6 +14560,15 @@ def get_discover_weekly():
|
||||||
for track_id in curated_track_ids:
|
for track_id in curated_track_ids:
|
||||||
if track_id in tracks_by_id:
|
if track_id in tracks_by_id:
|
||||||
track = tracks_by_id[track_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({
|
selected_tracks.append({
|
||||||
"spotify_track_id": track.spotify_track_id,
|
"spotify_track_id": track.spotify_track_id,
|
||||||
"track_name": track.track_name,
|
"track_name": track.track_name,
|
||||||
|
|
@ -14541,7 +14576,7 @@ def get_discover_weekly():
|
||||||
"album_name": track.album_name,
|
"album_name": track.album_name,
|
||||||
"album_cover_url": track.album_cover_url,
|
"album_cover_url": track.album_cover_url,
|
||||||
"duration_ms": track.duration_ms,
|
"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})
|
return jsonify({"success": True, "tracks": selected_tracks})
|
||||||
|
|
|
||||||
|
|
@ -1668,8 +1668,37 @@
|
||||||
<!-- Release Radar Section -->
|
<!-- Release Radar Section -->
|
||||||
<div class="discover-section">
|
<div class="discover-section">
|
||||||
<div class="discover-section-header">
|
<div class="discover-section-header">
|
||||||
<h2 class="discover-section-title">Release Radar</h2>
|
<div>
|
||||||
<p class="discover-section-subtitle">Fresh tracks from new releases</p>
|
<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>
|
||||||
<div class="discover-playlist-container compact" id="release-radar-playlist">
|
<div class="discover-playlist-container compact" id="release-radar-playlist">
|
||||||
<!-- Content will be populated dynamically -->
|
<!-- Content will be populated dynamically -->
|
||||||
|
|
@ -1683,8 +1712,37 @@
|
||||||
<!-- Discovery Weekly Section -->
|
<!-- Discovery Weekly Section -->
|
||||||
<div class="discover-section">
|
<div class="discover-section">
|
||||||
<div class="discover-section-header">
|
<div class="discover-section-header">
|
||||||
<h2 class="discover-section-title">Discovery Weekly</h2>
|
<div>
|
||||||
<p class="discover-section-subtitle">A personalized playlist just for you</p>
|
<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>
|
||||||
<div class="discover-playlist-container compact" id="discovery-weekly-playlist">
|
<div class="discover-playlist-container compact" id="discovery-weekly-playlist">
|
||||||
<!-- Content will be populated dynamically -->
|
<!-- Content will be populated dynamically -->
|
||||||
|
|
|
||||||
|
|
@ -24213,6 +24213,11 @@ let discoverHeroIndex = 0;
|
||||||
let discoverHeroArtists = [];
|
let discoverHeroArtists = [];
|
||||||
let discoverHeroInterval = null;
|
let discoverHeroInterval = null;
|
||||||
|
|
||||||
|
// Store discover playlist tracks for download/sync functionality
|
||||||
|
let discoverReleaseRadarTracks = [];
|
||||||
|
let discoverWeeklyTracks = [];
|
||||||
|
let discoverRecentAlbums = [];
|
||||||
|
|
||||||
async function loadDiscoverPage() {
|
async function loadDiscoverPage() {
|
||||||
console.log('Loading discover page...');
|
console.log('Loading discover page...');
|
||||||
|
|
||||||
|
|
@ -24224,6 +24229,71 @@ async function loadDiscoverPage() {
|
||||||
loadDiscoverWeekly(),
|
loadDiscoverWeekly(),
|
||||||
loadMoreForYou()
|
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() {
|
async function loadDiscoverHero() {
|
||||||
|
|
@ -24320,12 +24390,15 @@ async function loadDiscoverRecentReleases() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store albums for download functionality
|
||||||
|
discoverRecentAlbums = data.albums;
|
||||||
|
|
||||||
// Build carousel HTML
|
// Build carousel HTML
|
||||||
let html = '';
|
let html = '';
|
||||||
data.albums.forEach(album => {
|
data.albums.forEach((album, index) => {
|
||||||
const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
|
const coverUrl = album.album_cover_url || '/static/placeholder-album.png';
|
||||||
html += `
|
html += `
|
||||||
<div class="discover-card">
|
<div class="discover-card" onclick="openDownloadModalForRecentAlbum(${index})" style="cursor: pointer;">
|
||||||
<div class="discover-card-image">
|
<div class="discover-card-image">
|
||||||
<img src="${coverUrl}" alt="${album.album_name}">
|
<img src="${coverUrl}" alt="${album.album_name}">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24367,6 +24440,9 @@ async function loadDiscoverReleaseRadar() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store tracks for download/sync functionality
|
||||||
|
discoverReleaseRadarTracks = data.tracks;
|
||||||
|
|
||||||
// Build compact playlist HTML
|
// Build compact playlist HTML
|
||||||
let html = '<div class="discover-playlist-tracks-compact">';
|
let html = '<div class="discover-playlist-tracks-compact">';
|
||||||
data.tracks.forEach((track, index) => {
|
data.tracks.forEach((track, index) => {
|
||||||
|
|
@ -24421,6 +24497,9 @@ async function loadDiscoverWeekly() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store tracks for download/sync functionality
|
||||||
|
discoverWeeklyTracks = data.tracks;
|
||||||
|
|
||||||
// Build compact playlist HTML
|
// Build compact playlist HTML
|
||||||
let html = '<div class="discover-playlist-tracks-compact">';
|
let html = '<div class="discover-playlist-tracks-compact">';
|
||||||
data.tracks.forEach((track, index) => {
|
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 {
|
.discover-section-header {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discover-section-title {
|
.discover-section-title {
|
||||||
|
|
@ -16271,6 +16275,121 @@ body {
|
||||||
margin: 0;
|
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 */
|
/* Carousel Styles */
|
||||||
.discover-carousel {
|
.discover-carousel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue