Add sync tab deep-linking, redirect Discover sync to sync tab, add Force Download toggle
This commit is contained in:
parent
ed013d79b9
commit
f32da04fd1
4 changed files with 101 additions and 117 deletions
|
|
@ -7875,113 +7875,14 @@ function checkForActiveDiscoverDownloads() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startDiscoverPlaylistSync(playlistType, playlistName) {
|
async function startDiscoverPlaylistSync(playlistType, playlistName) {
|
||||||
console.log(`🔄 Starting sync for ${playlistName}`);
|
console.log(`🔄 Navigating to Sync → Discover tab for ${playlistName}`);
|
||||||
|
|
||||||
// Get tracks based on playlist type
|
// Navigate to the Sync page → Discover tab, highlight the card, and auto-sync
|
||||||
let tracks = [];
|
navigateToSyncTab('discover', {
|
||||||
if (playlistType === 'release_radar') {
|
highlight: `discover-sync-card-${playlistType}`,
|
||||||
tracks = discoverReleaseRadarTracks;
|
autoSync: playlistType,
|
||||||
} else if (playlistType === 'discovery_weekly') {
|
autoSyncName: playlistName,
|
||||||
tracks = discoverWeeklyTracks;
|
|
||||||
} else if (playlistType === 'seasonal_playlist') {
|
|
||||||
tracks = discoverSeasonalTracks;
|
|
||||||
} else if (playlistType === 'popular_picks') {
|
|
||||||
tracks = personalizedPopularPicks;
|
|
||||||
} else if (playlistType === 'hidden_gems') {
|
|
||||||
tracks = personalizedHiddenGems;
|
|
||||||
} else if (playlistType === 'discovery_shuffle') {
|
|
||||||
tracks = personalizedDiscoveryShuffle;
|
|
||||||
} else if (playlistType === 'familiar_favorites') {
|
|
||||||
tracks = personalizedFamiliarFavorites;
|
|
||||||
} else if (playlistType === 'build_playlist') {
|
|
||||||
tracks = buildPlaylistTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (convert underscores to hyphens for ID)
|
|
||||||
const statusId = playlistType.replace(/_/g, '-') + '-sync-status';
|
|
||||||
const statusDisplay = document.getElementById(statusId);
|
|
||||||
if (statusDisplay) {
|
|
||||||
statusDisplay.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable sync button to prevent duplicate syncs (convert underscores to hyphens for ID)
|
|
||||||
const buttonId = playlistType.replace(/_/g, '-') + '-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);
|
|
||||||
|
|
||||||
// Extract image URL from first track for download bar bubble
|
|
||||||
let imageUrl = null;
|
|
||||||
if (spotifyTracks && spotifyTracks.length > 0) {
|
|
||||||
const firstTrack = spotifyTracks[0];
|
|
||||||
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
|
||||||
imageUrl = firstTrack.album.images[0].url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to discover download bar
|
|
||||||
addDiscoverDownload(virtualPlaylistId, playlistName, playlistType, imageUrl);
|
|
||||||
|
|
||||||
// Start polling for progress updates
|
|
||||||
startDiscoverSyncPolling(playlistType, virtualPlaylistId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track active discover sync pollers
|
// Track active discover sync pollers
|
||||||
|
|
@ -9147,6 +9048,13 @@ function renderDiscoverSyncCard(playlist, container, sourceLabel) {
|
||||||
<span class="discover-sync-toggle-slider"></span>
|
<span class="discover-sync-toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="discover-sync-toggle-wrapper" title="${isEmpty ? '' : 'Skip quality filters and download any available source'}">
|
||||||
|
<label class="discover-sync-toggle-label">Force DL</label>
|
||||||
|
<label class="discover-sync-toggle">
|
||||||
|
<input type="checkbox" id="discover-force-dl-${playlist.type}" ${isEmpty ? 'disabled' : ''}>
|
||||||
|
<span class="discover-sync-toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<button class="discover-sync-btn" id="discover-sync-btn-${playlist.type}"
|
<button class="discover-sync-btn" id="discover-sync-btn-${playlist.type}"
|
||||||
onclick="syncDiscoverPlaylistFromTab('${playlist.type}', '${playlist.name}')"
|
onclick="syncDiscoverPlaylistFromTab('${playlist.type}', '${playlist.name}')"
|
||||||
${playlist.sync_status === 'syncing' || isEmpty ? 'disabled' : ''}>
|
${playlist.sync_status === 'syncing' || isEmpty ? 'disabled' : ''}>
|
||||||
|
|
@ -9190,10 +9098,15 @@ async function toggleDiscoverAutoUpdate(playlistType, enabled) {
|
||||||
const _discoverSyncQueue = [];
|
const _discoverSyncQueue = [];
|
||||||
let _discoverSyncRunning = false;
|
let _discoverSyncRunning = false;
|
||||||
|
|
||||||
async function syncDiscoverPlaylistFromTab(playlistType, playlistName) {
|
async function syncDiscoverPlaylistFromTab(playlistType, playlistName, forceDownload) {
|
||||||
|
// Read force-download toggle if not explicitly passed
|
||||||
|
if (forceDownload === undefined) {
|
||||||
|
const fdToggle = document.getElementById(`discover-force-dl-${playlistType}`);
|
||||||
|
forceDownload = fdToggle ? fdToggle.checked : false;
|
||||||
|
}
|
||||||
// Serialize sync operations to avoid concurrent backend contention
|
// Serialize sync operations to avoid concurrent backend contention
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
_discoverSyncQueue.push({ playlistType, playlistName, resolve });
|
_discoverSyncQueue.push({ playlistType, playlistName, forceDownload, resolve });
|
||||||
_processDiscoverSyncQueue();
|
_processDiscoverSyncQueue();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -9201,9 +9114,9 @@ async function syncDiscoverPlaylistFromTab(playlistType, playlistName) {
|
||||||
async function _processDiscoverSyncQueue() {
|
async function _processDiscoverSyncQueue() {
|
||||||
if (_discoverSyncRunning || _discoverSyncQueue.length === 0) return;
|
if (_discoverSyncRunning || _discoverSyncQueue.length === 0) return;
|
||||||
_discoverSyncRunning = true;
|
_discoverSyncRunning = true;
|
||||||
const { playlistType, playlistName, resolve } = _discoverSyncQueue.shift();
|
const { playlistType, playlistName, forceDownload, resolve } = _discoverSyncQueue.shift();
|
||||||
try {
|
try {
|
||||||
await _doSyncDiscoverPlaylist(playlistType, playlistName);
|
await _doSyncDiscoverPlaylist(playlistType, playlistName, forceDownload);
|
||||||
} finally {
|
} finally {
|
||||||
_discoverSyncRunning = false;
|
_discoverSyncRunning = false;
|
||||||
resolve();
|
resolve();
|
||||||
|
|
@ -9211,7 +9124,7 @@ async function _processDiscoverSyncQueue() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _doSyncDiscoverPlaylist(playlistType, playlistName) {
|
async function _doSyncDiscoverPlaylist(playlistType, playlistName, forceDownload) {
|
||||||
const btn = document.getElementById(`discover-sync-btn-${playlistType}`);
|
const btn = document.getElementById(`discover-sync-btn-${playlistType}`);
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
@ -9261,19 +9174,22 @@ async function _doSyncDiscoverPlaylist(playlistType, playlistName) {
|
||||||
|
|
||||||
// Use the download batch endpoint directly so the batch is labeled
|
// Use the download batch endpoint directly so the batch is labeled
|
||||||
// as "Discover" instead of going through sync → wishlist → "Wishlist" batch.
|
// as "Discover" instead of going through sync → wishlist → "Wishlist" batch.
|
||||||
// Omit force_download_all so it checks the library first and only downloads missing tracks.
|
const bodyPayload = {
|
||||||
|
tracks: syncTracks,
|
||||||
|
playlist_name: playlistName
|
||||||
|
};
|
||||||
|
if (forceDownload) bodyPayload.force_download_all = true;
|
||||||
|
|
||||||
const batchResponse = await fetch(`/api/playlists/${virtualPlaylistId}/start-missing-process`, {
|
const batchResponse = await fetch(`/api/playlists/${virtualPlaylistId}/start-missing-process`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(bodyPayload)
|
||||||
tracks: syncTracks,
|
|
||||||
playlist_name: playlistName
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await batchResponse.json();
|
const result = await batchResponse.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(`Downloading ${playlistName} (${syncTracks.length} tracks)...`, 'info');
|
const forceLabel = forceDownload ? ' (force download)' : '';
|
||||||
|
showToast(`Downloading ${playlistName}${forceLabel} (${syncTracks.length} tracks)...`, 'info');
|
||||||
const card = document.getElementById(`discover-sync-card-${playlistType}`);
|
const card = document.getElementById(`discover-sync-card-${playlistType}`);
|
||||||
if (card) {
|
if (card) {
|
||||||
const statusEl = card.querySelector('.discover-sync-status');
|
const statusEl = card.querySelector('.discover-sync-status');
|
||||||
|
|
|
||||||
|
|
@ -2141,7 +2141,13 @@ function initializeDownloadManagerToggle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToPage(pageId, options = {}) {
|
function navigateToPage(pageId, options = {}) {
|
||||||
if (pageId === currentPage) return;
|
if (pageId === currentPage) {
|
||||||
|
// Already on this page — still process pending sync tab actions
|
||||||
|
if (pageId === 'sync' && window._pendingSyncTabAction && typeof _applySyncTabAction === 'function') {
|
||||||
|
_applySyncTabAction();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Permission guard — redirect to home page if not allowed
|
// Permission guard — redirect to home page if not allowed
|
||||||
if (!isPageAllowed(pageId)) {
|
if (!isPageAllowed(pageId)) {
|
||||||
|
|
@ -2235,6 +2241,10 @@ async function loadPageData(pageId) {
|
||||||
case 'sync':
|
case 'sync':
|
||||||
initializeSyncPage();
|
initializeSyncPage();
|
||||||
await loadSyncData();
|
await loadSyncData();
|
||||||
|
// Process any pending deep-link tab switch (e.g. from Discover page)
|
||||||
|
if (window._pendingSyncTabAction && typeof _applySyncTabAction === 'function') {
|
||||||
|
_applySyncTabAction();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'downloads':
|
case 'downloads':
|
||||||
initializeSearch();
|
initializeSearch();
|
||||||
|
|
|
||||||
|
|
@ -59541,6 +59541,16 @@ body.reduce-effects *::after {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Deep-link highlight animation */
|
||||||
|
.discover-sync-card-highlight {
|
||||||
|
animation: discover-card-glow 2.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes discover-card-glow {
|
||||||
|
0% { border-color: rgba(167, 139, 250, 0.7); box-shadow: 0 0 20px rgba(167, 139, 250, 0.3); }
|
||||||
|
100% { border-color: rgba(255, 255, 255, 0.08); box-shadow: none; }
|
||||||
|
}
|
||||||
|
|
||||||
.discover-sync-empty-hint {
|
.discover-sync-empty-hint {
|
||||||
background: rgba(255, 193, 7, 0.08);
|
background: rgba(255, 193, 7, 0.08);
|
||||||
border: 1px solid rgba(255, 193, 7, 0.25);
|
border: 1px solid rgba(255, 193, 7, 0.25);
|
||||||
|
|
|
||||||
|
|
@ -2736,6 +2736,54 @@ async function startDeezerDownloadMissing(urlHash) {
|
||||||
// SYNC PAGE FUNCTIONALITY (REDESIGNED)
|
// SYNC PAGE FUNCTIONALITY (REDESIGNED)
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Sync page and activate a specific tab.
|
||||||
|
* Works from any page. If already on the sync page, just switches the tab.
|
||||||
|
* @param {string} tabId - Tab data-tab value (e.g. 'discover', 'spotify', 'mirrored')
|
||||||
|
* @param {object} [opts] - Options
|
||||||
|
* @param {string} [opts.highlight] - Element ID to scroll to and briefly highlight
|
||||||
|
* @param {string} [opts.autoSync] - Discover playlist type to auto-trigger sync on
|
||||||
|
* @param {boolean} [opts.forceDownload] - Pass force_download_all when auto-syncing
|
||||||
|
*/
|
||||||
|
function navigateToSyncTab(tabId, opts) {
|
||||||
|
window._pendingSyncTabAction = { tabId, ...(opts || {}) };
|
||||||
|
if (typeof currentPage !== 'undefined' && currentPage === 'sync') {
|
||||||
|
_applySyncTabAction();
|
||||||
|
} else {
|
||||||
|
navigateToPage('sync');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _applySyncTabAction() {
|
||||||
|
const action = window._pendingSyncTabAction;
|
||||||
|
if (!action) return;
|
||||||
|
window._pendingSyncTabAction = null;
|
||||||
|
const tabId = action.tabId;
|
||||||
|
|
||||||
|
// Click the target tab button to trigger normal tab-switch logic
|
||||||
|
const btn = document.querySelector(`.sync-tab-button[data-tab="${tabId}"]`);
|
||||||
|
if (btn && !btn.classList.contains('active')) {
|
||||||
|
btn.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for lazy-loaded content, then highlight / auto-sync
|
||||||
|
const apply = () => {
|
||||||
|
if (action.highlight) {
|
||||||
|
const el = document.getElementById(action.highlight);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
el.classList.add('discover-sync-card-highlight');
|
||||||
|
setTimeout(() => el.classList.remove('discover-sync-card-highlight'), 2500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action.autoSync) {
|
||||||
|
syncDiscoverPlaylistFromTab(action.autoSync, action.autoSyncName || action.autoSync, action.forceDownload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Small delay to let lazy tab content render
|
||||||
|
setTimeout(apply, 400);
|
||||||
|
}
|
||||||
|
|
||||||
function initializeSyncPage() {
|
function initializeSyncPage() {
|
||||||
// Logic for tab switching
|
// Logic for tab switching
|
||||||
const tabButtons = document.querySelectorAll('.sync-tab-button');
|
const tabButtons = document.querySelectorAll('.sync-tab-button');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue