Preserve watchlist scan timestamps for UI display instead of wiping on lookback changes
This commit is contained in:
parent
32f1cc946c
commit
e3fdb12d78
3 changed files with 96 additions and 22 deletions
|
|
@ -792,16 +792,33 @@ class WatchlistScanner:
|
||||||
time.sleep(0.3) # 300ms breathing room
|
time.sleep(0.3) # 300ms breathing room
|
||||||
|
|
||||||
# Determine cutoff date for filtering
|
# Determine cutoff date for filtering
|
||||||
cutoff_timestamp = last_scan_timestamp
|
lookback_period = self._get_lookback_period_setting()
|
||||||
|
|
||||||
# If no last scan timestamp, use lookback period setting
|
# If lookback is 'all', always return everything regardless of scan timestamp
|
||||||
if cutoff_timestamp is None:
|
if lookback_period == 'all':
|
||||||
lookback_period = self._get_lookback_period_setting()
|
cutoff_timestamp = None
|
||||||
if lookback_period != 'all':
|
elif last_scan_timestamp is not None:
|
||||||
# Convert period to days and create cutoff date (use UTC)
|
cutoff_timestamp = last_scan_timestamp
|
||||||
days = int(lookback_period)
|
|
||||||
cutoff_timestamp = datetime.now(timezone.utc) - timedelta(days=days)
|
# Check if a lookback period change requires a one-time wider window
|
||||||
logger.info(f"Using lookback period: {lookback_period} days (cutoff: {cutoff_timestamp})")
|
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
|
# Filter by release date if we have a cutoff timestamp
|
||||||
if cutoff_timestamp:
|
if cutoff_timestamp:
|
||||||
|
|
@ -998,6 +1015,45 @@ class WatchlistScanner:
|
||||||
logger.warning(f"Error getting lookback period setting, defaulting to 30 days: {e}")
|
logger.warning(f"Error getting lookback period setting, defaulting to 30 days: {e}")
|
||||||
return '30'
|
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:
|
def is_album_after_timestamp(self, album, timestamp: datetime) -> bool:
|
||||||
"""Check if album was released after the given timestamp"""
|
"""Check if album was released after the given timestamp"""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -4895,19 +4895,14 @@ class MusicDatabase:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def clear_wishlist(self, profile_id: int = 1) -> bool:
|
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:
|
try:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("DELETE FROM wishlist_tracks WHERE profile_id = ?", (profile_id,))
|
cursor.execute("DELETE FROM wishlist_tracks WHERE profile_id = ?", (profile_id,))
|
||||||
cleared_count = cursor.rowcount
|
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()
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error clearing wishlist: {e}")
|
logger.error(f"Error clearing wishlist: {e}")
|
||||||
|
|
|
||||||
|
|
@ -17367,14 +17367,25 @@ def set_discovery_lookback_period():
|
||||||
VALUES ('discovery_lookback_period', ?, CURRENT_TIMESTAMP)
|
VALUES ('discovery_lookback_period', ?, CURRENT_TIMESTAMP)
|
||||||
""", (period,))
|
""", (period,))
|
||||||
|
|
||||||
# When expanding the lookback window (especially to "entire disco"),
|
# Set a one-time rescan cutoff so the next scan cycle uses the new
|
||||||
# reset scan timestamps so the next scan re-discovers older releases
|
# lookback window for artists that were already scanned under the old setting.
|
||||||
# that were filtered out under the previous narrower setting
|
# This avoids wiping last_scan_timestamp (which is needed for UI display).
|
||||||
cursor.execute("UPDATE watchlist_artists SET last_scan_timestamp = NULL")
|
if period == 'all':
|
||||||
reset_count = cursor.rowcount
|
# '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()
|
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})
|
return jsonify({"success": True, "period": period})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -28821,6 +28832,12 @@ def start_watchlist_scan():
|
||||||
# Resume enrichment workers if we paused them
|
# Resume enrichment workers if we paused them
|
||||||
_resume_enrichment_workers(_ew_state, 'watchlist scan')
|
_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)
|
# Always reset flag when scan completes (success or error)
|
||||||
with watchlist_timer_lock:
|
with watchlist_timer_lock:
|
||||||
watchlist_auto_scanning = False
|
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 if we paused them
|
||||||
_resume_enrichment_workers(_ew_state, 'auto-watchlist scan')
|
_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
|
# Always reset flag
|
||||||
with watchlist_timer_lock:
|
with watchlist_timer_lock:
|
||||||
watchlist_auto_scanning = False
|
watchlist_auto_scanning = False
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue