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) ---
|
# --- 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."""
|
"""Execute: check guard → run action → send notification → update stats → reschedule."""
|
||||||
if not self._running:
|
if not self._running:
|
||||||
return
|
return
|
||||||
|
|
@ -553,6 +553,8 @@ class AutomationEngine:
|
||||||
# Inject automation identity for progress tracking
|
# Inject automation identity for progress tracking
|
||||||
action_config['_automation_id'] = automation_id
|
action_config['_automation_id'] = automation_id
|
||||||
action_config['_automation_name'] = auto.get('name', '')
|
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)
|
# Action delay (skipped for manual run_now)
|
||||||
delay_minutes = action_config.get('delay', 0)
|
delay_minutes = action_config.get('delay', 0)
|
||||||
|
|
@ -657,16 +659,21 @@ class AutomationEngine:
|
||||||
if self._running:
|
if self._running:
|
||||||
self.schedule_automation(automation_id)
|
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.
|
"""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)
|
auto = self.db.get_automation(automation_id)
|
||||||
if not auto:
|
if not auto:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self.run_automation,
|
target=self.run_automation,
|
||||||
args=(automation_id, True),
|
args=(automation_id, True, profile_id),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f'automation-run-{automation_id}'
|
name=f'automation-run-{automation_id}'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1075,9 +1075,14 @@ class SpotifyClient:
|
||||||
cached = cache.get_entity(source, 'track', track_id)
|
cached = cache.get_entity(source, 'track', track_id)
|
||||||
if cached:
|
if cached:
|
||||||
if source == 'spotify':
|
if source == 'spotify':
|
||||||
return self._build_enhanced_track(cached)
|
# Validate cache has full track data (not simplified from get_album_tracks)
|
||||||
# iTunes cache hit — delegate to iTunesClient which reconstructs enhanced format
|
if 'album' in cached:
|
||||||
return self._itunes.get_track_details(track_id)
|
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():
|
if self.is_spotify_authenticated():
|
||||||
try:
|
try:
|
||||||
|
|
@ -1159,9 +1164,14 @@ class SpotifyClient:
|
||||||
cached = cache.get_entity(source, 'album', album_id)
|
cached = cache.get_entity(source, 'album', album_id)
|
||||||
if cached:
|
if cached:
|
||||||
if source == 'spotify':
|
if source == 'spotify':
|
||||||
return cached # Spotify raw format is the expected format
|
# Validate cache has full album data (not simplified from artist_albums)
|
||||||
# iTunes cache hit — delegate to iTunesClient which reconstructs Spotify-compatible format
|
if 'tracks' in cached:
|
||||||
return self._itunes.get_album(album_id)
|
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():
|
if self.is_spotify_authenticated():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,10 @@ def _register_automation_handlers():
|
||||||
def _auto_scan_watchlist(config):
|
def _auto_scan_watchlist(config):
|
||||||
try:
|
try:
|
||||||
pre_state_id = id(watchlist_scan_state)
|
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)
|
# Only report stats if a fresh scan actually ran (state dict was reassigned)
|
||||||
if id(watchlist_scan_state) != pre_state_id:
|
if id(watchlist_scan_state) != pre_state_id:
|
||||||
summary = watchlist_scan_state.get('summary', {})
|
summary = watchlist_scan_state.get('summary', {})
|
||||||
|
|
@ -4752,7 +4755,7 @@ def run_automation_endpoint(automation_id):
|
||||||
try:
|
try:
|
||||||
if not automation_engine:
|
if not automation_engine:
|
||||||
return jsonify({"error": "Automation engine not available"}), 500
|
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:
|
if not success:
|
||||||
return jsonify({"error": "Automation not found"}), 404
|
return jsonify({"error": "Automation not found"}), 404
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
@ -28788,6 +28791,7 @@ def start_watchlist_scan():
|
||||||
# Get album tracks using provider-aware method
|
# Get album tracks using provider-aware method
|
||||||
album_data = scanner.metadata_service.get_album(album.id)
|
album_data = scanner.metadata_service.get_album(album.id)
|
||||||
if not album_data or 'tracks' not in album_data:
|
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
|
continue
|
||||||
|
|
||||||
tracks = album_data['tracks']['items']
|
tracks = album_data['tracks']['items']
|
||||||
|
|
@ -29565,11 +29569,18 @@ watchlist_scan_state = {
|
||||||
'error': None
|
'error': None
|
||||||
}
|
}
|
||||||
|
|
||||||
def _process_watchlist_scan_automatically(automation_id=None):
|
def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
|
||||||
"""Main automatic scanning logic that runs in background thread."""
|
"""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
|
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 = {}
|
_ew_state = {}
|
||||||
|
|
||||||
|
|
@ -29597,12 +29608,19 @@ def _process_watchlist_scan_automatically(automation_id=None):
|
||||||
from core.watchlist_scanner import get_watchlist_scanner
|
from core.watchlist_scanner import get_watchlist_scanner
|
||||||
from database.music_database import get_database
|
from database.music_database import get_database
|
||||||
|
|
||||||
# Check if we have artists to scan across all profiles
|
|
||||||
database = get_database()
|
database = get_database()
|
||||||
# Auto-scan covers all profiles
|
|
||||||
all_profiles = database.get_all_profiles()
|
# Determine which profiles to scan
|
||||||
watchlist_count = sum(database.get_watchlist_count(profile_id=p['id']) for p in all_profiles)
|
if profile_id:
|
||||||
print(f"🔍 [Auto-Watchlist] Watchlist count check: {watchlist_count} artists found across {len(all_profiles)} profiles")
|
# 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:
|
if watchlist_count == 0:
|
||||||
print("ℹ️ [Auto-Watchlist] No artists in watchlist for auto-scanning.")
|
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...")
|
print(f"👁️ [Auto-Watchlist] Found {watchlist_count} artists in watchlist, starting automatic scan...")
|
||||||
_update_automation_progress(automation_id, progress=5, phase='Loading watchlist',
|
_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 = []
|
watchlist_artists = []
|
||||||
for p in all_profiles:
|
for p in scan_profiles:
|
||||||
watchlist_artists.extend(database.get_watchlist_artists(profile_id=p['id']))
|
watchlist_artists.extend(database.get_watchlist_artists(profile_id=p['id']))
|
||||||
scanner = get_watchlist_scanner(spotify_client)
|
scanner = get_watchlist_scanner(spotify_client)
|
||||||
|
all_profiles = scan_profiles # Used later for discovery pool population
|
||||||
|
|
||||||
# Apply global overrides if enabled
|
# Apply global overrides if enabled
|
||||||
_apply_watchlist_global_overrides(watchlist_artists)
|
_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
|
# Get album tracks using provider-aware method
|
||||||
album_data = scanner.metadata_service.get_album(album.id)
|
album_data = scanner.metadata_service.get_album(album.id)
|
||||||
if not album_data or 'tracks' not in album_data:
|
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
|
continue
|
||||||
|
|
||||||
tracks = album_data['tracks']['items']
|
tracks = album_data['tracks']['items']
|
||||||
|
|
@ -29840,6 +29860,23 @@ def _process_watchlist_scan_automatically(automation_id=None):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
# Delay between artists
|
||||||
if i < len(watchlist_artists) - 1:
|
if i < len(watchlist_artists) - 1:
|
||||||
watchlist_scan_state['current_phase'] = 'rate_limiting'
|
watchlist_scan_state['current_phase'] = 'rate_limiting'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue