Add Sync to Server button for Beatport playlists in download modal

- "Sync to Server" button appears for Beatport chart/playlist downloads
- Starts media server sync via /api/sync/start with live progress bar
- Progress area renders below all modal buttons with matched/failed counts
- Cancel button to abort sync mid-way, auto-cleanup on completion
This commit is contained in:
Broque Thomas 2026-03-17 20:54:58 -07:00
parent 27a0cf8b81
commit dc612da9d5
2 changed files with 227 additions and 0 deletions

View file

@ -18624,6 +18624,175 @@ async function deleteSyncHistoryEntry(entryId) {
}
}
// ── Sync Playlist to Server (from Download Modal) ──────────────────
// Track active modal syncs
let _activeModalSyncs = {};
function _isBeatportPlaylistId(id) {
return id.startsWith('beatport_chart_') || id.startsWith('beatport_top100_') || id.startsWith('beatport_hype100_');
}
async function syncPlaylistToServer(playlistId) {
const process = activeDownloadProcesses[playlistId];
if (!process) { showToast('No playlist data found', 'error'); return; }
// Disable the sync button
const btn = document.getElementById(`sync-server-btn-${playlistId}`);
if (btn) { btn.disabled = true; btn.textContent = 'Syncing...'; }
// Show progress area
const progressArea = document.getElementById(`modal-sync-progress-${playlistId}`);
if (progressArea) progressArea.style.display = '';
const syncPlaylistId = `beatport_sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const playlistName = process.playlist?.name || 'Beatport Playlist';
// Format tracks for the sync API
const tracks = (process.tracks || []).map(t => {
const artists = Array.isArray(t.artists)
? (typeof t.artists[0] === 'object' ? t.artists.map(a => a.name || a) : t.artists)
: [t.artists || 'Unknown Artist'];
const albumName = typeof t.album === 'object' ? (t.album?.name || '') : (t.album || '');
return {
id: t.id || '',
name: t.name || '',
artists: artists,
album: albumName,
duration_ms: t.duration_ms || 0,
popularity: t.popularity || 0
};
});
try {
const response = await fetch('/api/sync/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playlist_id: syncPlaylistId,
playlist_name: playlistName,
tracks: tracks
})
});
const result = await response.json();
if (!result.success) {
showToast(`Sync failed: ${result.error || 'Unknown error'}`, 'error');
_cleanupModalSync(playlistId);
return;
}
_activeModalSyncs[playlistId] = { syncPlaylistId };
_pollModalSyncProgress(playlistId, syncPlaylistId);
} catch (err) {
console.error('Error starting playlist sync:', err);
showToast('Failed to start sync', 'error');
_cleanupModalSync(playlistId);
}
}
function _pollModalSyncProgress(playlistId, syncPlaylistId) {
const pollInterval = setInterval(async () => {
try {
const resp = await fetch(`/api/sync/status/${syncPlaylistId}`);
if (!resp.ok) { clearInterval(pollInterval); _cleanupModalSync(playlistId, 'error'); return; }
const state = await resp.json();
const bar = document.getElementById(`modal-sync-bar-${playlistId}`);
const stepEl = document.getElementById(`modal-sync-step-${playlistId}`);
const matchedEl = document.getElementById(`modal-sync-matched-${playlistId}`);
const failedEl = document.getElementById(`modal-sync-failed-${playlistId}`);
if (state.status === 'syncing' || state.status === 'starting') {
const p = state.progress || {};
const matched = p.matched_tracks || 0;
const failed = p.failed_tracks || 0;
const total = p.total_tracks || 0;
const step = p.current_step || 'Processing';
const currentTrack = p.current_track || '';
const processed = matched + failed;
const percent = total > 0 ? Math.round((processed / total) * 100) : 0;
if (bar) bar.style.width = `${percent}%`;
if (stepEl) stepEl.textContent = currentTrack ? `${step}${currentTrack}` : step;
if (matchedEl) matchedEl.textContent = `${matched} matched`;
if (failedEl) failedEl.textContent = `${failed} failed`;
} else if (state.status === 'finished') {
clearInterval(pollInterval);
const p = state.progress || state.result || {};
const matched = p.matched_tracks || 0;
const failed = p.failed_tracks || 0;
const total = p.total_tracks || 0;
const synced = p.synced_tracks || 0;
if (bar) bar.style.width = '100%';
if (stepEl) stepEl.textContent = `Sync complete — ${matched}/${total} matched, ${synced} synced`;
if (matchedEl) matchedEl.textContent = `${matched} matched`;
if (failedEl) failedEl.textContent = `${failed} failed`;
const cancelBtn = document.getElementById(`modal-sync-cancel-${playlistId}`);
if (cancelBtn) cancelBtn.style.display = 'none';
showToast(`Server sync complete: ${matched}/${total} matched`, 'success');
// Re-enable sync button after a delay
setTimeout(() => _cleanupModalSync(playlistId, 'finished'), 5000);
} else if (state.status === 'cancelled' || state.status === 'error') {
clearInterval(pollInterval);
if (stepEl) stepEl.textContent = state.status === 'cancelled' ? 'Sync cancelled' : `Sync error`;
const cancelBtn = document.getElementById(`modal-sync-cancel-${playlistId}`);
if (cancelBtn) cancelBtn.style.display = 'none';
setTimeout(() => _cleanupModalSync(playlistId, state.status), 3000);
}
} catch (err) {
console.error('Error polling modal sync status:', err);
clearInterval(pollInterval);
_cleanupModalSync(playlistId, 'error');
}
}, 2000);
if (_activeModalSyncs[playlistId]) {
_activeModalSyncs[playlistId].pollInterval = pollInterval;
}
}
async function cancelModalSync(playlistId) {
const active = _activeModalSyncs[playlistId];
if (!active) return;
try {
await fetch('/api/sync/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ playlist_id: active.syncPlaylistId })
});
const stepEl = document.getElementById(`modal-sync-step-${playlistId}`);
if (stepEl) stepEl.textContent = 'Cancelling...';
} catch (err) {
console.error('Error cancelling modal sync:', err);
}
}
function _cleanupModalSync(playlistId, finalStatus) {
const active = _activeModalSyncs[playlistId];
if (active && active.pollInterval) clearInterval(active.pollInterval);
delete _activeModalSyncs[playlistId];
const progressArea = document.getElementById(`modal-sync-progress-${playlistId}`);
const btn = document.getElementById(`sync-server-btn-${playlistId}`);
if (finalStatus === 'finished') {
// Keep progress visible but hide after fade
if (progressArea) setTimeout(() => { progressArea.style.display = 'none'; }, 300);
} else {
if (progressArea) progressArea.style.display = 'none';
}
if (btn) { btn.disabled = false; btn.textContent = 'Sync to Server'; }
}
// ── Metadata Cache Modal ────────────────────────────────────────────
let _mcacheCurrentTab = 'artist';
let _mcachePage = 0;
@ -32121,6 +32290,10 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis
<button class="download-control-btn primary" id="begin-analysis-btn-${virtualPlaylistId}" onclick="startMissingTracksProcess('${virtualPlaylistId}')">
Begin Analysis
</button>
${_isBeatportPlaylistId(virtualPlaylistId) ? `
<button class="download-control-btn sync-to-server-btn" id="sync-server-btn-${virtualPlaylistId}" onclick="syncPlaylistToServer('${virtualPlaylistId}')">
Sync to Server
</button>` : ''}
<button class="download-control-btn" id="add-to-wishlist-btn-${virtualPlaylistId}" onclick="addModalTracksToWishlist('${virtualPlaylistId}')" style="background-color: #9333ea; color: white;">
Add to Wishlist
</button>
@ -32135,6 +32308,20 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis
<button class="download-control-btn secondary" onclick="closeDownloadMissingModal('${virtualPlaylistId}')">Close</button>
</div>
</div>
<!-- Sync to server live progress (below footer, hidden by default) -->
<div class="modal-sync-progress-area" id="modal-sync-progress-${virtualPlaylistId}" style="display:none;">
<div class="modal-sync-progress-bar-bg">
<div class="modal-sync-progress-bar-fill" id="modal-sync-bar-${virtualPlaylistId}"></div>
</div>
<div class="modal-sync-progress-info">
<span class="modal-sync-step" id="modal-sync-step-${virtualPlaylistId}">Starting sync...</span>
<div class="modal-sync-stats">
<span class="matched" id="modal-sync-matched-${virtualPlaylistId}">0 matched</span>
<span class="failed" id="modal-sync-failed-${virtualPlaylistId}">0 failed</span>
</div>
<button class="modal-sync-cancel-btn" id="modal-sync-cancel-${virtualPlaylistId}" onclick="cancelModalSync('${virtualPlaylistId}')">Cancel</button>
</div>
</div>
</div>
`;

View file

@ -7181,6 +7181,46 @@ body {
.sync-history-cancel-btn:hover {
background: rgba(244, 67, 54, 0.2); border-color: rgba(244, 67, 54, 0.5);
}
/* Sync to Server button in download modal */
.sync-to-server-btn {
background: rgba(30, 136, 229, 0.15) !important; color: #42a5f5 !important;
border: 1px solid rgba(30, 136, 229, 0.3) !important;
}
.sync-to-server-btn:hover {
background: rgba(30, 136, 229, 0.25) !important; border-color: rgba(30, 136, 229, 0.5) !important;
}
.sync-to-server-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Sync progress area at bottom of download modal */
.modal-sync-progress-area {
padding: 10px 16px; background: rgba(30, 136, 229, 0.05);
border-top: 1px solid rgba(30, 136, 229, 0.15); border-radius: 0 0 12px 12px;
}
.modal-sync-progress-bar-bg {
height: 4px; background: rgba(255, 255, 255, 0.08); border-radius: 4px;
overflow: hidden; margin-bottom: 8px;
}
.modal-sync-progress-bar-fill {
height: 100%; background: #42a5f5; border-radius: 4px;
transition: width 0.3s ease; width: 0%;
}
.modal-sync-progress-info {
display: flex; align-items: center; gap: 10px; font-size: 11px;
}
.modal-sync-step {
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap; color: rgba(255, 255, 255, 0.5);
}
.modal-sync-stats { display: flex; gap: 8px; flex-shrink: 0; }
.modal-sync-stats .matched { color: #4caf50; font-size: 11px; }
.modal-sync-stats .failed { color: #f44336; font-size: 11px; }
.modal-sync-cancel-btn {
font-size: 10px; font-weight: 600; color: #f44336;
background: rgba(244, 67, 54, 0.1); border: 1px solid rgba(244, 67, 54, 0.25);
border-radius: 4px; padding: 3px 8px; cursor: pointer; flex-shrink: 0;
}
.modal-sync-cancel-btn:hover {
background: rgba(244, 67, 54, 0.2); border-color: rgba(244, 67, 54, 0.5);
}
.sync-history-empty {
text-align: center; padding: 48px 24px; color: rgba(255, 255, 255, 0.3); font-size: 14px;
}