diff --git a/requirements.txt b/requirements.txt
index 88789c3..30e3410 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ 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@0b906e94e774cdeb8557a14a53236c0834fb1a2e
+deezspot @ git+https://github.com/Xoconoch/deezspot-fork-again
defusedxml==0.7.1
fastapi==0.115.12
Flask==3.1.1
diff --git a/routes/artist.py b/routes/artist.py
index d6f0b5d..d585984 100644
--- a/routes/artist.py
+++ b/routes/artist.py
@@ -123,7 +123,6 @@ def get_artist_info():
)
try:
- from routes.utils.get_info import get_spotify_info
artist_info = get_spotify_info(spotify_id, "artist_discography")
# If artist_info is successfully fetched (it contains album items),
diff --git a/routes/utils/celery_tasks.py b/routes/utils/celery_tasks.py
index 650878f..6f7001c 100644
--- a/routes/utils/celery_tasks.py
+++ b/routes/utils/celery_tasks.py
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
# Setup Redis and Celery
from routes.utils.celery_config import REDIS_URL, REDIS_BACKEND, REDIS_PASSWORD, get_config_params
# Import for playlist watch DB update
-from routes.utils.watch.db import add_single_track_to_playlist_db
+from routes.utils.watch.db import add_single_track_to_playlist_db, add_or_update_album_for_artist
# Import history manager function
from .history_manager import add_entry_to_history
@@ -840,6 +840,41 @@ class ProgressTrackingTask(Task):
countdown=30 # Delay in seconds
)
+ # If from playlist_watch and successful, add track to DB
+ original_request = task_info.get("original_request", {})
+ if original_request.get("source") == "playlist_watch" and task_info.get("download_type") == "track": # ensure it's a track for playlist
+ playlist_id = original_request.get("playlist_id")
+ track_item_for_db = original_request.get("track_item_for_db")
+
+ if playlist_id and track_item_for_db and track_item_for_db.get('track'):
+ logger.info(f"Task {task_id} was from playlist watch for playlist {playlist_id}. Adding track to DB.")
+ try:
+ add_single_track_to_playlist_db(playlist_id, track_item_for_db)
+ except Exception as db_add_err:
+ logger.error(f"Failed to add track to DB for playlist {playlist_id} after successful download task {task_id}: {db_add_err}", exc_info=True)
+ else:
+ logger.warning(f"Task {task_id} was from playlist_watch but missing playlist_id or track_item_for_db for DB update. Original Request: {original_request}")
+
+ # If from artist_watch and successful, update album in DB
+ if original_request.get("source") == "artist_watch" and task_info.get("download_type") == "album":
+ artist_spotify_id = original_request.get("artist_spotify_id")
+ album_data_for_db = original_request.get("album_data_for_db")
+
+ if artist_spotify_id and album_data_for_db and album_data_for_db.get("id"):
+ album_spotify_id = album_data_for_db.get("id")
+ logger.info(f"Task {task_id} was from artist watch for artist {artist_spotify_id}, album {album_spotify_id}. Updating album in DB as complete.")
+ try:
+ add_or_update_album_for_artist(
+ artist_spotify_id=artist_spotify_id,
+ album_data=album_data_for_db,
+ task_id=task_id,
+ is_download_complete=True
+ )
+ except Exception as db_update_err:
+ logger.error(f"Failed to update album {album_spotify_id} in DB for artist {artist_spotify_id} after successful download task {task_id}: {db_update_err}", exc_info=True)
+ else:
+ logger.warning(f"Task {task_id} was from artist_watch (album) but missing key data (artist_spotify_id or album_data_for_db) for DB update. Original Request: {original_request}")
+
else:
# Generic done for other types
logger.info(f"Task {task_id} completed: {content_type.upper()}")
@@ -870,21 +905,16 @@ def task_prerun_handler(task_id=None, task=None, *args, **kwargs):
def task_postrun_handler(task_id=None, task=None, retval=None, state=None, *args, **kwargs):
"""Signal handler when a task finishes"""
try:
- # Skip if task is already marked as complete or error in Redis for history logging purposes
last_status_for_history = get_last_task_status(task_id)
if last_status_for_history and last_status_for_history.get("status") in [ProgressState.COMPLETE, ProgressState.ERROR, ProgressState.CANCELLED, "ERROR_RETRIED", "ERROR_AUTO_CLEANED"]:
- # Check if it was a REVOKED (cancelled) task, if so, ensure it's logged.
if state == states.REVOKED and last_status_for_history.get("status") != ProgressState.CANCELLED:
logger.info(f"Task {task_id} was REVOKED (likely cancelled), logging to history.")
_log_task_to_history(task_id, 'CANCELLED', "Task was revoked/cancelled.")
- # else:
- # logger.debug(f"History: Task {task_id} already in terminal state {last_status_for_history.get('status')} in Redis. History logging likely handled.")
- # return # Do not return here, let the normal status update proceed for Redis if necessary
+ # return # Let status update proceed if necessary
task_info = get_task_info(task_id)
current_redis_status = last_status_for_history.get("status") if last_status_for_history else None
- # Update task status based on Celery task state
if state == states.SUCCESS:
if current_redis_status != ProgressState.COMPLETE:
store_task_status(task_id, {
@@ -898,16 +928,15 @@ def task_postrun_handler(task_id=None, task=None, retval=None, state=None, *args
logger.info(f"Task {task_id} completed successfully: {task_info.get('name', 'Unknown')}")
_log_task_to_history(task_id, 'COMPLETED')
- # If the task was a single track, schedule its data for deletion after a delay
- if task_info.get("download_type") == "track":
+ if task_info.get("download_type") == "track": # Applies to single track downloads and tracks from playlists/albums
delayed_delete_task_data.apply_async(
args=[task_id, "Task completed successfully and auto-cleaned."],
- countdown=30 # Delay in seconds
+ countdown=30
)
- # If from playlist_watch and successful, add track to DB
original_request = task_info.get("original_request", {})
- if original_request.get("source") == "playlist_watch":
+ # Handle successful track from playlist watch
+ if original_request.get("source") == "playlist_watch" and task_info.get("download_type") == "track":
playlist_id = original_request.get("playlist_id")
track_item_for_db = original_request.get("track_item_for_db")
@@ -919,9 +948,29 @@ def task_postrun_handler(task_id=None, task=None, retval=None, state=None, *args
logger.error(f"Failed to add track to DB for playlist {playlist_id} after successful download task {task_id}: {db_add_err}", exc_info=True)
else:
logger.warning(f"Task {task_id} was from playlist_watch but missing playlist_id or track_item_for_db for DB update. Original Request: {original_request}")
+
+ # Handle successful album from artist watch
+ if original_request.get("source") == "artist_watch" and task_info.get("download_type") == "album":
+ artist_spotify_id = original_request.get("artist_spotify_id")
+ album_data_for_db = original_request.get("album_data_for_db")
+
+ if artist_spotify_id and album_data_for_db and album_data_for_db.get("id"):
+ album_spotify_id = album_data_for_db.get("id")
+ logger.info(f"Task {task_id} was from artist watch for artist {artist_spotify_id}, album {album_spotify_id}. Updating album in DB as complete.")
+ try:
+ add_or_update_album_for_artist(
+ artist_spotify_id=artist_spotify_id,
+ album_data=album_data_for_db,
+ task_id=task_id,
+ is_download_complete=True
+ )
+ except Exception as db_update_err:
+ logger.error(f"Failed to update album {album_spotify_id} in DB for artist {artist_spotify_id} after successful download task {task_id}: {db_update_err}", exc_info=True)
+ else:
+ logger.warning(f"Task {task_id} was from artist_watch (album) but missing key data (artist_spotify_id or album_data_for_db) for DB update. Original Request: {original_request}")
except Exception as e:
- logger.error(f"Error in task_postrun_handler: {e}")
+ logger.error(f"Error in task_postrun_handler: {e}", exc_info=True)
@task_failure.connect
def task_failure_handler(task_id=None, exception=None, traceback=None, *args, **kwargs):
diff --git a/routes/utils/watch/db.py b/routes/utils/watch/db.py
index d82129e..225690a 100644
--- a/routes/utils/watch/db.py
+++ b/routes/utils/watch/db.py
@@ -179,25 +179,31 @@ def get_playlist_track_ids_from_db(playlist_spotify_id: str):
return track_ids
def add_tracks_to_playlist_db(playlist_spotify_id: str, tracks_data: list):
- """Adds or updates a list of tracks in the specified playlist's tracks table in playlists.db."""
+ """
+ Updates existing tracks in the playlist's DB table to mark them as currently present
+ in Spotify and updates their last_seen timestamp. Also refreshes metadata.
+ Does NOT insert new tracks. New tracks are only added upon successful download.
+ """
table_name = f"playlist_{playlist_spotify_id.replace('-', '_')}"
if not tracks_data:
return
current_time = int(time.time())
- tracks_to_insert = []
+ tracks_to_update = []
for track_item in tracks_data:
track = track_item.get('track')
if not track or not track.get('id'):
- logger.warning(f"Skipping track due to missing data or ID in playlist {playlist_spotify_id}: {track_item}")
+ logger.warning(f"Skipping track update due to missing data or ID in playlist {playlist_spotify_id}: {track_item}")
continue
- # Ensure 'artists' and 'album' -> 'artists' are lists and extract names
artist_names = ", ".join([artist['name'] for artist in track.get('artists', []) if artist.get('name')])
album_artist_names = ", ".join([artist['name'] for artist in track.get('album', {}).get('artists', []) if artist.get('name')])
- tracks_to_insert.append((
- track['id'],
+ # Prepare tuple for UPDATE statement.
+ # Order: title, artist_names, album_name, album_artist_names, track_number,
+ # album_spotify_id, duration_ms, added_at_playlist,
+ # is_present_in_spotify, last_seen_in_spotify, spotify_track_id (for WHERE)
+ tracks_to_update.append((
track.get('name', 'N/A'),
artist_names,
track.get('album', {}).get('name', 'N/A'),
@@ -205,30 +211,44 @@ def add_tracks_to_playlist_db(playlist_spotify_id: str, tracks_data: list):
track.get('track_number'),
track.get('album', {}).get('id'),
track.get('duration_ms'),
- track_item.get('added_at'), # From playlist item
- current_time, # added_to_db
- 1, # is_present_in_spotify
- current_time # last_seen_in_spotify
+ track_item.get('added_at'), # From playlist item, update if changed
+ 1, # is_present_in_spotify flag
+ current_time, # last_seen_in_spotify timestamp
+ # added_to_db is NOT updated here as this function only updates existing records.
+ track['id'] # spotify_track_id for the WHERE clause
))
- if not tracks_to_insert:
- logger.info(f"No valid tracks to insert for playlist {playlist_spotify_id}.")
+ if not tracks_to_update:
+ logger.info(f"No valid tracks to prepare for update for playlist {playlist_spotify_id}.")
return
try:
with _get_playlists_db_connection() as conn: # Use playlists connection
cursor = conn.cursor()
- _create_playlist_tracks_table(playlist_spotify_id) # Ensure table exists
+ # The table should have been created when the playlist was added to watch
+ # or when the first track was successfully downloaded.
+ # _create_playlist_tracks_table(playlist_spotify_id) # Not strictly needed here if table creation is robust elsewhere.
+ # The fields in SET must match the order of ?s, excluding the last one for WHERE.
+ # This will only update rows where spotify_track_id matches.
cursor.executemany(f"""
- INSERT OR REPLACE INTO {table_name}
- (spotify_track_id, title, artist_names, album_name, album_artist_names, track_number, album_spotify_id, duration_ms, added_at_playlist, added_to_db, is_present_in_spotify, last_seen_in_spotify)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """, tracks_to_insert)
+ UPDATE {table_name} SET
+ title = ?,
+ artist_names = ?,
+ album_name = ?,
+ album_artist_names = ?,
+ track_number = ?,
+ album_spotify_id = ?,
+ duration_ms = ?,
+ added_at_playlist = ?,
+ is_present_in_spotify = ?,
+ last_seen_in_spotify = ?
+ WHERE spotify_track_id = ?
+ """, tracks_to_update)
conn.commit()
- logger.info(f"Added/updated {len(tracks_to_insert)} tracks in DB for playlist {playlist_spotify_id} in {PLAYLISTS_DB_PATH}.")
+ logger.info(f"Attempted to update metadata for {len(tracks_to_update)} tracks from API in DB for playlist {playlist_spotify_id}. Actual rows updated: {cursor.rowcount if cursor.rowcount != -1 else 'unknown'}.")
except sqlite3.Error as e:
- logger.error(f"Error adding tracks to playlist {playlist_spotify_id} in table {table_name} in {PLAYLISTS_DB_PATH}: {e}", exc_info=True)
+ logger.error(f"Error updating tracks in playlist {playlist_spotify_id} in table {table_name} in {PLAYLISTS_DB_PATH}: {e}", exc_info=True)
# Not raising here to allow other operations to continue if one batch fails.
def mark_tracks_as_not_present_in_spotify(playlist_spotify_id: str, track_ids_to_mark: list):
diff --git a/routes/utils/watch/manager.py b/routes/utils/watch/manager.py
index 560de77..c056e21 100644
--- a/routes/utils/watch/manager.py
+++ b/routes/utils/watch/manager.py
@@ -322,16 +322,14 @@ def check_watched_artists(specific_artist_id: str = None):
task_id_or_none = download_queue_manager.add_task(task_payload, from_watch_job=True)
if task_id_or_none: # Task was newly queued
- add_or_update_album_for_artist(artist_spotify_id, album_data, task_id=task_id_or_none, is_download_complete=False)
- logger.info(f"Artist Watch Manager: Queued download task {task_id_or_none} for new album '{album_name}' from artist '{artist_name}'.")
+ # REMOVED: add_or_update_album_for_artist(artist_spotify_id, album_data, task_id=task_id_or_none, is_download_complete=False)
+ # The album will be added/updated in the DB by celery_tasks.py upon successful download completion.
+ logger.info(f"Artist Watch Manager: Queued download task {task_id_or_none} for new album '{album_name}' from artist '{artist_name}'. DB entry will be created/updated on success.")
queued_for_download_count += 1
- # If task_id_or_none is None, it was a duplicate. We can still log/record album_data if needed, but without task_id or as already seen.
- # add_or_update_album_for_artist(artist_spotify_id, album_data, task_id=None) # This would just log metadata if not a duplicate.
- # The current add_task logic in celery_manager might create an error task for duplicates,
- # so we might not need to do anything special here for duplicates apart from not incrementing count.
+ # If task_id_or_none is None, it was a duplicate. Celery manager handles logging.
except Exception as e:
- logger.error(f"Artist Watch Manager: Failed to queue/record download for new album {album_id} ('{album_name}') from artist '{artist_name}': {e}", exc_info=True)
+ logger.error(f"Artist Watch Manager: Failed to queue download for new album {album_id} ('{album_name}') from artist '{artist_name}': {e}", exc_info=True)
else:
logger.info(f"Artist Watch Manager: Album '{album_name}' ({album_id}) by '{artist_name}' already known in DB (ID found in db_album_ids). Skipping queue.")
# Optionally, update its entry (e.g. last_seen, or if details changed), but for now, we only queue new ones.
diff --git a/src/js/artist.ts b/src/js/artist.ts
index 54bc66d..83d9a76 100644
--- a/src/js/artist.ts
+++ b/src/js/artist.ts
@@ -76,13 +76,16 @@ document.addEventListener('DOMContentLoaded', () => {
// This is done inside renderArtist after button element is potentially created.
});
-function renderArtist(artistData: ArtistData, artistId: string) {
+async function renderArtist(artistData: ArtistData, artistId: string) {
const loadingEl = document.getElementById('loading');
if (loadingEl) loadingEl.classList.add('hidden');
const errorEl = document.getElementById('error');
if (errorEl) errorEl.classList.add('hidden');
+ // Fetch watch status upfront to avoid race conditions for album button rendering
+ const isArtistActuallyWatched = await getArtistWatchStatus(artistId);
+
// Check if explicit filter is enabled
const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled();
@@ -107,7 +110,7 @@ function renderArtist(artistData: ArtistData, artistId: string) {
// Initialize Watch Button after other elements are rendered
const watchArtistBtn = document.getElementById('watchArtistBtn') as HTMLButtonElement | null;
if (watchArtistBtn) {
- initializeWatchButton(artistId);
+ initializeWatchButton(artistId, isArtistActuallyWatched);
} else {
console.warn("Watch artist button not found in HTML.");
}
@@ -202,8 +205,9 @@ function renderArtist(artistData: ArtistData, artistId: string) {
if (groupsContainer) {
groupsContainer.innerHTML = '';
- // Determine if the artist is being watched to show/hide management buttons for albums
- const isArtistWatched = watchArtistBtn && watchArtistBtn.dataset.watching === 'true';
+ // Use the definitively fetched watch status for rendering album buttons
+ // const isArtistWatched = watchArtistBtn && watchArtistBtn.dataset.watching === 'true'; // Old way
+ const useThisWatchStatusForAlbums = isArtistActuallyWatched; // New way
for (const [groupType, albums] of Object.entries(albumGroups)) {
const groupSection = document.createElement('section');
@@ -230,58 +234,75 @@ function renderArtist(artistData: ArtistData, artistId: string) {
if (!album) return;
const albumElement = document.createElement('div');
albumElement.className = 'album-card';
+ albumElement.dataset.albumId = album.id;
let albumCardHTML = `
+ class="album-cover ${album.is_locally_known === false ? 'album-missing-in-db' : ''}">