Scope automation-triggered watchlist scans to the calling profile & Fix watchlist scan silently skipping all albums due to metadata cache returning incomplete data
This commit is contained in:
parent
3bbbfb125e
commit
0b8bfa1e6b
3 changed files with 77 additions and 23 deletions
|
|
@ -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…
Reference in a new issue