5
.github/workflows/docker-build.yml
vendored
5
.github/workflows/docker-build.yml
vendored
@@ -40,6 +40,11 @@ jobs:
|
||||
# Keep dev tag for main/master branch
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Set version in config.html
|
||||
run: |
|
||||
VERSION=$(echo "${{ steps.meta.outputs.version }}" | sed 's/^v//')
|
||||
sed -i "s|Set on build|Version: $VERSION|g" static/html/config.html
|
||||
|
||||
# Build and push Docker image with multiarch support
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
|
||||
@@ -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
|
||||
celery==5.5.3
|
||||
Flask==3.1.1
|
||||
flask_cors==6.0.0
|
||||
deezspot-spotizerr==1.4.0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -111,7 +111,6 @@ def download_album(
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
make_zip=False,
|
||||
method_save=1,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
pad_tracks=pad_tracks,
|
||||
@@ -151,7 +150,6 @@ def download_album(
|
||||
recursive_quality=True,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
make_zip=False,
|
||||
real_time_dl=real_time,
|
||||
custom_dir_format=custom_dir_format,
|
||||
@@ -192,7 +190,6 @@ def download_album(
|
||||
recursive_quality=True,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
make_zip=False,
|
||||
real_time_dl=real_time,
|
||||
custom_dir_format=custom_dir_format,
|
||||
@@ -228,7 +225,6 @@ def download_album(
|
||||
quality_download=quality,
|
||||
recursive_quality=True,
|
||||
recursive_download=False,
|
||||
method_save=1,
|
||||
make_zip=False,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
|
||||
@@ -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}")
|
||||
logger.debug(f"Extracted album ID: {album_id}")
|
||||
|
||||
if not album_url:
|
||||
logger.warning(f"Skipping album without URL: {album_name}")
|
||||
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 = {
|
||||
|
||||
@@ -106,7 +106,6 @@ def download_playlist(
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
make_zip=False,
|
||||
method_save=1,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
pad_tracks=pad_tracks,
|
||||
@@ -146,7 +145,6 @@ def download_playlist(
|
||||
recursive_quality=True,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
make_zip=False,
|
||||
real_time_dl=real_time,
|
||||
custom_dir_format=custom_dir_format,
|
||||
@@ -187,7 +185,6 @@ def download_playlist(
|
||||
recursive_quality=True,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
make_zip=False,
|
||||
real_time_dl=real_time,
|
||||
custom_dir_format=custom_dir_format,
|
||||
@@ -223,7 +220,6 @@ def download_playlist(
|
||||
quality_download=quality,
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
method_save=1,
|
||||
make_zip=False,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
|
||||
@@ -106,7 +106,6 @@ def download_track(
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
initial_retry_delay=initial_retry_delay,
|
||||
@@ -140,7 +139,6 @@ def download_track(
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
real_time_dl=real_time,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
@@ -174,7 +172,6 @@ def download_track(
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=1,
|
||||
real_time_dl=real_time,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
@@ -204,7 +201,6 @@ def download_track(
|
||||
quality_download=quality,
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
method_save=1,
|
||||
custom_dir_format=custom_dir_format,
|
||||
custom_track_format=custom_track_format,
|
||||
pad_tracks=pad_tracks,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
115
src/js/artist.ts
115
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<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,13 +170,26 @@ 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;
|
||||
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 {
|
||||
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)
|
||||
// const artistUrl = `https://open.spotify.com/artist/${artistId}`; // Not directly used here anymore
|
||||
@@ -207,7 +283,7 @@ 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');
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
// 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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="config-container">
|
||||
<header class="config-header">
|
||||
<h1 class="header-title">Configuration</h1>
|
||||
<span class="version-text">2.0.1</span>
|
||||
<span class="version-text">Set on build</span>
|
||||
</header>
|
||||
|
||||
<div class="account-config card">
|
||||
@@ -118,9 +118,9 @@
|
||||
<option value="">-- Select placeholder --</option>
|
||||
<optgroup label="Common">
|
||||
<option value="%music%">%music% - Track title</option>
|
||||
<option value="%artist%">%artist% - Track artist</option>
|
||||
<option value="%artist%">%artist% - Track artist. If multiple artists, use %artist_1%, %artist_2%, etc. to select a specific one.</option>
|
||||
<option value="%album%">%album% - Album name</option>
|
||||
<option value="%ar_album%">%ar_album% - Album artist</option>
|
||||
<option value="%ar_album%">%ar_album% - Album artist. If multiple album artists, use %ar_album_1%, %ar_album_2%, etc. to select a specific one.</option>
|
||||
<option value="%tracknum%">%tracknum% - Track number</option>
|
||||
<option value="%year%">%year% - Year of release</option>
|
||||
</optgroup>
|
||||
@@ -171,9 +171,9 @@
|
||||
<option value="">-- Select placeholder --</option>
|
||||
<optgroup label="Common">
|
||||
<option value="%music%">%music% - Track title</option>
|
||||
<option value="%artist%">%artist% - Track artist</option>
|
||||
<option value="%artist%">%artist% - Track artist. If multiple artists, use %artist_1%, %artist_2%, etc. to select a specific one.</option>
|
||||
<option value="%album%">%album% - Album name</option>
|
||||
<option value="%ar_album%">%ar_album% - Album artist</option>
|
||||
<option value="%ar_album%">%ar_album% - Album artist. If multiple album artists, use %ar_album_1%, %ar_album_2%, etc. to select a specific one.</option>
|
||||
<option value="%tracknum%">%tracknum% - Track number</option>
|
||||
<option value="%year%">%year% - Year of release</option>
|
||||
</optgroup>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -50,10 +50,6 @@
|
||||
<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"
|
||||
class="btn-icon queue-icon floating-icon"
|
||||
|
||||
Reference in New Issue
Block a user