Compare commits

...

5 Commits

Author SHA1 Message Date
sbcrumb 88b75230a7 Bump version to 2.0.21 to resolve merge conflict with dev branch
Local Docker Build (Dev) / build-dev (pull_request) Successful in 4s
2025-10-10 15:30:06 -04:00
sbcrumb 035f2f6e02 Optimize TV processor performance with database-first episode lookup
Major performance improvement for manual scans:
- Check NFOGuard database before making Sonarr API calls
- Only query Sonarr history for episodes missing from database
- Filter Sonarr episode processing to only needed episodes
- Add detailed logging for cache hits vs API calls

Expected improvement: 172 API calls -> 0-10 API calls for typical scans
2025-10-10 15:30:06 -04:00
sbcrumb 76dcc0be33 Merge pull request 'Fix rename webhook targeting using episodeId lookup' (#39) from sonarrissues into dev
Local Docker Build (Dev) / build-dev (push) Successful in 4s
Reviewed-on: #39
2025-10-10 14:17:05 -04:00
sbcrumb d6c1b73959 Fix rename webhook targeting using episodeId lookup
Found that rename events contain episodeId field which can be used to fetch specific episode details. Replaced complex episode data extraction with direct API call to /api/v3/episode/{episodeId} to get season/episode numbers for targeted processing.

This should resolve rename webhooks processing entire series instead of single episodes.
2025-10-10 14:17:05 -04:00
sbcrumb ace671ab08 Merge pull request 'Add comprehensive rename event debugging' (#38) from sonarrissues into dev
Local Docker Build (Dev) / build-dev (push) Successful in 4s
Reviewed-on: #38
2025-10-10 14:11:53 -04:00
3 changed files with 99 additions and 78 deletions
+1 -1
View File
@@ -1 +1 @@
2.0.18 2.0.21
+21 -36
View File
@@ -152,54 +152,39 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks, de
if recent_renames: if recent_renames:
# Take the most recent rename event # Take the most recent rename event
latest_rename = recent_renames[0] latest_rename = recent_renames[0]
print(f"DEBUG: Latest rename event keys: {list(latest_rename.keys())}") print(f"DEBUG: Processing latest rename event")
print(f"DEBUG: Latest rename event full structure: {latest_rename}")
# Try different possible episode data locations # Extract episodeId directly from the rename event
episode_info = latest_rename.get("episode", {}) episode_id = latest_rename.get("episodeId")
episode_file_info = latest_rename.get("episodeFile", {}) print(f"DEBUG: Found episodeId {episode_id} in rename event")
data_info = latest_rename.get("data", {})
print(f"DEBUG: Episode info: {episode_info}") if episode_id:
print(f"DEBUG: EpisodeFile info: {episode_file_info}") # Fetch episode details using the episodeId
print(f"DEBUG: Data info: {data_info}") episode_detail_url = f"{config.sonarr_url}/api/v3/episode/{episode_id}"
episode_response = requests.get(episode_detail_url, headers={"X-Api-Key": os.environ.get("SONARR_API_KEY", "")}, timeout=10)
# Try to extract episode info from any available source if episode_response.status_code == 200:
season_num = None episode_detail = episode_response.json()
episode_num = None season_num = episode_detail.get("seasonNumber")
episode_id = None episode_num = episode_detail.get("episodeNumber")
episode_title = None episode_title = episode_detail.get("title")
# Check episode object first print(f"DEBUG: Episode details - Season: {season_num}, Episode: {episode_num}, Title: {episode_title}")
if episode_info:
season_num = episode_info.get("seasonNumber")
episode_num = episode_info.get("episodeNumber")
episode_id = episode_info.get("id")
episode_title = episode_info.get("title")
# Check episodeFile object if season_num is not None and episode_num is not None:
if not season_num and episode_file_info:
season_num = episode_file_info.get("seasonNumber")
episode_num = episode_file_info.get("episodeNumber")
episode_id = episode_file_info.get("episodeId") or episode_file_info.get("id")
# Check data object
if not season_num and data_info:
season_num = data_info.get("seasonNumber")
episode_num = data_info.get("episodeNumber")
print(f"DEBUG: Extracted - Season: {season_num}, Episode: {episode_num}, ID: {episode_id}")
if season_num and episode_num:
episodes_data = [{ episodes_data = [{
"seasonNumber": season_num, "seasonNumber": season_num,
"episodeNumber": episode_num, "episodeNumber": episode_num,
"id": episode_id, "id": episode_id,
"title": episode_title "title": episode_title
}] }]
print(f"INFO: Found recently renamed episode from history: S{season_num:02d}E{episode_num:02d}") print(f"INFO: Successfully identified renamed episode: S{season_num:02d}E{episode_num:02d} - {episode_title}")
else: else:
print(f"DEBUG: Could not extract episode data from rename event") print(f"DEBUG: Episode details missing season/episode numbers")
else:
print(f"DEBUG: Failed to fetch episode details: {episode_response.status_code}")
else:
print(f"DEBUG: No episodeId found in rename event")
else: else:
print(f"DEBUG: No recent rename events found in last hour") print(f"DEBUG: No recent rename events found in last hour")
else: else:
+45 -9
View File
@@ -102,14 +102,39 @@ class TVProcessor:
def _gather_episode_dates(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]]) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]: def _gather_episode_dates(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]]) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]:
"""Gather episode air dates and date added information""" """Gather episode air dates and date added information with database-first optimization"""
episode_dates = {} episode_dates = {}
episodes_needing_lookup = []
# Get data from Sonarr first if available # OPTIMIZATION: Check database first for existing dates
sonarr_episodes = self._get_sonarr_episodes(imdb_id) _log("DEBUG", f"Checking database for existing episode dates for {len(disk_episodes)} episodes")
db_cache_hits = 0
# Get data from external sources for missing information
for (season, episode) in disk_episodes: for (season, episode) in disk_episodes:
# Try database first - this is much faster than API calls
db_result = self.db.get_episode_date(imdb_id, season, episode)
if db_result and db_result.get('dateadded'):
# Found in database - use cached data
aired = db_result.get('aired')
dateadded = db_result.get('dateadded')
source = db_result.get('source', 'database_cache')
episode_dates[(season, episode)] = (aired, dateadded, source)
db_cache_hits += 1
_log("DEBUG", f"Database cache hit for S{season:02d}E{episode:02d}: {dateadded}")
else:
# Not in database or incomplete - needs lookup
episodes_needing_lookup.append((season, episode))
_log("INFO", f"Database cache hits: {db_cache_hits}/{len(disk_episodes)} episodes. Need API lookup: {len(episodes_needing_lookup)}")
# Only call Sonarr API for episodes not in database
if episodes_needing_lookup:
_log("DEBUG", f"Querying Sonarr for {len(episodes_needing_lookup)} episodes missing from database")
sonarr_episodes = self._get_sonarr_episodes(imdb_id, episodes_needing_lookup)
# Process episodes that needed lookup
for (season, episode) in episodes_needing_lookup:
aired = None aired = None
dateadded = None dateadded = None
source = "unknown" source = "unknown"
@@ -120,7 +145,6 @@ class TVProcessor:
aired = sonarr_data.get('airDate') aired = sonarr_data.get('airDate')
dateadded = sonarr_data.get('dateAdded') dateadded = sonarr_data.get('dateAdded')
if dateadded: if dateadded:
# Use more specific source to indicate this came from Sonarr import history
source = "sonarr:history.import" source = "sonarr:history.import"
# Fallback to external sources if needed # Fallback to external sources if needed
@@ -131,8 +155,7 @@ class TVProcessor:
if not dateadded: if not dateadded:
source = "external" source = "external"
# ONLY use air date as fallback for dateadded if we have no Sonarr import date at all # Use air date as fallback for dateadded if no import date found
# and if we couldn't get any import information from Sonarr history
if not dateadded and aired and source != "sonarr:history.import": if not dateadded and aired and source != "sonarr:history.import":
dateadded = aired dateadded = aired
source = f"{source}_fallback" if source != "unknown" else "aired_fallback" source = f"{source}_fallback" if source != "unknown" else "aired_fallback"
@@ -141,8 +164,8 @@ class TVProcessor:
return episode_dates return episode_dates
def _get_sonarr_episodes(self, imdb_id: str) -> Dict[Tuple[int, int], Dict[str, Any]]: def _get_sonarr_episodes(self, imdb_id: str, episodes_filter: List[Tuple[int, int]] = None) -> Dict[Tuple[int, int], Dict[str, Any]]:
"""Get episode information from Sonarr including import history""" """Get episode information from Sonarr including import history - optimized to only fetch needed episodes"""
try: try:
series_data = self.sonarr.series_by_imdb(imdb_id) series_data = self.sonarr.series_by_imdb(imdb_id)
if not series_data: if not series_data:
@@ -154,11 +177,20 @@ class TVProcessor:
episodes = self.sonarr.episodes_for_series(series_id) episodes = self.sonarr.episodes_for_series(series_id)
# Convert episodes_filter to set for faster lookup
filter_set = set(episodes_filter) if episodes_filter else None
episode_map = {} episode_map = {}
api_calls_made = 0
for episode in episodes: for episode in episodes:
season = episode.get('seasonNumber', 0) season = episode.get('seasonNumber', 0)
episode_num = episode.get('episodeNumber', 0) episode_num = episode.get('episodeNumber', 0)
# Skip episodes not in filter if filter is provided
if filter_set and (season, episode_num) not in filter_set:
continue
if season > 0 and episode_num > 0: if season > 0 and episode_num > 0:
# Get basic episode info # Get basic episode info
episode_data = { episode_data = {
@@ -170,6 +202,7 @@ class TVProcessor:
episode_id = episode.get('id') episode_id = episode.get('id')
if episode_id and episode.get('hasFile'): if episode_id and episode.get('hasFile'):
import_date = self.sonarr.get_episode_import_history(episode_id) import_date = self.sonarr.get_episode_import_history(episode_id)
api_calls_made += 1
if import_date: if import_date:
episode_data['dateAdded'] = import_date episode_data['dateAdded'] = import_date
_log("DEBUG", f"Got import date from history for S{season:02d}E{episode_num:02d}: {import_date}") _log("DEBUG", f"Got import date from history for S{season:02d}E{episode_num:02d}: {import_date}")
@@ -183,6 +216,9 @@ class TVProcessor:
episode_map[(season, episode_num)] = episode_data episode_map[(season, episode_num)] = episode_data
if filter_set:
_log("DEBUG", f"Made {api_calls_made} Sonarr history API calls for filtered episodes (instead of all episodes)")
return episode_map return episode_map
except Exception as e: except Exception as e: