This commit is contained in:
Xoconoch
2025-06-03 14:02:04 -06:00
parent 7bd11bdaa4
commit 0a1c66b02a
17 changed files with 486 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
waitress
celery
flask
flask_cors
deezspot-spotizerr
waitress==3.0.2
celery==5.5.3
Flask==3.1.1
flask_cors==6.0.0
deezspot-spotizerr==1.1

View File

@@ -22,7 +22,7 @@ from routes.utils.watch.db import (
remove_specific_albums_from_artist_table,
is_album_in_artist_db
)
from routes.utils.watch.manager import check_watched_artists
from routes.utils.watch.manager import check_watched_artists, get_watch_config
from routes.utils.get_info import get_spotify_info
artist_bp = Blueprint('artist', __name__, url_prefix='/api/artist')
@@ -159,6 +159,10 @@ def get_artist_info():
@artist_bp.route('/watch/<string:artist_spotify_id>', methods=['PUT'])
def add_artist_to_watchlist(artist_spotify_id):
"""Adds an artist to the watchlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally."}), 403
logger.info(f"Attempting to add artist {artist_spotify_id} to watchlist.")
try:
if get_watched_artist(artist_spotify_id):
@@ -224,6 +228,10 @@ def get_artist_watch_status(artist_spotify_id):
@artist_bp.route('/watch/<string:artist_spotify_id>', methods=['DELETE'])
def remove_artist_from_watchlist(artist_spotify_id):
"""Removes an artist from the watchlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally."}), 403
logger.info(f"Attempting to remove artist {artist_spotify_id} from watchlist.")
try:
if not get_watched_artist(artist_spotify_id):
@@ -249,6 +257,10 @@ def list_watched_artists_endpoint():
@artist_bp.route('/watch/trigger_check', methods=['POST'])
def trigger_artist_check_endpoint():
"""Manually triggers the artist checking mechanism for all watched artists."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot trigger check."}), 403
logger.info("Manual trigger for artist check received for all artists.")
try:
thread = threading.Thread(target=check_watched_artists, args=(None,))
@@ -261,6 +273,10 @@ def trigger_artist_check_endpoint():
@artist_bp.route('/watch/trigger_check/<string:artist_spotify_id>', methods=['POST'])
def trigger_specific_artist_check_endpoint(artist_spotify_id: str):
"""Manually triggers the artist checking mechanism for a specific artist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot trigger check."}), 403
logger.info(f"Manual trigger for specific artist check received for ID: {artist_spotify_id}")
try:
watched_artist = get_watched_artist(artist_spotify_id)
@@ -279,6 +295,10 @@ def trigger_specific_artist_check_endpoint(artist_spotify_id: str):
@artist_bp.route('/watch/<string:artist_spotify_id>/albums', methods=['POST'])
def mark_albums_as_known_for_artist(artist_spotify_id):
"""Fetches details for given album IDs and adds/updates them in the artist's local DB table."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot mark albums."}), 403
logger.info(f"Attempting to mark albums as known for artist {artist_spotify_id}.")
try:
album_ids = request.json
@@ -313,6 +333,10 @@ def mark_albums_as_known_for_artist(artist_spotify_id):
@artist_bp.route('/watch/<string:artist_spotify_id>/albums', methods=['DELETE'])
def mark_albums_as_missing_locally_for_artist(artist_spotify_id):
"""Removes specified albums from the artist's local DB table."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot mark albums."}), 403
logger.info(f"Attempting to mark albums as missing (delete locally) for artist {artist_spotify_id}.")
try:
album_ids = request.json

View File

@@ -20,7 +20,7 @@ from routes.utils.watch.db import (
is_track_in_playlist_db # Added import
)
from routes.utils.get_info import get_spotify_info # Already used, but ensure it's here
from routes.utils.watch.manager import check_watched_playlists # For manual trigger
from routes.utils.watch.manager import check_watched_playlists, get_watch_config # For manual trigger & config
logger = logging.getLogger(__name__) # Added logger initialization
playlist_bp = Blueprint('playlist', __name__, url_prefix='/api/playlist')
@@ -180,6 +180,10 @@ def get_playlist_info():
@playlist_bp.route('/watch/<string:playlist_spotify_id>', methods=['PUT'])
def add_to_watchlist(playlist_spotify_id):
"""Adds a playlist to the watchlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally."}), 403
logger.info(f"Attempting to add playlist {playlist_spotify_id} to watchlist.")
try:
# Check if already watched
@@ -227,6 +231,10 @@ def get_playlist_watch_status(playlist_spotify_id):
@playlist_bp.route('/watch/<string:playlist_spotify_id>', methods=['DELETE'])
def remove_from_watchlist(playlist_spotify_id):
"""Removes a playlist from the watchlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally."}), 403
logger.info(f"Attempting to remove playlist {playlist_spotify_id} from watchlist.")
try:
if not get_watched_playlist(playlist_spotify_id):
@@ -242,6 +250,10 @@ def remove_from_watchlist(playlist_spotify_id):
@playlist_bp.route('/watch/<string:playlist_spotify_id>/tracks', methods=['POST'])
def mark_tracks_as_known(playlist_spotify_id):
"""Fetches details for given track IDs and adds/updates them in the playlist's local DB table."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot mark tracks."}), 403
logger.info(f"Attempting to mark tracks as known for playlist {playlist_spotify_id}.")
try:
track_ids = request.json
@@ -275,7 +287,11 @@ def mark_tracks_as_known(playlist_spotify_id):
@playlist_bp.route('/watch/<string:playlist_spotify_id>/tracks', methods=['DELETE'])
def mark_tracks_as_missing_locally(playlist_spotify_id):
"""Removes specified tracks from the playlist's local DB table."""
logger.info(f"Attempting to mark tracks as missing (delete locally) for playlist {playlist_spotify_id}.")
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot mark tracks."}), 403
logger.info(f"Attempting to mark tracks as missing (remove locally) for playlist {playlist_spotify_id}.")
try:
track_ids = request.json
if not isinstance(track_ids, list) or not all(isinstance(tid, str) for tid in track_ids):
@@ -304,6 +320,10 @@ def list_watched_playlists_endpoint():
@playlist_bp.route('/watch/trigger_check', methods=['POST'])
def trigger_playlist_check_endpoint():
"""Manually triggers the playlist checking mechanism for all watched playlists."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot trigger check."}), 403
logger.info("Manual trigger for playlist check received for all playlists.")
try:
# Run check_watched_playlists without an ID to check all
@@ -317,6 +337,10 @@ def trigger_playlist_check_endpoint():
@playlist_bp.route('/watch/trigger_check/<string:playlist_spotify_id>', methods=['POST'])
def trigger_specific_playlist_check_endpoint(playlist_spotify_id: str):
"""Manually triggers the playlist checking mechanism for a specific playlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
return jsonify({"error": "Watch feature is currently disabled globally. Cannot trigger check."}), 403
logger.info(f"Manual trigger for specific playlist check received for ID: {playlist_spotify_id}")
try:
# Check if the playlist is actually in the watchlist first

View File

