From 7bd11bdaa44fd3ab2f9f1c0717eab545860c4eda Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Tue, 3 Jun 2025 13:29:18 -0600 Subject: [PATCH 1/5] fixed #141 --- requirements.txt | 66 ++++-------------------------------------- routes/utils/artist.py | 14 +++++---- 2 files changed, 13 insertions(+), 67 deletions(-) diff --git a/requirements.txt b/requirements.txt index 30e3410..138e47a 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,61 +1,5 @@ -amqp==5.3.1 -annotated-types==0.7.0 -anyio==4.9.0 -billiard==4.2.1 -blinker==1.9.0 -celery==5.5.2 -certifi==2025.4.26 -charset-normalizer==3.4.2 -click==8.2.1 -click-didyoumean==0.3.1 -click-plugins==1.1.1 -click-repl==0.3.0 -deezspot @ git+https://github.com/Xoconoch/deezspot-fork-again -defusedxml==0.7.1 -fastapi==0.115.12 -Flask==3.1.1 -Flask-Celery-Helper==1.1.0 -flask-cors==6.0.0 -h11==0.16.0 -httptools==0.6.4 -idna==3.10 -ifaddr==0.2.0 -itsdangerous==2.2.0 -Jinja2==3.1.6 -kombu==5.5.3 -librespot==0.0.9 -MarkupSafe==3.0.2 -mutagen==1.47.0 -prompt_toolkit==3.0.51 -protobuf==3.20.1 -pycryptodome==3.23.0 -pycryptodomex==3.17 -pydantic==2.11.5 -pydantic_core==2.33.2 -PyOgg==0.6.14a1 -python-dateutil==2.9.0.post0 -python-dotenv==1.1.0 -PyYAML==6.0.2 -redis==6.2.0 -requests==2.30.0 -six==1.17.0 -sniffio==1.3.1 -spotipy==2.25.1 -spotipy_anon==1.4 -sse-starlette==2.3.5 -starlette==0.46.2 -tqdm==4.67.1 -typing-inspection==0.4.1 -typing_extensions==4.13.2 -tzdata==2025.2 -urllib3==2.4.0 -uvicorn==0.34.2 -uvloop==0.21.0 -vine==5.1.0 -waitress==3.0.2 -watchfiles==1.0.5 -wcwidth==0.2.13 -websocket-client==1.5.1 -websockets==15.0.1 -Werkzeug==3.1.3 -zeroconf==0.62.0 +waitress +celery +flask +flask_cors +deezspot-spotizerr diff --git a/routes/utils/artist.py b/routes/utils/artist.py index e0cac69..c07f93e 100644 --- a/routes/utils/artist.py +++ b/routes/utils/artist.py @@ -100,7 +100,7 @@ def download_artist_albums(url, album_type="album,single,compilation", request_a raise ValueError(error_msg) # Get artist info with albums - artist_data = get_spotify_info(artist_id, "artist") + artist_data = get_spotify_info(artist_id, "artist_discography") # Debug logging to inspect the structure of artist_data logger.debug(f"Artist data structure has keys: {list(artist_data.keys() if isinstance(artist_data, dict) else [])}") @@ -153,11 +153,13 @@ def download_artist_albums(url, album_type="album,single,compilation", request_a album_name = album.get('name', 'Unknown Album') album_artists = album.get('artists', []) album_artist = album_artists[0].get('name', 'Unknown Artist') if album_artists else 'Unknown Artist' - + album_id = album.get('id') + logger.debug(f"Extracted album URL: {album_url}") - - if not album_url: - logger.warning(f"Skipping album without URL: {album_name}") + logger.debug(f"Extracted album ID: {album_id}") + + if not album_url or not album_id: + logger.warning(f"Skipping album without URL or ID: {album_name}") continue # Create album-specific request args instead of using original artist request @@ -172,7 +174,7 @@ def download_artist_albums(url, album_type="album,single,compilation", request_a } # Include original download URL for this album task - album_request_args["original_url"] = url_for('album.handle_download', url=album_url, _external=True) + album_request_args["original_url"] = url_for('album.handle_download', album_id=album_id, _external=True) # Create task for this album task_data = { From 0a1c66b02a8c0b7b78c63fb81d7f78027f88db83 Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Tue, 3 Jun 2025 14:02:04 -0600 Subject: [PATCH 2/5] fixed #142 --- requirements.txt | 10 +-- routes/artist.py | 26 +++++++- routes/playlist.py | 28 +++++++- src/js/album.ts | 36 ++++++++++ src/js/artist.ts | 123 +++++++++++++++++++++++++++++----- src/js/config.ts | 37 ++++++++++ src/js/main.ts | 36 ++++++++++ src/js/playlist.ts | 137 +++++++++++++++++++++++++++++++++++--- src/js/track.ts | 36 ++++++++++ src/js/watch.ts | 50 +++++++++++++- static/html/album.html | 2 +- static/html/artist.html | 2 +- static/html/config.html | 2 +- static/html/main.html | 2 +- static/html/playlist.html | 2 +- static/html/track.html | 2 +- static/html/watch.html | 4 -- 17 files changed, 486 insertions(+), 49 deletions(-) diff --git a/requirements.txt b/requirements.txt index 138e47a..189450b 100755 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/routes/artist.py b/routes/artist.py index d585984..dbd5e32 100644 --- a/routes/artist.py +++ b/routes/artist.py @@ -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/', 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/', 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/', 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//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//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 diff --git a/routes/playlist.py b/routes/playlist.py index 2f53fb1..b7a7966 100755 --- a/routes/playlist.py +++ b/routes/playlist.py @@ -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/', 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/', 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//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//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/', 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 diff --git a/src/js/album.ts b/src/js/album.ts index 1d62863..3345c8d 100644 --- a/src/js/album.ts +++ b/src/js/album.ts @@ -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) { diff --git a/src/js/artist.ts b/src/js/artist.ts index 83d9a76..f26d8a0 100644 --- a/src/js/artist.ts +++ b/src/js/artist.ts @@ -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 { + 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; }) - .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 diff --git a/src/js/config.ts b/src/js/config.ts index 4e73a70..c172313 100644 --- a/src/js/config.ts +++ b/src/js/config.ts @@ -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); } diff --git a/src/js/main.ts b/src/js/main.ts index bc14a91..6d31dc1 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -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'); diff --git a/src/js/playlist.ts b/src/js/playlist.ts index c2873fe..7c20762 100644 --- a/src/js/playlist.ts +++ b/src/js/playlist.ts @@ -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 { + 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; }) - .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; diff --git a/src/js/track.ts b/src/js/track.ts index 2a5c9fa..e5cac4d 100644 --- a/src/js/track.ts +++ b/src/js/track.ts @@ -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(); }); /** diff --git a/src/js/watch.ts b/src/js/watch.ts index b5beb11..d2710cf 100644 --- a/src/js/watch.ts +++ b/src/js/watch.ts @@ -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 { + 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 = ` +
+ Watch Disabled +

The Watchlist feature is currently disabled in the application settings.

+

Please enable it in Settings to use this page.

+
+ `; + } + // Ensure the main loading indicator is also hidden if it was shown by default + if (loadingIndicator) loadingIndicator.classList.add('hidden'); + } }); const MAX_NOTIFICATIONS = 3; diff --git a/static/html/album.html b/static/html/album.html index a47bb37..ba32c49 100644 --- a/static/html/album.html +++ b/static/html/album.html @@ -53,7 +53,7 @@ Home - + diff --git a/static/html/artist.html b/static/html/artist.html index bc93f87..fd7c2fa 100644 --- a/static/html/artist.html +++ b/static/html/artist.html @@ -58,7 +58,7 @@ Home - + diff --git a/static/html/config.html b/static/html/config.html index 49b611b..6585e1d 100644 --- a/static/html/config.html +++ b/static/html/config.html @@ -324,7 +324,7 @@ Back - + diff --git a/static/html/main.html b/static/html/main.html index e9a5b66..d0580ba 100755 --- a/static/html/main.html +++ b/static/html/main.html @@ -66,7 +66,7 @@ Settings - + diff --git a/static/html/playlist.html b/static/html/playlist.html index de08696..f9721fa 100644 --- a/static/html/playlist.html +++ b/static/html/playlist.html @@ -65,7 +65,7 @@ Home - + diff --git a/static/html/track.html b/static/html/track.html index 4dbb8bd..a027bff 100644 --- a/static/html/track.html +++ b/static/html/track.html @@ -52,7 +52,7 @@ Home - + diff --git a/static/html/watch.html b/static/html/watch.html index 6462734..b7e40b4 100644 --- a/static/html/watch.html +++ b/static/html/watch.html @@ -49,10 +49,6 @@ Home - - - Watchlist -