Files
spotizerr-dev/src/js/playlist.ts
2025-06-07 18:47:18 +02:00

865 lines
32 KiB
TypeScript

// Import the downloadQueue singleton from your working queue.js implementation.
import { downloadQueue } from './queue.js';
// Define interfaces for API data
interface Image {
url: string;
height?: number;
width?: number;
}
interface Artist {
id: string;
name: string;
external_urls?: { spotify?: string };
}
interface Album {
id: string;
name: string;
images?: Image[];
external_urls?: { spotify?: string };
}
interface Track {
id: string;
name: string;
artists: Artist[];
album: Album;
duration_ms: number;
explicit: boolean;
external_urls?: { spotify?: string };
is_locally_known?: boolean; // Added for local DB status
}
interface PlaylistItem {
track: Track | null;
// Add other playlist item properties like added_at, added_by if needed
}
interface Playlist {
id: string;
name: string;
description: string | null;
owner: {
display_name?: string;
id?: string;
};
images: Image[];
tracks: {
items: PlaylistItem[];
total: number;
};
followers?: {
total: number;
};
external_urls?: { spotify?: string };
}
interface WatchedPlaylistStatus {
is_watched: boolean;
playlist_data?: Playlist; // Optional, present if watched
}
// Added: Interface for global watch config
interface GlobalWatchConfig {
enabled: boolean;
[key: string]: any;
}
interface DownloadQueueItem {
name: string;
artist?: string; // Can be a simple string for the queue
album?: { name: string }; // Match QueueItem's album structure
owner?: string; // For playlists, owner can be a string
// Add any other properties your item might have, compatible with QueueItem
}
// Added: Helper function to fetch global watch config
async function getGlobalWatchConfig(): Promise<GlobalWatchConfig> {
try {
const response = await fetch('/api/config/watch');
if (!response.ok) {
console.error('Failed to fetch global watch config, assuming disabled.');
return { enabled: false }; // Default to disabled on error
}
return await response.json() as GlobalWatchConfig;
} catch (error) {
console.error('Error fetching global watch config:', error);
return { enabled: false }; // Default to disabled on error
}
}
document.addEventListener('DOMContentLoaded', async () => {
// Parse playlist ID from URL
const pathSegments = window.location.pathname.split('/');
const playlistId = pathSegments[pathSegments.indexOf('playlist') + 1];
if (!playlistId) {
showError('No playlist ID provided.');
return;
}
const globalWatchConfig = await getGlobalWatchConfig(); // Fetch global config
const isGlobalWatchActuallyEnabled = globalWatchConfig.enabled;
// Fetch playlist info directly
fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json() as Promise<Playlist>;
})
.then(data => renderPlaylist(data, isGlobalWatchActuallyEnabled))
.catch(error => {
console.error('Error:', error);
showError('Failed to load playlist.');
});
// Fetch initial watch status for the specific playlist
if (isGlobalWatchActuallyEnabled) {
fetchWatchStatus(playlistId); // This function then calls updateWatchButtons
} else {
// If global watch is disabled, ensure watch-related buttons are hidden/disabled
const watchBtn = document.getElementById('watchPlaylistBtn') as HTMLButtonElement;
const syncBtn = document.getElementById('syncPlaylistBtn') as HTMLButtonElement;
if (watchBtn) {
watchBtn.classList.add('hidden');
watchBtn.disabled = true;
// Remove any existing event listener to prevent actions
watchBtn.onclick = null;
}
if (syncBtn) {
syncBtn.classList.add('hidden');
syncBtn.disabled = true;
syncBtn.onclick = null;
}
}
const queueIcon = document.getElementById('queueIcon');
if (queueIcon) {
queueIcon.addEventListener('click', () => {
downloadQueue.toggleVisibility();
});
}
// Attempt to set initial watchlist button visibility from cache
const watchlistButton = document.getElementById('watchlistButton') as HTMLAnchorElement | null;
if (watchlistButton) {
const cachedWatchEnabled = localStorage.getItem('spotizerr_watch_enabled_cached');
if (cachedWatchEnabled === 'true') {
watchlistButton.classList.remove('hidden');
}
}
// Fetch watch config to determine if watchlist button should be visible
async function updateWatchlistButtonVisibility() {
if (watchlistButton) {
try {
const response = await fetch('/api/config/watch');
if (response.ok) {
const watchConfig = await response.json();
localStorage.setItem('spotizerr_watch_enabled_cached', watchConfig.enabled ? 'true' : 'false');
if (watchConfig && watchConfig.enabled === false) {
watchlistButton.classList.add('hidden');
} else {
watchlistButton.classList.remove('hidden'); // Ensure it's shown if enabled
}
} else {
console.error('Failed to fetch watch config for playlist page, defaulting to hidden');
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide if config fetch fails
}
} catch (error) {
console.error('Error fetching watch config for playlist page:', error);
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide on error
}
}
}
updateWatchlistButtonVisibility();
});
/**
* Renders playlist header and tracks.
*/
function renderPlaylist(playlist: Playlist, isGlobalWatchEnabled: boolean) {
// Hide loading and error messages
const loadingEl = document.getElementById('loading');
if (loadingEl) loadingEl.classList.add('hidden');
const errorEl = document.getElementById('error');
if (errorEl) errorEl.classList.add('hidden');
// Check if explicit filter is enabled
const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled();
// Update header info
const playlistNameEl = document.getElementById('playlist-name');
if (playlistNameEl) playlistNameEl.textContent = playlist.name || 'Unknown Playlist';
const playlistOwnerEl = document.getElementById('playlist-owner');
if (playlistOwnerEl) playlistOwnerEl.textContent = `By ${playlist.owner?.display_name || 'Unknown User'}`;
const playlistStatsEl = document.getElementById('playlist-stats');
if (playlistStatsEl) playlistStatsEl.textContent =
`${playlist.followers?.total || '0'} followers • ${playlist.tracks?.total || '0'} songs`;
const playlistDescriptionEl = document.getElementById('playlist-description');
if (playlistDescriptionEl) playlistDescriptionEl.textContent = playlist.description || '';
const image = playlist.images?.[0]?.url || '/static/images/placeholder.jpg';
const playlistImageEl = document.getElementById('playlist-image') as HTMLImageElement;
if (playlistImageEl) playlistImageEl.src = image;
// --- Add Home Button ---
let homeButton = document.getElementById('homeButton') as HTMLButtonElement;
if (!homeButton) {
homeButton = document.createElement('button');
homeButton.id = 'homeButton';
homeButton.className = 'home-btn';
// Use an <img> tag to display the SVG icon.
homeButton.innerHTML = `<img src="/static/images/home.svg" alt="Home">`;
// Insert the home button at the beginning of the header container.
const headerContainer = document.getElementById('playlist-header');
if (headerContainer) {
headerContainer.insertBefore(homeButton, headerContainer.firstChild);
}
}
homeButton.addEventListener('click', () => {
// Navigate to the site's base URL.
window.location.href = window.location.origin;
});
// Check if any track in the playlist is explicit when filter is enabled
let hasExplicitTrack = false;
if (isExplicitFilterEnabled && playlist.tracks?.items) {
hasExplicitTrack = playlist.tracks.items.some((item: PlaylistItem) => item?.track && item.track.explicit);
}
// --- Add "Download Whole Playlist" Button ---
let downloadPlaylistBtn = document.getElementById('downloadPlaylistBtn') as HTMLButtonElement;
if (!downloadPlaylistBtn) {
downloadPlaylistBtn = document.createElement('button');
downloadPlaylistBtn.id = 'downloadPlaylistBtn';
downloadPlaylistBtn.textContent = 'Download Whole Playlist';
downloadPlaylistBtn.className = 'download-btn download-btn--main';
// Insert the button into the header container.
const headerContainer = document.getElementById('playlist-header');
if (headerContainer) {
headerContainer.appendChild(downloadPlaylistBtn);
}
}
// --- Add "Download Playlist's Albums" Button ---
let downloadAlbumsBtn = document.getElementById('downloadAlbumsBtn') as HTMLButtonElement;
if (!downloadAlbumsBtn) {
downloadAlbumsBtn = document.createElement('button');
downloadAlbumsBtn.id = 'downloadAlbumsBtn';
downloadAlbumsBtn.textContent = "Download Playlist's Albums";
downloadAlbumsBtn.className = 'download-btn download-btn--main';
// Insert the new button into the header container.
const headerContainer = document.getElementById('playlist-header');
if (headerContainer) {
headerContainer.appendChild(downloadAlbumsBtn);
}
}
if (isExplicitFilterEnabled && hasExplicitTrack) {
// Disable both playlist buttons and display messages explaining why
if (downloadPlaylistBtn) {
downloadPlaylistBtn.disabled = true;
downloadPlaylistBtn.classList.add('download-btn--disabled');
downloadPlaylistBtn.innerHTML = `<span title="Cannot download entire playlist because it contains explicit tracks">Playlist Contains Explicit Tracks</span>`;
}
if (downloadAlbumsBtn) {
downloadAlbumsBtn.disabled = true;
downloadAlbumsBtn.classList.add('download-btn--disabled');
downloadAlbumsBtn.innerHTML = `<span title="Cannot download albums from this playlist because it contains explicit tracks">Albums Access Restricted</span>`;
}
} else {
// Normal behavior when no explicit tracks are present
if (downloadPlaylistBtn) {
downloadPlaylistBtn.addEventListener('click', () => {
// Remove individual track download buttons (but leave the whole playlist button).
document.querySelectorAll('.download-btn').forEach(btn => {
if (btn.id !== 'downloadPlaylistBtn') {
btn.remove();
}
});
// Disable the whole playlist button to prevent repeated clicks.
downloadPlaylistBtn.disabled = true;
downloadPlaylistBtn.textContent = 'Queueing...';
// Initiate the playlist download.
downloadWholePlaylist(playlist).then(() => {
downloadPlaylistBtn.textContent = 'Queued!';
}).catch((err: any) => {
showError('Failed to queue playlist download: ' + (err?.message || 'Unknown error'));
if (downloadPlaylistBtn) downloadPlaylistBtn.disabled = false; // Re-enable on error
});
});
}
if (downloadAlbumsBtn) {
downloadAlbumsBtn.addEventListener('click', () => {
// Remove individual track download buttons (but leave this album button).
document.querySelectorAll('.download-btn').forEach(btn => {
if (btn.id !== 'downloadAlbumsBtn') btn.remove();
});
downloadAlbumsBtn.disabled = true;
downloadAlbumsBtn.textContent = 'Queueing...';
downloadPlaylistAlbums(playlist)
.then(() => {
if (downloadAlbumsBtn) downloadAlbumsBtn.textContent = 'Queued!';
})
.catch((err: any) => {
showError('Failed to queue album downloads: ' + (err?.message || 'Unknown error'));
if (downloadAlbumsBtn) downloadAlbumsBtn.disabled = false; // Re-enable on error
});
});
}
}
// Render tracks list
const tracksList = document.getElementById('tracks-list');
if (!tracksList) return;
tracksList.innerHTML = ''; // Clear any existing content
// Determine if the playlist is being watched to show/hide management buttons
const watchPlaylistButton = document.getElementById('watchPlaylistBtn') as HTMLButtonElement;
// isIndividuallyWatched checks if the button is visible and has the 'watching' class.
// This implies global watch is enabled if the button is even interactable for individual status.
const isIndividuallyWatched = watchPlaylistButton &&
watchPlaylistButton.classList.contains('watching') &&
!watchPlaylistButton.classList.contains('hidden');
if (playlist.tracks?.items) {
playlist.tracks.items.forEach((item: PlaylistItem, index: number) => {
if (!item || !item.track) return; // Skip null/undefined tracks
const track = item.track;
// Skip explicit tracks if filter is enabled
if (isExplicitFilterEnabled && track.explicit) {
// Add a placeholder for filtered explicit tracks
const trackElement = document.createElement('div');
trackElement.className = 'track track-filtered';
trackElement.innerHTML = `
<div class="track-number">${index + 1}</div>
<div class="track-info">
<div class="track-name explicit-filtered">Explicit Content Filtered</div>
<div class="track-artist">This track is not shown due to explicit content filter settings</div>
</div>
<div class="track-album">Not available</div>
<div class="track-duration">--:--</div>
`;
tracksList.appendChild(trackElement);
return;
}
const trackLink = `/track/${track.id || ''}`;
const artistLink = `/artist/${track.artists?.[0]?.id || ''}`;
const albumLink = `/album/${track.album?.id || ''}`;
const trackElement = document.createElement('div');
trackElement.className = 'track';
let trackHTML = `
<div class="track-number">${index + 1}</div>
<div class="track-info">
<div class="track-name">
<a href="${trackLink}" title="View track details">${track.name || 'Unknown Track'}</a>
</div>
<div class="track-artist">
<a href="${artistLink}" title="View artist details">${track.artists?.[0]?.name || 'Unknown Artist'}</a>
</div>
</div>
<div class="track-album">
<a href="${albumLink}" title="View album details">${track.album?.name || 'Unknown Album'}</a>
</div>
<div class="track-duration">${msToTime(track.duration_ms || 0)}</div>
`;
const actionsContainer = document.createElement('div');
actionsContainer.className = 'track-actions-container';
if (!(isExplicitFilterEnabled && hasExplicitTrack)) {
const downloadBtnHTML = `
<button class="download-btn download-btn--circle track-download-btn"
data-id="${track.id || ''}"
data-type="track"
data-name="${track.name || 'Unknown Track'}"
title="Download">
<img src="/static/images/download.svg" alt="Download">
</button>
`;
actionsContainer.innerHTML += downloadBtnHTML;
}
if (isGlobalWatchEnabled && isIndividuallyWatched) { // Check global and individual watch status
// Initial state is set based on track.is_locally_known
const isKnown = track.is_locally_known === true; // Ensure boolean check, default to false if undefined
const initialStatus = isKnown ? "known" : "missing";
const initialIcon = isKnown ? "/static/images/check.svg" : "/static/images/missing.svg";
const initialTitle = isKnown ? "Click to mark as missing from DB" : "Click to mark as known in DB";
const toggleKnownBtnHTML = `
<button class="action-btn toggle-known-status-btn"
data-id="${track.id || ''}"
data-playlist-id="${playlist.id || ''}"
data-status="${initialStatus}"
title="${initialTitle}">
<img src="${initialIcon}" alt="Mark as Missing/Known">
</button>
`;
actionsContainer.innerHTML += toggleKnownBtnHTML;
}
trackElement.innerHTML = trackHTML;
trackElement.appendChild(actionsContainer);
tracksList.appendChild(trackElement);
});
}
// Reveal header and tracks container
const playlistHeaderEl = document.getElementById('playlist-header');
if (playlistHeaderEl) playlistHeaderEl.classList.remove('hidden');
const tracksContainerEl = document.getElementById('tracks-container');
if (tracksContainerEl) tracksContainerEl.classList.remove('hidden');
// Attach download listeners to newly rendered download buttons
attachTrackActionListeners(isGlobalWatchEnabled);
}
/**
* Converts milliseconds to minutes:seconds.
*/
function msToTime(duration: number) {
if (!duration || isNaN(duration)) return '0:00';
const minutes = Math.floor(duration / 60000);
const seconds = ((duration % 60000) / 1000).toFixed(0);
return `${minutes}:${seconds.padStart(2, '0')}`;
}
/**
* Displays an error message in the UI.
*/
function showError(message: string) {
const errorEl = document.getElementById('error');
if (errorEl) {
errorEl.textContent = message || 'An error occurred';
errorEl.classList.remove('hidden');
}
}
/**
* Attaches event listeners to all individual track action buttons (download, mark known, mark missing).
*/
function attachTrackActionListeners(isGlobalWatchEnabled: boolean) {
document.querySelectorAll('.track-download-btn').forEach((btn) => {
btn.addEventListener('click', (e: Event) => {
e.stopPropagation();
const currentTarget = e.currentTarget as HTMLButtonElement;
const itemId = currentTarget.dataset.id || '';
const type = currentTarget.dataset.type || 'track';
const name = currentTarget.dataset.name || 'Unknown';
if (!itemId) {
showError('Missing item ID for download on playlist page');
return;
}
currentTarget.remove();
startDownload(itemId, type, { name }, '');
});
});
document.querySelectorAll('.toggle-known-status-btn').forEach((btn) => {
btn.addEventListener('click', async (e: Event) => {
e.stopPropagation();
const button = e.currentTarget as HTMLButtonElement;
const trackId = button.dataset.id || '';
const playlistId = button.dataset.playlistId || '';
const currentStatus = button.dataset.status;
const img = button.querySelector('img');
if (!trackId || !playlistId || !img) {
showError('Missing data for toggling track status');
return;
}
if (!isGlobalWatchEnabled) { // Added check
showNotification("Watch feature is currently disabled globally. Cannot change track status.");
return;
}
button.disabled = true;
try {
if (currentStatus === 'missing') {
await handleMarkTrackAsKnown(playlistId, trackId);
button.dataset.status = 'known';
img.src = '/static/images/check.svg';
button.title = 'Click to mark as missing from DB';
} else {
await handleMarkTrackAsMissing(playlistId, trackId);
button.dataset.status = 'missing';
img.src = '/static/images/missing.svg';
button.title = 'Click to mark as known in DB';
}
} catch (error) {
// Revert UI on error if needed, error is shown by handlers
showError('Failed to update track status. Please try again.');
}
button.disabled = false;
});
});
}
async function handleMarkTrackAsKnown(playlistId: string, trackId: string) {
try {
const response = await fetch(`/api/playlist/watch/${playlistId}/tracks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([trackId]),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
showNotification(result.message || 'Track marked as known.');
} catch (error: any) {
showError(`Failed to mark track as known: ${error.message}`);
throw error; // Re-throw for the caller to handle button state if needed
}
}
async function handleMarkTrackAsMissing(playlistId: string, trackId: string) {
try {
const response = await fetch(`/api/playlist/watch/${playlistId}/tracks`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([trackId]),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
showNotification(result.message || 'Track marked as missing.');
} catch (error: any) {
showError(`Failed to mark track as missing: ${error.message}`);
throw error; // Re-throw
}
}
/**
* Initiates the whole playlist download by calling the playlist endpoint.
*/
async function downloadWholePlaylist(playlist: Playlist) {
if (!playlist) {
throw new Error('Invalid playlist data');
}
const playlistId = playlist.id || '';
if (!playlistId) {
throw new Error('Missing playlist ID');
}
try {
// Use the centralized downloadQueue.download method
await downloadQueue.download(playlistId, 'playlist', {
name: playlist.name || 'Unknown Playlist',
owner: playlist.owner?.display_name // Pass owner as a string
// total_tracks can also be passed if QueueItem supports it directly
});
// Make the queue visible after queueing
downloadQueue.toggleVisibility(true);
} catch (error: any) {
showError('Playlist download failed: ' + (error?.message || 'Unknown error'));
throw error;
}
}
/**
* Initiates album downloads for each unique album in the playlist,
* adding a 20ms delay between each album download and updating the button
* with the progress (queued_albums/total_albums).
*/
async function downloadPlaylistAlbums(playlist: Playlist) {
if (!playlist?.tracks?.items) {
showError('No tracks found in this playlist.');
return;
}
// Build a map of unique albums (using album ID as the key).
const albumMap = new Map<string, Album>();
playlist.tracks.items.forEach((item: PlaylistItem) => {
if (!item?.track?.album) return;
const album = item.track.album;
if (album && album.id) {
albumMap.set(album.id, album);
}
});
const uniqueAlbums = Array.from(albumMap.values());
const totalAlbums = uniqueAlbums.length;
if (totalAlbums === 0) {
showError('No albums found in this playlist.');
return;
}
// Get a reference to the "Download Playlist's Albums" button.
const downloadAlbumsBtn = document.getElementById('downloadAlbumsBtn') as HTMLButtonElement | null;
if (downloadAlbumsBtn) {
// Initialize the progress display.
downloadAlbumsBtn.textContent = `0/${totalAlbums}`;
}
try {
// Process each album sequentially.
for (let i = 0; i < totalAlbums; i++) {
const album = uniqueAlbums[i];
if (!album) continue;
const albumUrl = album.external_urls?.spotify || '';
if (!albumUrl) continue;
// Use the centralized downloadQueue.download method
await downloadQueue.download(
album.id, // Pass album ID directly
'album',
{
name: album.name || 'Unknown Album',
// If artist information is available on album objects from playlist, pass it
// artist: album.artists?.[0]?.name
}
);
// Update button text with current progress.
if (downloadAlbumsBtn) {
downloadAlbumsBtn.textContent = `${i + 1}/${totalAlbums}`;
}
// Wait 20 milliseconds before processing the next album.
await new Promise(resolve => setTimeout(resolve, 20));
}
// Once all albums have been queued, update the button text.
if (downloadAlbumsBtn) {
downloadAlbumsBtn.textContent = 'Queued!';
}
// Make the queue visible after queueing all albums
downloadQueue.toggleVisibility(true);
} catch (error: any) {
// Propagate any errors encountered.
throw error;
}
}
/**
* Starts the download process using the centralized download method from the queue.
*/
async function startDownload(itemId: string, type: string, item: DownloadQueueItem, albumType?: string) {
if (!itemId || !type) {
showError('Missing ID or type for download');
return;
}
try {
// Use the centralized downloadQueue.download method
await downloadQueue.download(itemId, type, item, albumType);
// Make the queue visible after queueing
downloadQueue.toggleVisibility(true);
} catch (error: any) {
showError('Download failed: ' + (error?.message || 'Unknown error'));
throw error;
}
}
/**
* A helper function to extract a display name from the URL.
*/
function extractName(url: string | null): string {
return url || 'Unknown';
}
/**
* Fetches the watch status of the current playlist and updates the UI.
*/
async function fetchWatchStatus(playlistId: string) {
if (!playlistId) return;
try {
const response = await fetch(`/api/playlist/watch/${playlistId}/status`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch watch status');
}
const data: WatchedPlaylistStatus = await response.json();
updateWatchButtons(data.is_watched, playlistId);
} catch (error) {
console.error('Error fetching watch status:', error);
// Don't show a blocking error, but maybe a small notification or log
// For now, assume not watched if status fetch fails, or keep buttons in default state
updateWatchButtons(false, playlistId);
}
}
/**
* Updates the Watch/Unwatch and Sync buttons based on the playlist's watch status.
*/
function updateWatchButtons(isWatched: boolean, playlistId: string) {
const watchBtn = document.getElementById('watchPlaylistBtn') as HTMLButtonElement;
const syncBtn = document.getElementById('syncPlaylistBtn') as HTMLButtonElement;
if (!watchBtn || !syncBtn) return;
const watchBtnImg = watchBtn.querySelector('img');
if (isWatched) {
watchBtn.innerHTML = `<img src="/static/images/eye-crossed.svg" alt="Unwatch"> Unwatch Playlist`;
watchBtn.classList.add('watching');
watchBtn.onclick = () => unwatchPlaylist(playlistId);
syncBtn.classList.remove('hidden');
syncBtn.onclick = () => syncPlaylist(playlistId);
} else {
watchBtn.innerHTML = `<img src="/static/images/eye.svg" alt="Watch"> Watch Playlist`;
watchBtn.classList.remove('watching');
watchBtn.onclick = () => watchPlaylist(playlistId);
syncBtn.classList.add('hidden');
}
watchBtn.disabled = false; // Enable after status is known
}
/**
* Adds the current playlist to the watchlist.
*/
async function watchPlaylist(playlistId: string) {
const watchBtn = document.getElementById('watchPlaylistBtn') as HTMLButtonElement;
if (watchBtn) watchBtn.disabled = true;
// This function should only be callable if global watch is enabled.
// We can add a check here or rely on the UI not presenting the button.
// For safety, let's check global config again before proceeding.
const globalConfig = await getGlobalWatchConfig();
if (!globalConfig.enabled) {
showError("Cannot watch playlist, feature is disabled globally.");
if (watchBtn) {
watchBtn.disabled = false; // Re-enable if it was somehow clicked
updateWatchButtons(false, playlistId); // Reset button to non-watching state
}
return;
}
try {
const response = await fetch(`/api/playlist/watch/${playlistId}`, { method: 'PUT' });
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to watch playlist');
}
updateWatchButtons(true, playlistId);
// Re-fetch and re-render playlist data
const newPlaylistInfoResponse = await fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`);
if (!newPlaylistInfoResponse.ok) throw new Error('Failed to re-fetch playlist info after watch.');
const newPlaylistData = await newPlaylistInfoResponse.json() as Playlist;
renderPlaylist(newPlaylistData, globalConfig.enabled); // Pass current global enabled state
showNotification(`Playlist added to watchlist. Tracks are being updated.`);
} catch (error: any) {
showError(`Error watching playlist: ${error.message}`);
if (watchBtn) watchBtn.disabled = false; // Re-enable on error before potential UI revert
}
}
/**
* Removes the current playlist from the watchlist.
*/
async function unwatchPlaylist(playlistId: string) {
const watchBtn = document.getElementById('watchPlaylistBtn') as HTMLButtonElement;
if (watchBtn) watchBtn.disabled = true;
// Similarly, check global config
const globalConfig = await getGlobalWatchConfig();
if (!globalConfig.enabled) {
// This case should be rare if UI behaves, but good for robustness
showError("Cannot unwatch playlist, feature is disabled globally.");
if (watchBtn) {
watchBtn.disabled = false;
// updateWatchButtons(true, playlistId); // Or keep as is if it was 'watching'
}
return;
}
try {
const response = await fetch(`/api/playlist/watch/${playlistId}`, { method: 'DELETE' });
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to unwatch playlist');
}
updateWatchButtons(false, playlistId);
// Re-fetch and re-render playlist data
const newPlaylistInfoResponse = await fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`);
if (!newPlaylistInfoResponse.ok) throw new Error('Failed to re-fetch playlist info after unwatch.');
const newPlaylistData = await newPlaylistInfoResponse.json() as Playlist;
renderPlaylist(newPlaylistData, globalConfig.enabled); // Pass current global enabled state
showNotification('Playlist removed from watchlist. Track statuses updated.');
} catch (error: any) {
showError(`Error unwatching playlist: ${error.message}`);
if (watchBtn) watchBtn.disabled = false; // Re-enable on error before potential UI revert
}
}
/**
* Triggers a manual sync for the watched playlist.
*/
async function syncPlaylist(playlistId: string) {
const syncBtn = document.getElementById('syncPlaylistBtn') as HTMLButtonElement;
let originalButtonContent = ''; // Define outside
// Check global config
const globalConfig = await getGlobalWatchConfig();
if (!globalConfig.enabled) {
showError("Cannot sync playlist, feature is disabled globally.");
return;
}
if (syncBtn) {
syncBtn.disabled = true;
originalButtonContent = syncBtn.innerHTML; // Store full HTML
syncBtn.innerHTML = `<img src="/static/images/refresh.svg" alt="Sync"> Syncing...`; // Keep icon
}
try {
const response = await fetch(`/api/playlist/watch/trigger_check/${playlistId}`, { method: 'POST' });
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to trigger sync');
}
showNotification('Playlist sync triggered successfully.');
} catch (error: any) {
showError(`Error triggering sync: ${error.message}`);
} finally {
if (syncBtn) {
syncBtn.disabled = false;
syncBtn.innerHTML = originalButtonContent; // Restore full original HTML
}
}
}
/**
* Displays a temporary notification message.
*/
function showNotification(message: string) {
// Basic notification - consider a more robust solution for production
const notificationEl = document.createElement('div');
notificationEl.className = 'notification';
notificationEl.textContent = message;
document.body.appendChild(notificationEl);
setTimeout(() => {
notificationEl.remove();
}, 3000);
}