From a33f891fa60ea6770ab6284d485ecd2c7b145a71 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:17:02 -0700 Subject: [PATCH] Add per-artist watchlist lookback period override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Scan Lookback" dropdown in the watchlist artist config modal. Each artist can override the global lookback period (7d to entire discography). Default is "Use Global Setting" (NULL in DB). - Database: lookback_days INTEGER DEFAULT NULL on watchlist_artists, auto-migrated on startup - Scanner: checks per-artist lookback_days first, falls back to global discovery_lookback_period if NULL - Backend: GET/POST /api/watchlist/artist//config includes lookback_days. Changing lookback clears last_scan_timestamp to force a rescan with the new window - Frontend: dropdown with 8 options in artist config modal - Fully backwards compatible — existing artists unchanged --- core/watchlist_scanner.py | 28 +++++++++++++++++----------- database/music_database.py | 19 ++++++++++++++++++- web_server.py | 23 ++++++++++++++++++++--- webui/index.html | 18 ++++++++++++++++++ webui/static/script.js | 4 ++++ 5 files changed, 77 insertions(+), 15 deletions(-) diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index e282aaea..cc0d91e7 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -502,7 +502,7 @@ class WatchlistScanner: logger.warning(f"No valid client/ID for {watchlist_artist.artist_name}") return None - albums = self._get_artist_discography_with_client(client, artist_id, last_scan_timestamp) + albums = self._get_artist_discography_with_client(client, artist_id, last_scan_timestamp, lookback_days=watchlist_artist.lookback_days) # If primary provider returned nothing, try the other provider as fallback if not albums: @@ -533,7 +533,7 @@ class WatchlistScanner: if fallback_client and fallback_id: logger.info(f"{provider.capitalize()} returned no albums for {watchlist_artist.artist_name}, falling back to {'iTunes' if provider == 'spotify' else 'Spotify'}") - albums = self._get_artist_discography_with_client(fallback_client, fallback_id, last_scan_timestamp) + albums = self._get_artist_discography_with_client(fallback_client, fallback_id, last_scan_timestamp, lookback_days=watchlist_artist.lookback_days) return albums @@ -725,7 +725,7 @@ class WatchlistScanner: logger.warning(f"Could not update artist image for {watchlist_artist.artist_name}: {img_error}") # Get artist discography using active provider - albums = self._get_artist_discography_with_client(client, artist_id, watchlist_artist.last_scan_timestamp) + albums = self._get_artist_discography_with_client(client, artist_id, watchlist_artist.last_scan_timestamp, lookback_days=watchlist_artist.lookback_days) if albums is None: return ScanResult( @@ -863,14 +863,19 @@ class WatchlistScanner: # Determine cutoff date for filtering cutoff_timestamp = last_scan_timestamp - # If no last scan timestamp, use lookback period setting + # If no last scan timestamp, use per-artist lookback or global setting if cutoff_timestamp is None: - lookback_period = self._get_lookback_period_setting() - if lookback_period != 'all': - # Convert period to days and create cutoff date (use UTC) - days = int(lookback_period) - cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=days) - logger.info(f"Using lookback period: {lookback_period} days (cutoff: {cutoff_timestamp})") + if lookback_days is not None: + # Per-artist override + cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=lookback_days) + logger.info(f"Using per-artist lookback: {lookback_days} days (cutoff: {cutoff_timestamp})") + else: + # Global setting + lookback_period = self._get_lookback_period_setting() + if lookback_period != 'all': + days = int(lookback_period) + cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=days) + logger.info(f"Using global lookback period: {lookback_period} days (cutoff: {cutoff_timestamp})") # Filter by release date if we have a cutoff timestamp if cutoff_timestamp: @@ -889,7 +894,7 @@ class WatchlistScanner: logger.error(f"Error getting discography for artist {spotify_artist_id}: {e}") return None - def _get_artist_discography_with_client(self, client, artist_id: str, last_scan_timestamp: Optional[datetime] = None) -> Optional[List]: + def _get_artist_discography_with_client(self, client, artist_id: str, last_scan_timestamp: Optional[datetime] = None, lookback_days: Optional[int] = None) -> Optional[List]: """ Get artist's discography using the specified client, optionally filtered by release date. @@ -898,6 +903,7 @@ class WatchlistScanner: artist_id: Artist ID for the given client last_scan_timestamp: Only return releases after this date (for incremental scans) If None, uses lookback period setting from database + lookback_days: Per-artist override for lookback period (None = use global setting) """ try: # Get all artist albums (albums + singles) diff --git a/database/music_database.py b/database/music_database.py index e9386cf9..4ee02bb2 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -96,6 +96,7 @@ class WatchlistArtist: include_acoustic: bool = False include_compilations: bool = False include_instrumentals: bool = False + lookback_days: Optional[int] = None # Per-artist override; None = use global setting profile_id: int = 1 @dataclass @@ -305,6 +306,9 @@ class MusicDatabase: # Add content type filter columns to watchlist_artists (migration) self._add_watchlist_content_type_filters(cursor) + # Add per-artist lookback_days column to watchlist_artists (migration) + self._add_watchlist_lookback_days_column(cursor) + # Add iTunes artist ID column to watchlist_artists (migration) self._add_watchlist_itunes_id_column(cursor) @@ -1168,6 +1172,17 @@ class MusicDatabase: logger.error(f"Error adding content type filter columns to watchlist_artists: {e}") # Don't raise - this is a migration, database can still function + def _add_watchlist_lookback_days_column(self, cursor): + """Add per-artist lookback_days column to watchlist_artists table""" + try: + cursor.execute("PRAGMA table_info(watchlist_artists)") + columns = [column[1] for column in cursor.fetchall()] + if 'lookback_days' not in columns: + cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN lookback_days INTEGER DEFAULT NULL") + logger.info("Added lookback_days column to watchlist_artists table") + except Exception as e: + logger.error(f"Error adding lookback_days column to watchlist_artists: {e}") + def _add_watchlist_itunes_id_column(self, cursor): """Add iTunes artist ID column to watchlist_artists table for cross-provider support""" try: @@ -6318,7 +6333,7 @@ class MusicDatabase: 'last_scan_timestamp', 'created_at', 'updated_at'] optional_columns = ['image_url', 'itunes_artist_id', 'deezer_artist_id', 'include_albums', 'include_eps', 'include_singles', 'include_live', 'include_remixes', 'include_acoustic', 'include_compilations', - 'include_instrumentals'] + 'include_instrumentals', 'lookback_days'] columns_to_select = base_columns + [col for col in optional_columns if col in existing_columns] @@ -6352,6 +6367,7 @@ class MusicDatabase: include_acoustic = bool(row['include_acoustic']) if 'include_acoustic' in existing_columns else False include_compilations = bool(row['include_compilations']) if 'include_compilations' in existing_columns else False include_instrumentals = bool(row['include_instrumentals']) if 'include_instrumentals' in existing_columns else False + lookback_days = row['lookback_days'] if 'lookback_days' in existing_columns else None watchlist_artists.append(WatchlistArtist( id=row['id'], @@ -6372,6 +6388,7 @@ class MusicDatabase: include_acoustic=include_acoustic, include_compilations=include_compilations, include_instrumentals=include_instrumentals, + lookback_days=lookback_days, profile_id=profile_id )) diff --git a/web_server.py b/web_server.py index ea765a39..89c7e0bc 100644 --- a/web_server.py +++ b/web_server.py @@ -34538,7 +34538,8 @@ def watchlist_artist_config(artist_id): SELECT include_albums, include_eps, include_singles, include_live, include_remixes, include_acoustic, include_compilations, artist_name, image_url, spotify_artist_id, itunes_artist_id, - last_scan_timestamp, date_added, include_instrumentals, deezer_artist_id + last_scan_timestamp, date_added, include_instrumentals, deezer_artist_id, + lookback_days FROM watchlist_artists WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? """, (artist_id, artist_id, artist_id)) @@ -34640,6 +34641,7 @@ def watchlist_artist_config(artist_id): 'include_instrumentals': bool(result[13]) if result[13] is not None else False, 'last_scan_timestamp': result[11], 'date_added': result[12], + 'lookback_days': result[15] if len(result) > 15 else None, } return jsonify({ @@ -34666,6 +34668,10 @@ def watchlist_artist_config(artist_id): include_acoustic = data.get('include_acoustic', False) include_compilations = data.get('include_compilations', False) include_instrumentals = data.get('include_instrumentals', False) + lookback_days = data.get('lookback_days', None) # None = use global setting + # Validate lookback_days if provided + if lookback_days is not None: + lookback_days = int(lookback_days) if lookback_days != '' else None # Validate at least one release type is selected if not (include_albums or include_eps or include_singles): @@ -34674,16 +34680,27 @@ def watchlist_artist_config(artist_id): # Update database conn = sqlite3.connect(str(database.database_path)) cursor = conn.cursor() + + # Check if lookback_days changed — if so, clear last_scan_timestamp to force rescan + cursor.execute(""" + SELECT lookback_days FROM watchlist_artists + WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? + """, (artist_id, artist_id, artist_id)) + old_row = cursor.fetchone() + old_lookback = old_row[0] if old_row else None + lookback_changed = old_lookback != lookback_days + cursor.execute(""" UPDATE watchlist_artists SET include_albums = ?, include_eps = ?, include_singles = ?, include_live = ?, include_remixes = ?, include_acoustic = ?, include_compilations = ?, - include_instrumentals = ?, + include_instrumentals = ?, lookback_days = ?, + last_scan_timestamp = CASE WHEN ? THEN NULL ELSE last_scan_timestamp END, updated_at = CURRENT_TIMESTAMP WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? """, (int(include_albums), int(include_eps), int(include_singles), int(include_live), int(include_remixes), int(include_acoustic), int(include_compilations), - int(include_instrumentals), + int(include_instrumentals), lookback_days, lookback_changed, artist_id, artist_id, artist_id)) conn.commit() diff --git a/webui/index.html b/webui/index.html index e4439d6d..3c549ba3 100644 --- a/webui/index.html +++ b/webui/index.html @@ -5925,6 +5925,24 @@ +
+

Scan Lookback

+

How far back to look for releases on first scan of this artist

+
+ +
+
+