Scope automation-triggered watchlist scans to the calling profile & Fix watchlist scan silently skipping all albums due to metadata cache returning incomplete data

pull/253/head
Broque Thomas 2 months ago
parent 3bbbfb125e
commit 0b8bfa1e6b

@ -518,7 +518,7 @@ class AutomationEngine:
# --- Schedule Execution (timer-based) ---
def run_automation(self, automation_id, skip_delay=False):
def run_automation(self, automation_id, skip_delay=False, profile_id=None):
"""Execute: check guard → run action → send notification → update stats → reschedule."""
if not self._running:
return
@ -553,6 +553,8 @@ class AutomationEngine:
# Inject automation identity for progress tracking
action_config['_automation_id'] = automation_id
action_config['_automation_name'] = auto.get('name', '')
if profile_id is not None:
action_config['_profile_id'] = profile_id
# Action delay (skipped for manual run_now)
delay_minutes = action_config.get('delay', 0)
@ -657,16 +659,21 @@ class AutomationEngine:
if self._running:
self.schedule_automation(automation_id)
def run_now(self, automation_id):
def run_now(self, automation_id, profile_id=None):
"""Manual trigger — run immediately in a background thread.
Always uses run_automation (skips condition checks and action delay)."""
Always uses run_automation (skips condition checks and action delay).
Args:
automation_id: ID of automation to run
profile_id: If provided, scopes the run to this profile (manual trigger from UI)
"""
auto = self.db.get_automation(automation_id)
if not auto:
return False
thread = threading.Thread(
target=self.run_automation,
args=(automation_id, True),
args=(automation_id, True, profile_id),
daemon=True,
name=f'automation-run-{automation_id}'
)

@ -1075,9 +1075,14 @@ class SpotifyClient:
cached = cache.get_entity(source, 'track', track_id)
if cached:
if source == 'spotify':
return self._build_enhanced_track(cached)
# iTunes cache hit — delegate to iTunesClient which reconstructs enhanced format
return self._itunes.get_track_details(track_id)
# Validate cache has full track data (not simplified from get_album_tracks)
if 'album' in cached:
return self._build_enhanced_track(cached)
# Simplified track cached by get_album_tracks — treat as cache miss
logger.debug(f"Cache hit for track {track_id} lacks album data, fetching full data")
else:
# iTunes cache hit — delegate to iTunesClient which reconstructs enhanced format
return self._itunes.get_track_details(track_id)
if self.is_spotify_authenticated():
try:
@ -1159,9 +1164,14 @@ class SpotifyClient:
cached = cache.get_entity(source, 'album', album_id)
if cached:
if source == 'spotify':
return cached # Spotify raw format is the expected format
# iTunes cache hit — delegate to iTunesClient which reconstructs Spotify-compatible format
return self._itunes.get_album(album_id)
# Validate cache has full album data (not simplified from artist_albums)
if 'tracks' in cached:
return cached
# Simplified album cached by get_artist_albums — treat as cache miss
logger.debug(f"Cache hit for album {album_id} lacks tracks, fetching full data")
else:
# iTunes cache hit — delegate to iTunesClient which reconstructs Spotify-compatible format
return self._itunes.get_album(album_id)
if self.is_spotify_authenticated():
try:

@ -366,7 +366,10 @@ def _register_automation_handlers():
def _auto_scan_watchlist(config):
try:
pre_state_id = id(watchlist_scan_state)
_process_watchlist_scan_automatically(automation_id=config.get('_automation_id'))
_process_watchlist_scan_automatically(
automation_id=config.get('_automation_id'),
profile_id=config.get('_profile_id')
)
# Only report stats if a fresh scan actually ran (state dict was reassigned)
if id(watchlist_scan_state) != pre_state_id:
summary = watchlist_scan_state.get('summary', {})
@ -4752,7 +4755,7 @@ def run_automation_endpoint(automation_id):
try:
if not automation_engine:
return jsonify({"error": "Automation engine not available"}), 500
success = automation_engine.run_now(automation_id)
success = automation_engine.run_now(automation_id, profile_id=get_current_profile_id())
if not success:
return jsonify({"error": "Automation not found"}), 404
return jsonify({"success": True})
@ -28788,6 +28791,7 @@ def start_watchlist_scan():
# Get album tracks using provider-aware method
album_data = scanner.metadata_service.get_album(album.id)
if not album_data or 'tracks' not in album_data:
logger.debug(f"Skipping album {album.name} (id={album.id}): no track data returned")
continue
tracks = album_data['tracks']['items']
@ -29565,11 +29569,18 @@ watchlist_scan_state = {
'error': None
}
def _process_watchlist_scan_automatically(automation_id=None):
"""Main automatic scanning logic that runs in background thread."""
def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
"""Main automatic scanning logic that runs in background thread.
Args:
automation_id: ID of the automation triggering this scan
profile_id: If provided, only scan this profile's watchlist (manual trigger).
If None, scan all profiles (scheduled automation).
"""
global watchlist_auto_scanning, watchlist_auto_scanning_timestamp, watchlist_scan_state
print("🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan...")
scope_label = f"profile {profile_id}" if profile_id else "all profiles"
print(f"🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan ({scope_label})...")
_ew_state = {}
@ -29597,12 +29608,19 @@ def _process_watchlist_scan_automatically(automation_id=None):
from core.watchlist_scanner import get_watchlist_scanner
from database.music_database import get_database
# Check if we have artists to scan across all profiles
database = get_database()
# Auto-scan covers all profiles
all_profiles = database.get_all_profiles()
watchlist_count = sum(database.get_watchlist_count(profile_id=p['id']) for p in all_profiles)
print(f"🔍 [Auto-Watchlist] Watchlist count check: {watchlist_count} artists found across {len(all_profiles)} profiles")
# Determine which profiles to scan
if profile_id:
# Manual trigger — scan only the triggering profile
scan_profiles = [{'id': profile_id}]
else:
# Scheduled automation — scan all profiles
scan_profiles = database.get_all_profiles()
watchlist_count = sum(database.get_watchlist_count(profile_id=p['id']) for p in scan_profiles)
profile_label = f"profile {profile_id}" if profile_id else f"{len(scan_profiles)} profiles"
print(f"🔍 [Auto-Watchlist] Watchlist count check: {watchlist_count} artists found ({profile_label})")
if watchlist_count == 0:
print(" [Auto-Watchlist] No artists in watchlist for auto-scanning.")
@ -29620,13 +29638,14 @@ def _process_watchlist_scan_automatically(automation_id=None):
print(f"👁️ [Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...")
_update_automation_progress(automation_id, progress=5, phase='Loading watchlist',
log_line=f'{watchlist_count} artists across {len(all_profiles)} profiles', log_type='info')
log_line=f'{watchlist_count} artists ({profile_label})', log_type='info')
# Get list of artists to scan (all profiles combined for auto-scan)
# Get list of artists to scan
watchlist_artists = []
for p in all_profiles:
for p in scan_profiles:
watchlist_artists.extend(database.get_watchlist_artists(profile_id=p['id']))
scanner = get_watchlist_scanner(spotify_client)
all_profiles = scan_profiles # Used later for discovery pool population
# Apply global overrides if enabled
_apply_watchlist_global_overrides(watchlist_artists)
@ -29745,6 +29764,7 @@ def _process_watchlist_scan_automatically(automation_id=None):
# Get album tracks using provider-aware method
album_data = scanner.metadata_service.get_album(album.id)
if not album_data or 'tracks' not in album_data:
logger.debug(f"Skipping album {album.name} (id={album.id}): no track data returned")
continue
tracks = album_data['tracks']['items']
@ -29840,6 +29860,23 @@ def _process_watchlist_scan_automatically(automation_id=None):
except Exception:
pass
# Fetch similar artists for discovery feature (per-profile)
try:
watchlist_scan_state['current_phase'] = 'fetching_similar_artists'
source_artist_id = artist.spotify_artist_id or artist.itunes_artist_id or str(artist.id)
artist_profile_id = getattr(artist, 'profile_id', 1)
spotify_authenticated = spotify_client and spotify_client.is_spotify_authenticated()
if database.has_fresh_similar_artists(source_artist_id, days_threshold=30, require_spotify=spotify_authenticated, profile_id=artist_profile_id):
print(f" Similar artists for {artist.artist_name} are cached and fresh (profile {artist_profile_id})")
scanner._backfill_similar_artists_itunes_ids(source_artist_id, profile_id=artist_profile_id)
else:
print(f" Fetching similar artists for {artist.artist_name} (profile {artist_profile_id})...")
scanner.update_similar_artists(artist, profile_id=artist_profile_id)
print(f" Similar artists updated for {artist.artist_name}")
except Exception as similar_error:
print(f" ⚠️ Failed to update similar artists for {artist.artist_name}: {similar_error}")
# Delay between artists
if i < len(watchlist_artists) - 1:
watchlist_scan_state['current_phase'] = 'rate_limiting'

Loading…
Cancel
Save