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 @@ +
How far back to look for releases on first scan of this artist
+