@@ -73,6 +73,42 @@ document.addEventListener('DOMContentLoaded', () => {
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, 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:', error);
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide on error
}
}
}
updateWatchlistButtonVisibility();
});
function renderAlbum(album: Album) {

View File

@@ -46,7 +46,28 @@ interface WatchStatusResponse {
artist_data?: any; // The artist data from DB if watched
}
document.addEventListener('DOMContentLoaded', () => {
// Added: Interface for global watch config
interface GlobalWatchConfig {
enabled: boolean;
[key: string]: any;
}
// 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 () => {
const pathSegments = window.location.pathname.split('/');
const artistId = pathSegments[pathSegments.indexOf('artist') + 1];
@@ -55,13 +76,16 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
const globalWatchConfig = await getGlobalWatchConfig(); // Fetch global config
const isGlobalWatchActuallyEnabled = globalWatchConfig.enabled;
// Fetch artist info directly
fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json() as Promise<ArtistData>;
})
.then(data => renderArtist(data, artistId))
.then(data => renderArtist(data, artistId, isGlobalWatchActuallyEnabled))
.catch(error => {
console.error('Error:', error);
showError('Failed to load artist info.');
@@ -72,11 +96,47 @@ document.addEventListener('DOMContentLoaded', () => {
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 artist 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 artist page:', error);
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide on error
}
}
}
updateWatchlistButtonVisibility();
// Initialize the watch button after main artist rendering
// This is done inside renderArtist after button element is potentially created.
});
async function renderArtist(artistData: ArtistData, artistId: string) {
async function renderArtist(artistData: ArtistData, artistId: string, isGlobalWatchEnabled: boolean) {
const loadingEl = document.getElementById('loading');
if (loadingEl) loadingEl.classList.add('hidden');
@@ -84,7 +144,10 @@ async function renderArtist(artistData: ArtistData, artistId: string) {
if (errorEl) errorEl.classList.add('hidden');
// Fetch watch status upfront to avoid race conditions for album button rendering
const isArtistActuallyWatched = await getArtistWatchStatus(artistId);
let isArtistActuallyWatched = false; // Default
if (isGlobalWatchEnabled) { // Only fetch if globally enabled
isArtistActuallyWatched = await getArtistWatchStatus(artistId);
}
// Check if explicit filter is enabled
const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled();
@@ -107,12 +170,25 @@ async function renderArtist(artistData: ArtistData, artistId: string) {
artistImageEl.src = artistImageSrc;
}
// Initialize Watch Button after other elements are rendered
const watchArtistBtn = document.getElementById('watchArtistBtn') as HTMLButtonElement | null;
if (watchArtistBtn) {
initializeWatchButton(artistId, isArtistActuallyWatched);
const syncArtistBtn = document.getElementById('syncArtistBtn') as HTMLButtonElement | null;
if (!isGlobalWatchEnabled) {
if (watchArtistBtn) {
watchArtistBtn.classList.add('hidden');
watchArtistBtn.disabled = true;
}
if (syncArtistBtn) {
syncArtistBtn.classList.add('hidden');
syncArtistBtn.disabled = true;
}
} else {
console.warn("Watch artist button not found in HTML.");
if (watchArtistBtn) {
initializeWatchButton(artistId, isArtistActuallyWatched);
} else {
console.warn("Watch artist button not found in HTML.");
}
// Sync button visibility is managed by initializeWatchButton
}
// Define the artist URL (used by both full-discography and group downloads)
@@ -207,8 +283,8 @@ async function renderArtist(artistData: ArtistData, artistId: string) {
// Use the definitively fetched watch status for rendering album buttons
// const isArtistWatched = watchArtistBtn && watchArtistBtn.dataset.watching === 'true'; // Old way
const useThisWatchStatusForAlbums = isArtistActuallyWatched; // New way
// const useThisWatchStatusForAlbums = isArtistActuallyWatched; // Old way, now combination of global and individual
for (const [groupType, albums] of Object.entries(albumGroups)) {
const groupSection = document.createElement('section');
groupSection.className = 'album-group';
@@ -253,7 +329,7 @@ async function renderArtist(artistData: ArtistData, artistId: string) {
albumCardActions.className = 'album-card-actions';
// Persistent Mark as Known/Missing button (if artist is watched) - Appears first (left)
if (useThisWatchStatusForAlbums && album.id) {
if (isGlobalWatchEnabled && isArtistActuallyWatched && album.id) {
const toggleKnownBtn = document.createElement('button');
toggleKnownBtn.className = 'toggle-known-status-btn persistent-album-action-btn';
toggleKnownBtn.dataset.albumId = album.id;
@@ -351,7 +427,7 @@ async function renderArtist(artistData: ArtistData, artistId: string) {
albumCardActions_AppearsOn.className = 'album-card-actions';
// Persistent Mark as Known/Missing button for appearing_on albums (if artist is watched) - Appears first (left)
if (useThisWatchStatusForAlbums && album.id) {
if (isGlobalWatchEnabled && isArtistActuallyWatched && album.id) {
const toggleKnownBtn = document.createElement('button');
toggleKnownBtn.className = 'toggle-known-status-btn persistent-album-action-btn';
toggleKnownBtn.dataset.albumId = album.id;
@@ -413,7 +489,7 @@ async function renderArtist(artistData: ArtistData, artistId: string) {
if (albumsContainerEl) albumsContainerEl.classList.remove('hidden');
if (!isExplicitFilterEnabled) {
attachAlbumActionListeners(artistId);
attachAlbumActionListeners(artistId, isGlobalWatchEnabled);
attachGroupDownloadListeners(artistId, artistName);
}
}
@@ -448,7 +524,7 @@ function attachGroupDownloadListeners(artistId: string, artistName: string) {
});
}
function attachAlbumActionListeners(artistIdForContext: string) {
function attachAlbumActionListeners(artistIdForContext: string, isGlobalWatchEnabled: boolean) {
const groupsContainer = document.getElementById('album-groups');
if (!groupsContainer) return;
@@ -457,6 +533,10 @@ function attachAlbumActionListeners(artistIdForContext: string) {
const button = target.closest('.toggle-known-status-btn') as HTMLButtonElement | null;
if (button && button.dataset.albumId) {
if (!isGlobalWatchEnabled) {
showNotification("Watch feature is currently disabled globally.");
return;
}
const albumId = button.dataset.albumId;
const currentStatus = button.dataset.status;
@@ -685,15 +765,24 @@ async function initializeWatchButton(artistId: string, initialIsWatching: boolea
if (currentlyWatching) {
await unwatchArtist(artistId);
updateWatchButton(artistId, false);
// Re-fetch and re-render artist data
// Re-fetch and re-render artist data, passing the global watch status again
const newArtistData = await (await fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`)).json() as ArtistData;
renderArtist(newArtistData, artistId);
// Assuming renderArtist needs the global status, which it does. We need to get it or have it available.
// Since initializeWatchButton is called from renderArtist, we can assume isGlobalWatchEnabled is in that scope.
// This part is tricky as initializeWatchButton doesn't have isGlobalWatchEnabled.
// Let's re-fetch global config or rely on the fact that if this button is clickable, global is on.
// For simplicity, the re-render will pick up the global status from its own scope if called from top level.
// The click handler itself does not need to pass isGlobalWatchEnabled to renderArtist, renderArtist's caller does.
// Let's ensure renderArtist is called correctly after watch/unwatch.
const globalWatchConfig = await getGlobalWatchConfig(); // Re-fetch for re-render
renderArtist(newArtistData, artistId, globalWatchConfig.enabled);
} else {
await watchArtist(artistId);
updateWatchButton(artistId, true);
// Re-fetch and re-render artist data
const newArtistData = await (await fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`)).json() as ArtistData;
renderArtist(newArtistData, artistId);
const globalWatchConfig = await getGlobalWatchConfig(); // Re-fetch for re-render
renderArtist(newArtistData, artistId, globalWatchConfig.enabled);
}
} catch (error) {
// On error, revert button to its state before the click attempt

View File

@@ -168,6 +168,43 @@ document.addEventListener('DOMContentLoaded', async () => {
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 config 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 config page:', error);
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide on error
}
}
}
updateWatchlistButtonVisibility();
} catch (error: any) {
showConfigError(error.message);
}

View File

@@ -79,6 +79,7 @@ document.addEventListener('DOMContentLoaded', function() {
const queueIcon = document.getElementById('queueIcon');
const emptyState = document.getElementById('emptyState');
const loadingResults = document.getElementById('loadingResults');
const watchlistButton = document.getElementById('watchlistButton') as HTMLAnchorElement | null;
// Initialize the queue
if (queueIcon) {
@@ -124,6 +125,41 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Attempt to set initial watchlist button visibility from cache
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, defaulting to hidden');
// Don't update cache on error, rely on default hidden or previous cache state until success
watchlistButton.classList.add('hidden'); // Hide if config fetch fails
}
} catch (error) {
console.error('Error fetching watch config:', error);
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide on error
}
}
}
updateWatchlistButtonVisibility();
// Check for URL parameters
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q');

View File

@@ -61,6 +61,12 @@ interface WatchedPlaylistStatus {
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
@@ -69,7 +75,22 @@ interface DownloadQueueItem {
// Add any other properties your item might have, compatible with QueueItem
}
document.addEventListener('DOMContentLoaded', () => {
// 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];
@@ -79,20 +100,40 @@ document.addEventListener('DOMContentLoaded', () => {
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))
.then(data => renderPlaylist(data, isGlobalWatchActuallyEnabled))
.catch(error => {
console.error('Error:', error);
showError('Failed to load playlist.');
});
// Fetch initial watch status
fetchWatchStatus(playlistId);
// 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) {
@@ -100,12 +141,48 @@ document.addEventListener('DOMContentLoaded', () => {
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) {
function renderPlaylist(playlist: Playlist, isGlobalWatchEnabled: boolean) {
// Hide loading and error messages
const loadingEl = document.getElementById('loading');
if (loadingEl) loadingEl.classList.add('hidden');
@@ -250,7 +327,11 @@ function renderPlaylist(playlist: Playlist) {
// Determine if the playlist is being watched to show/hide management buttons
const watchPlaylistButton = document.getElementById('watchPlaylistBtn') as HTMLButtonElement;
const isPlaylistWatched = watchPlaylistButton && watchPlaylistButton.classList.contains('watching');
// 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) => {
@@ -314,7 +395,7 @@ function renderPlaylist(playlist: Playlist) {
actionsContainer.innerHTML += downloadBtnHTML;
}
if (isPlaylistWatched) {
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";
@@ -346,7 +427,7 @@ function renderPlaylist(playlist: Playlist) {
if (tracksContainerEl) tracksContainerEl.classList.remove('hidden');
// Attach download listeners to newly rendered download buttons
attachTrackActionListeners();
attachTrackActionListeners(isGlobalWatchEnabled);
}
/**
@@ -374,7 +455,7 @@ function showError(message: string) {
/**
* Attaches event listeners to all individual track action buttons (download, mark known, mark missing).
*/
function attachTrackActionListeners() {
function attachTrackActionListeners(isGlobalWatchEnabled: boolean) {
document.querySelectorAll('.track-download-btn').forEach((btn) => {
btn.addEventListener('click', (e: Event) => {
e.stopPropagation();
@@ -405,6 +486,11 @@ function attachTrackActionListeners() {
return;
}
if (!isGlobalWatchEnabled) { // Added check
showNotification("Watch feature is currently disabled globally. Cannot change track status.");
return;
}
button.disabled = true;
try {
if (currentStatus === 'missing') {
@@ -656,6 +742,18 @@ function updateWatchButtons(isWatched: boolean, playlistId: string) {
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' });
@@ -668,7 +766,7 @@ async function watchPlaylist(playlistId: string) {
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);
renderPlaylist(newPlaylistData, globalConfig.enabled); // Pass current global enabled state
showNotification(`Playlist added to watchlist. Tracks are being updated.`);
} catch (error: any) {
@@ -683,6 +781,17 @@ async function watchPlaylist(playlistId: string) {
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' });
@@ -695,7 +804,7 @@ async function unwatchPlaylist(playlistId: string) {
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);
renderPlaylist(newPlaylistData, globalConfig.enabled); // Pass current global enabled state
showNotification('Playlist removed from watchlist. Track statuses updated.');
} catch (error: any) {
@@ -710,6 +819,12 @@ async function unwatchPlaylist(playlistId: string) {
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;

View File

@@ -30,6 +30,42 @@ document.addEventListener('DOMContentLoaded', () => {
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 track 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 track page:', error);
// Don't update cache on error
watchlistButton.classList.add('hidden'); // Hide on error
}
}
}
updateWatchlistButtonVisibility();
});
/**

View File

@@ -133,13 +133,37 @@ interface WatchedPlaylistOriginal {
type WatchedItem = (WatchedArtistOriginal & { itemType: 'artist' }) | (WatchedPlaylistOriginal & { itemType: 'playlist' });
document.addEventListener('DOMContentLoaded', function() {
// Added: Interface for global watch config
interface GlobalWatchConfig {
enabled: boolean;
[key: string]: any; // Allow other properties
}
// 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 function() {
const watchedItemsContainer = document.getElementById('watchedItemsContainer');
const loadingIndicator = document.getElementById('loadingWatchedItems');
const emptyStateIndicator = document.getElementById('emptyWatchedItems');
const queueIcon = document.getElementById('queueIcon');
const checkAllWatchedBtn = document.getElementById('checkAllWatchedBtn') as HTMLButtonElement | null;
// Fetch global watch config first
const globalWatchConfig = await getGlobalWatchConfig();
if (queueIcon) {
queueIcon.addEventListener('click', () => {
downloadQueue.toggleVisibility();
@@ -214,8 +238,28 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Initial load
loadWatchedItems();
// Initial load is now conditional
if (globalWatchConfig.enabled) {
if (checkAllWatchedBtn) checkAllWatchedBtn.classList.remove('hidden');
loadWatchedItems();
} else {
// Watch feature is disabled globally
showLoading(false);
showEmptyState(false);
if (checkAllWatchedBtn) checkAllWatchedBtn.classList.add('hidden'); // Hide the button
if (watchedItemsContainer) {
watchedItemsContainer.innerHTML = `
<div class="empty-state-container">
<img src="/static/images/eye-crossed.svg" alt="Watch Disabled" class="empty-state-icon">
<p class="empty-state-message">The Watchlist feature is currently disabled in the application settings.</p>
<p class="empty-state-submessage">Please enable it in <a href="/settings" class="settings-link">Settings</a> to use this page.</p>
</div>
`;
}
// Ensure the main loading indicator is also hidden if it was shown by default
if (loadingIndicator) loadingIndicator.classList.add('hidden');
}
});
const MAX_NOTIFICATIONS = 3;

View File

@@ -53,7 +53,7 @@
<img src="{{ url_for('static', filename='images/home.svg') }}" alt="Home">
</button>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<a id="watchlistButton" href="/watchlist" class="btn-icon watchlist-icon floating-icon hidden" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>

View File

@@ -58,7 +58,7 @@
<img src="{{ url_for('static', filename='images/home.svg') }}" alt="Home">
</button>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<a id="watchlistButton" href="/watchlist" class="btn-icon watchlist-icon floating-icon hidden" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>

View File

@@ -324,7 +324,7 @@
<img src="{{ url_for('static', filename='images/arrow-left.svg') }}" alt="Back" />
</a>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<a id="watchlistButton" href="/watchlist" class="btn-icon watchlist-icon floating-icon hidden" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>

View File

@@ -66,7 +66,7 @@
<img src="{{ url_for('static', filename='images/settings.svg') }}" alt="Settings" onerror="handleImageError(this)"/>
</a>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<a id="watchlistButton" href="/watchlist" class="btn-icon watchlist-icon floating-icon hidden" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>

View File

@@ -65,7 +65,7 @@
<img src="{{ url_for('static', filename='images/home.svg') }}" alt="Home">
</button>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<a id="watchlistButton" href="/watchlist" class="btn-icon watchlist-icon floating-icon hidden" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>

View File

@@ -52,7 +52,7 @@
<img src="{{ url_for('static', filename='images/home.svg') }}" alt="Home">
</button>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<a id="watchlistButton" href="/watchlist" class="btn-icon watchlist-icon floating-icon hidden" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>

View File

@@ -49,10 +49,6 @@
<a href="/" class="btn-icon home-btn floating-icon settings-icon" aria-label="Return to Home" title="Return to Home">
<img src="{{ url_for('static', filename='images/home.svg') }}" alt="Home" onerror="handleImageError(this)"/>
</a>
<a href="/watchlist" class="btn-icon watchlist-icon floating-icon" aria-label="Watchlist" title="Go to Watchlist">
<img src="{{ url_for('static', filename='images/binoculars.svg') }}" alt="Watchlist" onerror="handleImageError(this)"/>
</a>
<button
id="queueIcon"