diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index 6c6b7a95..8fec5b6f 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -792,16 +792,33 @@ class WatchlistScanner: time.sleep(0.3) # 300ms breathing room # Determine cutoff date for filtering - cutoff_timestamp = last_scan_timestamp - - # If no last scan timestamp, use lookback period 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})") + lookback_period = self._get_lookback_period_setting() + + # If lookback is 'all', always return everything regardless of scan timestamp + if lookback_period == 'all': + cutoff_timestamp = None + elif last_scan_timestamp is not None: + cutoff_timestamp = last_scan_timestamp + + # Check if a lookback period change requires a one-time wider window + rescan_cutoff = self._get_rescan_cutoff() + if rescan_cutoff == 'all': + logger.info(f"Lookback period changed to 'all' — returning full discography") + cutoff_timestamp = None + elif rescan_cutoff is not None: + scan_ts = cutoff_timestamp + if scan_ts.tzinfo is None: + scan_ts = scan_ts.replace(tzinfo=timezone.utc) + if rescan_cutoff.tzinfo is None: + rescan_cutoff = rescan_cutoff.replace(tzinfo=timezone.utc) + if rescan_cutoff < scan_ts: + logger.info(f"Lookback period change detected — expanding cutoff from {cutoff_timestamp} to {rescan_cutoff}") + cutoff_timestamp = rescan_cutoff + else: + # No scan timestamp — use lookback period + 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})") # Filter by release date if we have a cutoff timestamp if cutoff_timestamp: @@ -998,6 +1015,45 @@ class WatchlistScanner: logger.warning(f"Error getting lookback period setting, defaulting to 30 days: {e}") return '30' + def _get_rescan_cutoff(self): + """ + Check if a lookback period change requires a one-time wider scan window. + + When the lookback period is expanded, a 'watchlist_rescan_cutoff' metadata key + is set with the new cutoff date. This method returns that cutoff so the scanner + can use the wider window for artists scanned before the change. After a full + scan cycle, the key is cleared by _clear_rescan_cutoff(). + + Returns: + datetime cutoff if a rescan is pending with a specific date, + 'all' string if lookback was set to entire discography, + None if no rescan is pending + """ + try: + with self.database._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM metadata WHERE key = 'watchlist_rescan_cutoff'") + row = cursor.fetchone() + if row is not None: + val = row['value'] + if val == '': + return 'all' # Lookback set to 'all' — scan everything + return datetime.fromisoformat(val) + except Exception as e: + logger.debug(f"Error reading rescan cutoff: {e}") + return None + + def _clear_rescan_cutoff(self): + """Clear the one-time rescan cutoff after a full scan cycle completes.""" + try: + with self.database._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM metadata WHERE key = 'watchlist_rescan_cutoff'") + conn.commit() + logger.info("Cleared watchlist rescan cutoff flag") + except Exception as e: + logger.debug(f"Error clearing rescan cutoff: {e}") + def is_album_after_timestamp(self, album, timestamp: datetime) -> bool: """Check if album was released after the given timestamp""" try: diff --git a/database/music_database.py b/database/music_database.py index 65b10ced..6850c01b 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -4895,19 +4895,14 @@ class MusicDatabase: return 0 def clear_wishlist(self, profile_id: int = 1) -> bool: - """Clear all tracks from the wishlist for the given profile and reset scan timestamps""" + """Clear all tracks from the wishlist for the given profile""" try: with self._get_connection() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM wishlist_tracks WHERE profile_id = ?", (profile_id,)) cleared_count = cursor.rowcount - # Reset last_scan_timestamp on this profile's watchlist artists so the next scan - # uses the lookback period setting (e.g. "entire discography") instead - # of only finding albums released after the old scan date - cursor.execute("UPDATE watchlist_artists SET last_scan_timestamp = NULL WHERE profile_id = ?", (profile_id,)) - reset_count = cursor.rowcount conn.commit() - logger.info(f"Cleared {cleared_count} tracks from wishlist, reset scan timestamps on {reset_count} artists (profile: {profile_id})") + logger.info(f"Cleared {cleared_count} tracks from wishlist (profile: {profile_id})") return True except Exception as e: logger.error(f"Error clearing wishlist: {e}") diff --git a/web_server.py b/web_server.py index e9cdf157..e262725b 100644 --- a/web_server.py +++ b/web_server.py @@ -17367,14 +17367,25 @@ def set_discovery_lookback_period(): VALUES ('discovery_lookback_period', ?, CURRENT_TIMESTAMP) """, (period,)) - # When expanding the lookback window (especially to "entire disco"), - # reset scan timestamps so the next scan re-discovers older releases - # that were filtered out under the previous narrower setting - cursor.execute("UPDATE watchlist_artists SET last_scan_timestamp = NULL") - reset_count = cursor.rowcount + # Set a one-time rescan cutoff so the next scan cycle uses the new + # lookback window for artists that were already scanned under the old setting. + # This avoids wiping last_scan_timestamp (which is needed for UI display). + if period == 'all': + # 'all' means no cutoff — store empty to signal "scan everything" + rescan_value = '' + else: + from datetime import datetime, timedelta, timezone + cutoff = datetime.now(timezone.utc) - timedelta(days=int(period)) + rescan_value = cutoff.isoformat() + + cursor.execute(""" + INSERT OR REPLACE INTO metadata (key, value, updated_at) + VALUES ('watchlist_rescan_cutoff', ?, CURRENT_TIMESTAMP) + """, (rescan_value,)) + conn.commit() - print(f"✅ Discovery lookback period set to: {period}, reset scan timestamps on {reset_count} artists") + print(f"✅ Discovery lookback period set to: {period}") return jsonify({"success": True, "period": period}) except Exception as e: @@ -28821,6 +28832,12 @@ def start_watchlist_scan(): # Resume enrichment workers if we paused them _resume_enrichment_workers(_ew_state, 'watchlist scan') + # Clear one-time rescan cutoff after full scan cycle + try: + scanner._clear_rescan_cutoff() + except Exception: + pass + # Always reset flag when scan completes (success or error) with watchlist_timer_lock: watchlist_auto_scanning = False @@ -29809,6 +29826,12 @@ def _process_watchlist_scan_automatically(automation_id=None): # Resume enrichment workers if we paused them _resume_enrichment_workers(_ew_state, 'auto-watchlist scan') + # Clear one-time rescan cutoff after full scan cycle + try: + scanner._clear_rescan_cutoff() + except Exception: + pass + # Always reset flag with watchlist_timer_lock: watchlist_auto_scanning = False