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
+27 -42
View File
@@ -152,54 +152,39 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks, de
if recent_renames:
# Take the most recent rename event
latest_rename = recent_renames[0]
print(f"DEBUG: Latest rename event keys: {list(latest_rename.keys())}")
print(f"DEBUG: Latest rename event full structure: {latest_rename}")
print(f"DEBUG: Processing latest rename event")
# Try different possible episode data locations
episode_info = latest_rename.get("episode", {})
episode_file_info = latest_rename.get("episodeFile", {})
data_info = latest_rename.get("data", {})
# Extract episodeId directly from the rename event
episode_id = latest_rename.get("episodeId")
print(f"DEBUG: Found episodeId {episode_id} in rename event")
print(f"DEBUG: Episode info: {episode_info}")
print(f"DEBUG: EpisodeFile info: {episode_file_info}")
print(f"DEBUG: Data info: {data_info}")
if episode_id:
# Fetch episode details using the episodeId
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
season_num = None
episode_num = None
episode_id = None
episode_title = None
if episode_response.status_code == 200:
episode_detail = episode_response.json()
season_num = episode_detail.get("seasonNumber")
episode_num = episode_detail.get("episodeNumber")
episode_title = episode_detail.get("title")
# Check episode object first
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")
print(f"DEBUG: Episode details - Season: {season_num}, Episode: {episode_num}, Title: {episode_title}")
# Check episodeFile object
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 = [{
"seasonNumber": season_num,
"episodeNumber": episode_num,
"id": episode_id,
"title": episode_title
}]
print(f"INFO: Found recently renamed episode from history: S{season_num:02d}E{episode_num:02d}")
if season_num is not None and episode_num is not None:
episodes_data = [{
"seasonNumber": season_num,
"episodeNumber": episode_num,
"id": episode_id,
"title": episode_title
}]
print(f"INFO: Successfully identified renamed episode: S{season_num:02d}E{episode_num:02d} - {episode_title}")
else:
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: Could not extract episode data from rename event")
print(f"DEBUG: No episodeId found in rename event")
else:
print(f"DEBUG: No recent rename events found in last hour")
else:
+66 -30
View File
@@ -102,47 +102,70 @@ 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]]:
"""Gather episode air dates and date added information"""
"""Gather episode air dates and date added information with database-first optimization"""
episode_dates = {}
episodes_needing_lookup = []
# Get data from Sonarr first if available
sonarr_episodes = self._get_sonarr_episodes(imdb_id)
# OPTIMIZATION: Check database first for existing dates
_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:
aired = None
dateadded = None
source = "unknown"
# Try database first - this is much faster than API calls
db_result = self.db.get_episode_date(imdb_id, season, episode)
# Try Sonarr first
if (season, episode) in sonarr_episodes:
sonarr_data = sonarr_episodes[(season, episode)]
aired = sonarr_data.get('airDate')
dateadded = sonarr_data.get('dateAdded')
if dateadded:
# Use more specific source to indicate this came from Sonarr import history
source = "sonarr:history.import"
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))
# Fallback to external sources if needed
if not aired:
external_aired = self.external_clients.get_episode_air_date(imdb_id, season, episode)
if external_aired:
aired = external_aired
if not dateadded:
source = "external"
_log("INFO", f"Database cache hits: {db_cache_hits}/{len(disk_episodes)} episodes. Need API lookup: {len(episodes_needing_lookup)}")
# ONLY use air date as fallback for dateadded if we have no Sonarr import date at all
# and if we couldn't get any import information from Sonarr history
if not dateadded and aired and source != "sonarr:history.import":
dateadded = aired
source = f"{source}_fallback" if source != "unknown" else "aired_fallback"
# 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)
episode_dates[(season, episode)] = (aired, dateadded, source)
# Process episodes that needed lookup
for (season, episode) in episodes_needing_lookup:
aired = None
dateadded = None
source = "unknown"
# Try Sonarr first
if (season, episode) in sonarr_episodes:
sonarr_data = sonarr_episodes[(season, episode)]
aired = sonarr_data.get('airDate')
dateadded = sonarr_data.get('dateAdded')
if dateadded:
source = "sonarr:history.import"
# Fallback to external sources if needed
if not aired:
external_aired = self.external_clients.get_episode_air_date(imdb_id, season, episode)
if external_aired:
aired = external_aired
if not dateadded:
source = "external"
# Use air date as fallback for dateadded if no import date found
if not dateadded and aired and source != "sonarr:history.import":
dateadded = aired
source = f"{source}_fallback" if source != "unknown" else "aired_fallback"
episode_dates[(season, episode)] = (aired, dateadded, source)
return episode_dates
def _get_sonarr_episodes(self, imdb_id: str) -> Dict[Tuple[int, int], Dict[str, Any]]:
"""Get episode information from Sonarr including import history"""
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 - optimized to only fetch needed episodes"""
try:
series_data = self.sonarr.series_by_imdb(imdb_id)
if not series_data:
@@ -154,11 +177,20 @@ class TVProcessor:
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 = {}
api_calls_made = 0
for episode in episodes:
season = episode.get('seasonNumber', 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:
# Get basic episode info
episode_data = {
@@ -170,6 +202,7 @@ class TVProcessor:
episode_id = episode.get('id')
if episode_id and episode.get('hasFile'):
import_date = self.sonarr.get_episode_import_history(episode_id)
api_calls_made += 1
if 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}")
@@ -183,6 +216,9 @@ class TVProcessor:
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
except Exception as e: