Add Deezer user playlists tab via ARL authentication
New "Deezer" tab on sync page shows authenticated user's playlists, identical to the Spotify tab pattern — same card layout, details modal with track list, sync button, and download missing tracks flow. Existing URL import renamed to "Deezer Link" tab (unchanged). Backend: get_user_playlists() and get_playlist_tracks() on the download client fetch via public API with ARL session cookies. Album release dates batch-fetched for $year template variable. Three new endpoints: arl-status, arl-playlists, arl-playlist/<id>. Frontend: cards use Spotify-identical HTML structure with live sync status, progress indicators, and View Progress/Results buttons. Downloads reuse openDownloadMissingModal with zero modifications. Track data cached on first open, instant on subsequent clicks.
This commit is contained in:
parent
e65b6bab67
commit
31518a3ef3
4 changed files with 477 additions and 6 deletions
|
|
@ -236,6 +236,137 @@ class DeezerDownloadClient:
|
|||
labels = {'flac': 'FLAC (Lossless)', 'mp3_320': 'MP3 320kbps', 'mp3_128': 'MP3 128kbps'}
|
||||
return labels.get(self._quality, 'MP3 320kbps')
|
||||
|
||||
# ─── User Playlists (ARL-authenticated) ─────────────────────
|
||||
|
||||
def get_user_playlists(self) -> list:
|
||||
"""Fetch the authenticated user's playlists via Deezer public API with ARL cookies."""
|
||||
if not self._authenticated or not self._user_data:
|
||||
return []
|
||||
user_id = self._user_data.get('USER_ID')
|
||||
if not user_id:
|
||||
return []
|
||||
|
||||
playlists = []
|
||||
index = 0
|
||||
while True:
|
||||
try:
|
||||
resp = self._session.get(
|
||||
f'https://api.deezer.com/user/{user_id}/playlists',
|
||||
params={'index': index, 'limit': 100},
|
||||
timeout=15
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if 'error' in data:
|
||||
logger.warning(f"Deezer playlists error: {data['error']}")
|
||||
break
|
||||
items = data.get('data', [])
|
||||
if not items:
|
||||
break
|
||||
for p in items:
|
||||
playlists.append({
|
||||
'id': str(p.get('id', '')),
|
||||
'name': p.get('title', ''),
|
||||
'track_count': p.get('nb_tracks', 0),
|
||||
'image_url': p.get('picture_medium', ''),
|
||||
'owner': p.get('creator', {}).get('name', ''),
|
||||
'description': p.get('description', ''),
|
||||
})
|
||||
if not data.get('next'):
|
||||
break
|
||||
index += len(items)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching user playlists at index {index}: {e}")
|
||||
break
|
||||
|
||||
logger.info(f"Fetched {len(playlists)} user playlists from Deezer")
|
||||
return playlists
|
||||
|
||||
def get_playlist_tracks(self, playlist_id: str) -> Optional[dict]:
|
||||
"""Fetch full playlist details with tracks via public API (ARL cookies grant private access)."""
|
||||
try:
|
||||
resp = self._session.get(
|
||||
f'https://api.deezer.com/playlist/{playlist_id}',
|
||||
timeout=15
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if 'error' in data:
|
||||
logger.error(f"Deezer playlist error: {data['error']}")
|
||||
return None
|
||||
|
||||
total_tracks = data.get('nb_tracks', 0)
|
||||
raw_tracks = data.get('tracks', {}).get('data', [])
|
||||
|
||||
# Paginate if needed
|
||||
while len(raw_tracks) < total_tracks:
|
||||
idx = len(raw_tracks)
|
||||
page_resp = self._session.get(
|
||||
f'https://api.deezer.com/playlist/{playlist_id}/tracks',
|
||||
params={'index': idx, 'limit': 400},
|
||||
timeout=15
|
||||
)
|
||||
page_resp.raise_for_status()
|
||||
page_data = page_resp.json()
|
||||
if 'error' in page_data:
|
||||
break
|
||||
page_tracks = page_data.get('data', [])
|
||||
if not page_tracks:
|
||||
break
|
||||
raw_tracks.extend(page_tracks)
|
||||
|
||||
# Batch-fetch release dates for unique albums
|
||||
album_ids = set()
|
||||
for t in raw_tracks:
|
||||
aid = t.get('album', {}).get('id')
|
||||
if aid:
|
||||
album_ids.add(str(aid))
|
||||
album_release_dates = {}
|
||||
for aid in album_ids:
|
||||
try:
|
||||
time.sleep(0.3) # Respect rate limits
|
||||
a_resp = self._session.get(f'https://api.deezer.com/album/{aid}', timeout=10)
|
||||
if a_resp.ok:
|
||||
a_data = a_resp.json()
|
||||
album_release_dates[aid] = a_data.get('release_date', '')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tracks = []
|
||||
for i, t in enumerate(raw_tracks, start=1):
|
||||
artist_name = t.get('artist', {}).get('name', 'Unknown Artist')
|
||||
album_data = t.get('album', {})
|
||||
album_cover = album_data.get('cover_medium') or album_data.get('cover_small') or ''
|
||||
album_id = str(album_data.get('id', ''))
|
||||
tracks.append({
|
||||
'id': str(t.get('id', '')),
|
||||
'name': t.get('title', ''),
|
||||
'artists': [{'name': artist_name}],
|
||||
'album': {
|
||||
'name': album_data.get('title', ''),
|
||||
'images': [{'url': album_cover}] if album_cover else [],
|
||||
'release_date': album_release_dates.get(album_id, ''),
|
||||
'album_type': 'album',
|
||||
'total_tracks': total_tracks,
|
||||
'id': album_id,
|
||||
},
|
||||
'duration_ms': t.get('duration', 0) * 1000,
|
||||
'track_number': i,
|
||||
})
|
||||
|
||||
return {
|
||||
'id': str(data.get('id', '')),
|
||||
'name': data.get('title', ''),
|
||||
'description': data.get('description', ''),
|
||||
'track_count': total_tracks,
|
||||
'image_url': data.get('picture_medium', ''),
|
||||
'owner': data.get('creator', {}).get('name', ''),
|
||||
'tracks': tracks,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching playlist {playlist_id}: {e}")
|
||||
return None
|
||||
|
||||
# ─── Track Info ──────────────────────────────────────────────
|
||||
|
||||
def _get_track_data(self, track_id: str) -> Optional[dict]:
|
||||
|
|
|
|||
|
|
@ -32982,6 +32982,75 @@ def _get_metadata_fallback_client():
|
|||
from core.itunes_client import iTunesClient
|
||||
return iTunesClient()
|
||||
|
||||
@app.route('/api/deezer/arl-status', methods=['GET'])
|
||||
def get_deezer_arl_status():
|
||||
"""Check if Deezer ARL is configured and authenticated."""
|
||||
try:
|
||||
deezer_dl = None
|
||||
if soulseek_client and hasattr(soulseek_client, 'deezer_dl') and soulseek_client.deezer_dl:
|
||||
deezer_dl = soulseek_client.deezer_dl
|
||||
if deezer_dl and deezer_dl.is_authenticated():
|
||||
user_data = deezer_dl._user_data or {}
|
||||
return jsonify({
|
||||
'authenticated': True,
|
||||
'user_name': user_data.get('BLOG_NAME', 'Unknown'),
|
||||
'user_id': user_data.get('USER_ID'),
|
||||
})
|
||||
return jsonify({'authenticated': False})
|
||||
except Exception as e:
|
||||
return jsonify({'authenticated': False, 'error': str(e)})
|
||||
|
||||
|
||||
@app.route('/api/deezer/arl-playlists', methods=['GET'])
|
||||
def get_deezer_arl_playlists():
|
||||
"""Fetch user playlists via Deezer ARL authentication (like /api/spotify/playlists)."""
|
||||
try:
|
||||
deezer_dl = None
|
||||
if soulseek_client and hasattr(soulseek_client, 'deezer_dl') and soulseek_client.deezer_dl:
|
||||
deezer_dl = soulseek_client.deezer_dl
|
||||
if not deezer_dl or not deezer_dl.is_authenticated():
|
||||
return jsonify({'error': 'Deezer ARL not authenticated. Configure your ARL token in Settings > Downloads.'}), 401
|
||||
|
||||
playlists = deezer_dl.get_user_playlists()
|
||||
|
||||
# Add sync_status field to match Spotify format
|
||||
playlist_data = []
|
||||
for p in playlists:
|
||||
playlist_data.append({
|
||||
'id': p['id'],
|
||||
'name': p['name'],
|
||||
'owner': p.get('owner', ''),
|
||||
'track_count': p.get('track_count', 0),
|
||||
'image_url': p.get('image_url', ''),
|
||||
'sync_status': 'Never Synced',
|
||||
})
|
||||
|
||||
print(f"🎵 Loaded {len(playlist_data)} Deezer user playlists via ARL")
|
||||
return jsonify(playlist_data)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/deezer/arl-playlist/<playlist_id>', methods=['GET'])
|
||||
def get_deezer_arl_playlist_tracks(playlist_id):
|
||||
"""Fetch full playlist with tracks via ARL (like /api/spotify/playlist/<id>)."""
|
||||
try:
|
||||
deezer_dl = None
|
||||
if soulseek_client and hasattr(soulseek_client, 'deezer_dl') and soulseek_client.deezer_dl:
|
||||
deezer_dl = soulseek_client.deezer_dl
|
||||
if not deezer_dl or not deezer_dl.is_authenticated():
|
||||
return jsonify({'error': 'Deezer ARL not authenticated.'}), 401
|
||||
|
||||
playlist = deezer_dl.get_playlist_tracks(playlist_id)
|
||||
if not playlist:
|
||||
return jsonify({'error': 'Playlist not found or unable to access.'}), 404
|
||||
|
||||
print(f"🎵 Loaded {len(playlist.get('tracks', []))} tracks from Deezer playlist: {playlist.get('name')}")
|
||||
return jsonify(playlist)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/deezer/playlist/<playlist_id>', methods=['GET'])
|
||||
def get_deezer_playlist(playlist_id):
|
||||
"""Fetch a Deezer playlist by ID or URL"""
|
||||
|
|
|
|||
|
|
@ -1145,6 +1145,9 @@
|
|||
<button class="sync-tab-button" data-tab="deezer">
|
||||
<span class="tab-icon deezer-icon"></span> Deezer
|
||||
</button>
|
||||
<button class="sync-tab-button" data-tab="deezer-link">
|
||||
<span class="tab-icon deezer-icon"></span> Deezer Link
|
||||
</button>
|
||||
<button class="sync-tab-button" data-tab="youtube">
|
||||
<span class="tab-icon youtube-icon"></span> YouTube
|
||||
</button>
|
||||
|
|
@ -1181,8 +1184,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deezer Tab Content -->
|
||||
<!-- Deezer Tab Content (ARL User Playlists) -->
|
||||
<div class="sync-tab-content" id="deezer-tab-content">
|
||||
<div class="playlist-header">
|
||||
<h3>Your Deezer Playlists</h3>
|
||||
<button class="refresh-button deezer" id="deezer-arl-refresh-btn">🔄 Refresh</button>
|
||||
</div>
|
||||
<div class="playlist-scroll-container" id="deezer-arl-playlist-container">
|
||||
<div class="playlist-placeholder">Click 'Refresh' to load your Deezer playlists.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deezer Link Tab Content (URL Import) -->
|
||||
<div class="sync-tab-content" id="deezer-link-tab-content">
|
||||
<div class="youtube-input-section">
|
||||
<input type="text" id="deezer-url-input"
|
||||
placeholder="Paste Deezer Playlist URL...">
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ let tidalPlaylistStates = {}; // Key: playlist_id, Value: playlist state with ph
|
|||
let tidalPlaylistsLoaded = false;
|
||||
let deezerPlaylists = [];
|
||||
let deezerPlaylistStates = {};
|
||||
let deezerArlPlaylists = [];
|
||||
let deezerArlPlaylistsLoaded = false;
|
||||
|
||||
// --- Beatport Chart State Management (Similar to YouTube/Tidal) ---
|
||||
let beatportChartStates = {}; // Key: chart_hash, Value: chart state with phases
|
||||
|
|
@ -15742,7 +15744,11 @@ async function startPlaylistSync(playlistId) {
|
|||
const trackFetchStart = Date.now();
|
||||
console.log(`🔄 [${new Date().toTimeString().split(' ')[0]}] Cache miss - fetching tracks for playlist ${playlistId}`);
|
||||
try {
|
||||
const response = await fetch(`/api/spotify/playlist/${playlistId}`);
|
||||
// Use the right endpoint based on playlist source
|
||||
const fetchUrl = playlistId.startsWith('deezer_arl_')
|
||||
? `/api/deezer/arl-playlist/${playlistId.replace('deezer_arl_', '')}`
|
||||
: `/api/spotify/playlist/${playlistId}`;
|
||||
const response = await fetch(fetchUrl);
|
||||
const fullPlaylist = await response.json();
|
||||
if (fullPlaylist.error) throw new Error(fullPlaylist.error);
|
||||
tracks = fullPlaylist.tracks;
|
||||
|
|
@ -15785,7 +15791,7 @@ async function startPlaylistSync(playlistId) {
|
|||
showToast(`Sync started for "${playlist.name}"`, 'success');
|
||||
|
||||
// Show initial sync state in modal if open
|
||||
const modal = document.getElementById('playlist-details-modal');
|
||||
const modal = document.getElementById('playlist-details-modal') || document.getElementById('deezer-arl-playlist-details-modal');
|
||||
if (modal && modal.style.display !== 'none') {
|
||||
const statusDisplay = document.getElementById(`modal-sync-status-${playlist.id}`);
|
||||
if (statusDisplay) {
|
||||
|
|
@ -15853,7 +15859,7 @@ function startSyncPolling(playlistId) {
|
|||
stopSyncPolling(playlistId);
|
||||
updateCardToDefault(playlistId, state);
|
||||
// Also update the modal if it's open
|
||||
closePlaylistDetailsModal(); // Close modal on completion/error
|
||||
closePlaylistDetailsModal(); closeDeezerArlPlaylistDetailsModal(); // Close modal on completion/error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error polling sync status for ${playlistId}:`, error);
|
||||
|
|
@ -16071,7 +16077,7 @@ function updateCardToDefault(playlistId, finalState = null) {
|
|||
|
||||
// Update the modal's sync progress display (matches GUI functionality)
|
||||
function updateModalSyncProgress(playlistId, progress) {
|
||||
const modal = document.getElementById('playlist-details-modal');
|
||||
const modal = document.getElementById('playlist-details-modal') || document.getElementById('deezer-arl-playlist-details-modal');
|
||||
if (modal && modal.style.display !== 'none') {
|
||||
console.log(`📊 Updating modal sync progress for ${playlistId}:`, progress);
|
||||
|
||||
|
|
@ -26349,6 +26355,237 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,
|
|||
}
|
||||
|
||||
|
||||
// ===================================================================
|
||||
// DEEZER ARL PLAYLIST MANAGEMENT (Spotify-identical pattern)
|
||||
// ===================================================================
|
||||
|
||||
async function loadDeezerArlPlaylists() {
|
||||
const container = document.getElementById('deezer-arl-playlist-container');
|
||||
const refreshBtn = document.getElementById('deezer-arl-refresh-btn');
|
||||
|
||||
container.innerHTML = `<div class="playlist-placeholder">🔄 Loading playlists...</div>`;
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.textContent = '🔄 Loading...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/deezer/arl-playlists');
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to fetch Deezer playlists');
|
||||
}
|
||||
deezerArlPlaylists = await response.json();
|
||||
renderDeezerArlPlaylists();
|
||||
deezerArlPlaylistsLoaded = true;
|
||||
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div class="playlist-placeholder">❌ Error: ${error.message}</div>`;
|
||||
showToast(`Error loading Deezer playlists: ${error.message}`, 'error');
|
||||
} finally {
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.textContent = '🔄 Refresh';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeezerArlPlaylists() {
|
||||
const container = document.getElementById('deezer-arl-playlist-container');
|
||||
if (deezerArlPlaylists.length === 0) {
|
||||
container.innerHTML = `<div class="playlist-placeholder">No Deezer playlists found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = deezerArlPlaylists.map(p => {
|
||||
const arlId = `deezer_arl_${p.id}`;
|
||||
let statusClass = 'status-never-synced';
|
||||
if (p.sync_status && p.sync_status.startsWith('Synced')) statusClass = 'status-synced';
|
||||
|
||||
return `
|
||||
<div class="playlist-card deezer-arl-playlist-card" data-playlist-id="${arlId}">
|
||||
<div class="playlist-card-main">
|
||||
<div class="playlist-card-content">
|
||||
<div class="playlist-card-name">${escapeHtml(p.name)}</div>
|
||||
<div class="playlist-card-info">
|
||||
<span>${p.track_count} tracks</span> •
|
||||
<span class="playlist-card-status ${statusClass}">${p.sync_status || 'Never Synced'}</span>
|
||||
</div>
|
||||
<div class="sync-progress-indicator" id="progress-${arlId}"></div>
|
||||
</div>
|
||||
<div class="playlist-card-actions">
|
||||
<button id="action-btn-${arlId}" onclick="openDeezerArlPlaylistDetailsModal(event, '${p.id}')">Sync / Download</button>
|
||||
<button id="progress-btn-${arlId}" class="view-progress-btn hidden" onclick="handleDeezerArlViewProgressClick(event, '${p.id}')">
|
||||
View Progress
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function handleDeezerArlViewProgressClick(event, playlistId) {
|
||||
event.stopPropagation();
|
||||
const arlPlaylistId = `deezer_arl_${playlistId}`;
|
||||
const process = activeDownloadProcesses[arlPlaylistId];
|
||||
if (process && process.modalElement) {
|
||||
process.modalElement.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
async function openDeezerArlPlaylistDetailsModal(event, playlistId) {
|
||||
event.stopPropagation();
|
||||
|
||||
const playlist = deezerArlPlaylists.find(p => String(p.id) === String(playlistId));
|
||||
if (!playlist) return;
|
||||
|
||||
const arlPlaylistId = `deezer_arl_${playlistId}`;
|
||||
showLoadingOverlay(`Loading playlist: ${playlist.name}...`);
|
||||
|
||||
try {
|
||||
if (playlistTrackCache[arlPlaylistId]) {
|
||||
const fullPlaylist = { ...playlist, id: arlPlaylistId, tracks: playlistTrackCache[arlPlaylistId] };
|
||||
showDeezerArlPlaylistDetailsModal(fullPlaylist, playlistId);
|
||||
} else {
|
||||
const response = await fetch(`/api/deezer/arl-playlist/${playlistId}`);
|
||||
const fullPlaylist = await response.json();
|
||||
if (fullPlaylist.error) throw new Error(fullPlaylist.error);
|
||||
|
||||
playlistTrackCache[arlPlaylistId] = fullPlaylist.tracks;
|
||||
|
||||
// Auto-mirror
|
||||
mirrorPlaylist('deezer', playlistId, fullPlaylist.name, fullPlaylist.tracks.map(t => ({
|
||||
track_name: t.name,
|
||||
artist_name: (t.artists && t.artists[0]) ? (typeof t.artists[0] === 'object' ? t.artists[0].name : t.artists[0]) : '',
|
||||
album_name: t.album ? (typeof t.album === 'object' ? t.album.name : t.album) : '',
|
||||
duration_ms: t.duration_ms || 0,
|
||||
source_track_id: t.id || ''
|
||||
})), { description: fullPlaylist.description, owner: fullPlaylist.owner, image_url: fullPlaylist.image_url });
|
||||
|
||||
showDeezerArlPlaylistDetailsModal({ ...fullPlaylist, id: arlPlaylistId }, playlistId);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(`Error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
function showDeezerArlPlaylistDetailsModal(playlist, originalDeezerPlaylistId) {
|
||||
let modal = document.getElementById('deezer-arl-playlist-details-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'deezer-arl-playlist-details-modal';
|
||||
modal.className = 'modal-overlay';
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
const playlistId = playlist.id;
|
||||
const activeProcess = activeDownloadProcesses[playlistId];
|
||||
const hasCompletedProcess = activeProcess && activeProcess.status === 'complete';
|
||||
const isSyncing = !!activeSyncPollers[playlistId];
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-container playlist-modal">
|
||||
<div class="playlist-modal-header">
|
||||
<div class="playlist-header-content">
|
||||
<h2>${escapeHtml(playlist.name)}</h2>
|
||||
<div class="playlist-quick-info">
|
||||
<span class="playlist-track-count">${playlist.track_count || (playlist.tracks ? playlist.tracks.length : 0)} tracks</span>
|
||||
<span class="playlist-owner">by ${escapeHtml(playlist.owner || '')}</span>
|
||||
</div>
|
||||
<div class="playlist-modal-sync-status" id="modal-sync-status-${playlistId}" style="display: none;">
|
||||
<span class="sync-stat total-tracks">♪ <span id="modal-total-${playlistId}">0</span></span>
|
||||
<span class="sync-separator">/</span>
|
||||
<span class="sync-stat matched-tracks">✓ <span id="modal-matched-${playlistId}">0</span></span>
|
||||
<span class="sync-separator">/</span>
|
||||
<span class="sync-stat failed-tracks">✗ <span id="modal-failed-${playlistId}">0</span></span>
|
||||
<span class="sync-stat percentage">(<span id="modal-percentage-${playlistId}">0</span>%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="playlist-modal-close" onclick="closeDeezerArlPlaylistDetailsModal()">×</span>
|
||||
</div>
|
||||
|
||||
<div class="playlist-modal-body">
|
||||
${playlist.description ? `<div class="playlist-description">${escapeHtml(playlist.description)}</div>` : ''}
|
||||
|
||||
<div class="playlist-tracks-container">
|
||||
<div class="playlist-tracks-list">
|
||||
${(playlist.tracks || []).map((track, index) => `
|
||||
<div class="playlist-track-item">
|
||||
<span class="playlist-track-number">${index + 1}</span>
|
||||
<div class="playlist-track-info">
|
||||
<div class="playlist-track-name">${escapeHtml(track.name)}</div>
|
||||
<div class="playlist-track-artists">${formatArtists(track.artists)}</div>
|
||||
</div>
|
||||
<div class="playlist-track-duration">${formatDuration(track.duration_ms)}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-modal-footer">
|
||||
<button class="playlist-modal-btn playlist-modal-btn-secondary" onclick="closeDeezerArlPlaylistDetailsModal()">Close</button>
|
||||
<button class="playlist-modal-btn playlist-modal-btn-tertiary" onclick="closeDeezerArlPlaylistDetailsModal(); openDownloadMissingModal('${playlistId}')">
|
||||
${hasCompletedProcess ? '📊 View Download Results' : '📥 Download Missing Tracks'}
|
||||
</button>
|
||||
<button id="sync-btn-${playlistId}" class="playlist-modal-btn playlist-modal-btn-primary" onclick="startPlaylistSync('${playlistId}')" ${isSyncing ? 'disabled' : ''}>${isSyncing ? '⏳ Syncing...' : 'Sync Playlist'}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store playlist in spotifyPlaylists-compatible format for openDownloadMissingModal
|
||||
if (!spotifyPlaylists.find(p => p.id === playlistId)) {
|
||||
spotifyPlaylists.push({
|
||||
id: playlistId,
|
||||
name: playlist.name,
|
||||
track_count: playlist.tracks ? playlist.tracks.length : 0,
|
||||
image_url: playlist.image_url || '',
|
||||
owner: playlist.owner || '',
|
||||
});
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeDeezerArlPlaylistDetailsModal() {
|
||||
const modal = document.getElementById('deezer-arl-playlist-details-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
function updateDeezerArlPlaylistCardUI(playlistId) {
|
||||
const arlPlaylistId = `deezer_arl_${playlistId}`;
|
||||
const process = activeDownloadProcesses[arlPlaylistId];
|
||||
const progressBtn = document.getElementById(`progress-btn-${arlPlaylistId}`);
|
||||
const actionBtn = document.getElementById(`action-btn-${arlPlaylistId}`);
|
||||
const card = document.querySelector(`.playlist-card[data-playlist-id="${arlPlaylistId}"]`);
|
||||
|
||||
if (!progressBtn || !actionBtn) return;
|
||||
|
||||
if (process && process.status === 'running') {
|
||||
progressBtn.classList.remove('hidden');
|
||||
progressBtn.textContent = 'View Progress';
|
||||
progressBtn.style.backgroundColor = '';
|
||||
actionBtn.textContent = '📥 Downloading...';
|
||||
actionBtn.disabled = true;
|
||||
if (card) card.classList.remove('download-complete');
|
||||
} else if (process && process.status === 'complete') {
|
||||
progressBtn.classList.remove('hidden');
|
||||
progressBtn.textContent = '📋 View Results';
|
||||
progressBtn.style.backgroundColor = '#28a745';
|
||||
progressBtn.style.color = 'white';
|
||||
actionBtn.textContent = '✅ Ready for Review';
|
||||
actionBtn.disabled = false;
|
||||
if (card) card.classList.add('download-complete');
|
||||
} else {
|
||||
progressBtn.classList.add('hidden');
|
||||
progressBtn.style.backgroundColor = '';
|
||||
progressBtn.style.color = '';
|
||||
actionBtn.textContent = 'Sync / Download';
|
||||
actionBtn.disabled = false;
|
||||
if (card) card.classList.remove('download-complete');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===================================================================
|
||||
// DEEZER PLAYLIST MANAGEMENT (URL-input like YouTube, reuses YouTube modal)
|
||||
// ===================================================================
|
||||
|
|
@ -27347,6 +27584,19 @@ function initializeSyncPage() {
|
|||
syncContentArea.style.gridTemplateColumns = '1fr';
|
||||
}
|
||||
|
||||
// Auto-load Deezer ARL playlists on first tab activation
|
||||
if (tabId === 'deezer' && !deezerArlPlaylistsLoaded) {
|
||||
// Check ARL status first
|
||||
fetch('/api/deezer/arl-status').then(r => r.json()).then(data => {
|
||||
const container = document.getElementById('deezer-arl-playlist-container');
|
||||
if (data.authenticated) {
|
||||
loadDeezerArlPlaylists();
|
||||
} else if (container) {
|
||||
container.innerHTML = `<div class="playlist-placeholder">Deezer ARL not configured. Add your ARL token in Settings > Downloads to see your playlists here.</div>`;
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Auto-load mirrored playlists on first tab activation
|
||||
if (tabId === 'mirrored' && !mirroredPlaylistsLoaded) {
|
||||
loadMirroredPlaylists();
|
||||
|
|
@ -27380,7 +27630,14 @@ function initializeSyncPage() {
|
|||
tidalRefreshBtn.addEventListener('click', loadTidalPlaylists);
|
||||
}
|
||||
|
||||
// Logic for the Deezer parse button
|
||||
// Logic for the Deezer ARL refresh button
|
||||
const deezerArlRefreshBtn = document.getElementById('deezer-arl-refresh-btn');
|
||||
if (deezerArlRefreshBtn) {
|
||||
deezerArlRefreshBtn.removeEventListener('click', loadDeezerArlPlaylists);
|
||||
deezerArlRefreshBtn.addEventListener('click', loadDeezerArlPlaylists);
|
||||
}
|
||||
|
||||
// Logic for the Deezer Link parse button
|
||||
const deezerParseBtn = document.getElementById('deezer-parse-btn');
|
||||
if (deezerParseBtn) {
|
||||
deezerParseBtn.addEventListener('click', loadDeezerPlaylist);
|
||||
|
|
|
|||
Loading…
Reference in a new issue