Files
nfoguard/core/database_populator.py
T
sbcrumb 97a9f3431a
Local Docker Build (Dev) / build-dev (push) Successful in 35s
refactor: update and remove anything to do with NFO files
moving to an all DB approach since radarr overwrites .nfo files
2025-11-02 13:43:28 -05:00

268 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Database Populator for NFOGuard
Bulk populates the NFOGuard database from Radarr/Sonarr
Phase 4: Replace NFO-based initial population with direct DB/API queries
"""
import time
from typing import Dict, List, Optional, Tuple
from datetime import datetime
from core.database import NFOGuardDatabase
from clients.radarr_client import RadarrClient
from clients.sonarr_client import SonarrClient
from utils.logging import _log
class DatabasePopulator:
"""Populates NFOGuard database from Radarr/Sonarr sources"""
def __init__(self, db: NFOGuardDatabase, radarr_client: RadarrClient, sonarr_client: SonarrClient):
self.db = db
self.radarr = radarr_client
self.sonarr = sonarr_client
def populate_movies(self) -> Dict[str, any]:
"""
Populate movies from Radarr database/API
Returns:
Dictionary with statistics: {
'total': int,
'added': int,
'updated': int,
'skipped': int,
'errors': int,
'duration': float
}
"""
_log("INFO", "Starting movie population from Radarr")
start_time = time.time()
stats = {
'total': 0,
'added': 0,
'updated': 0,
'skipped': 0,
'errors': 0,
'duration': 0.0
}
try:
# Get all movies from Radarr
movies = self.radarr.get_all_movies()
if not movies:
_log("WARNING", "No movies found in Radarr")
return stats
stats['total'] = len(movies)
_log("INFO", f"Found {stats['total']} movies in Radarr")
# Process each movie
for movie in movies:
try:
imdb_id = movie.get('imdbId')
if not imdb_id:
_log("DEBUG", f"Skipping movie without IMDb ID: {movie.get('title')}")
stats['skipped'] += 1
continue
# Check if movie already exists in database
existing = self.db.get_movie_dates(imdb_id)
if existing and existing.get('dateadded'):
_log("DEBUG", f"Movie {imdb_id} already in database, skipping")
stats['skipped'] += 1
continue
# Get movie path
path = movie.get('path', '')
# Get release date
released = None
if movie.get('digitalRelease'):
released = movie.get('digitalRelease')
source_type = 'radarr:digital'
elif movie.get('physicalRelease'):
released = movie.get('physicalRelease')
source_type = 'radarr:physical'
elif movie.get('inCinemas'):
released = movie.get('inCinemas')
source_type = 'radarr:theatrical'
else:
source_type = 'radarr:unknown'
# Get import date from Radarr history
import_date = self.radarr.get_movie_import_date(imdb_id)
if import_date:
dateadded = import_date
source = 'radarr:import_history'
elif released:
# Use release date as fallback
dateadded = released
source = f'{source_type}_fallback'
else:
_log("DEBUG", f"No date available for movie {imdb_id}, skipping")
stats['skipped'] += 1
continue
# Insert into database
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, has_video_file=True)
stats['added'] += 1
_log("DEBUG", f"Added movie {imdb_id}: {movie.get('title')} (source: {source})")
except Exception as e:
_log("ERROR", f"Error processing movie {movie.get('title', 'unknown')}: {e}")
stats['errors'] += 1
continue
except Exception as e:
_log("ERROR", f"Error during movie population: {e}")
stats['errors'] += 1
stats['duration'] = time.time() - start_time
_log("INFO", f"Movie population complete: {stats['added']} added, {stats['skipped']} skipped, {stats['errors']} errors in {stats['duration']:.2f}s")
return stats
def populate_tv_episodes(self) -> Dict[str, any]:
"""
Populate TV episodes from Sonarr API
Returns:
Dictionary with statistics: {
'total_series': int,
'total_episodes': int,
'added': int,
'updated': int,
'skipped': int,
'errors': int,
'duration': float
}
"""
_log("INFO", "Starting TV episode population from Sonarr")
start_time = time.time()
stats = {
'total_series': 0,
'total_episodes': 0,
'added': 0,
'updated': 0,
'skipped': 0,
'errors': 0,
'duration': 0.0
}
try:
# Get all series from Sonarr
all_series = self.sonarr.get_all_series()
if not all_series:
_log("WARNING", "No series found in Sonarr")
return stats
stats['total_series'] = len(all_series)
_log("INFO", f"Found {stats['total_series']} series in Sonarr")
# Process each series
for series in all_series:
try:
imdb_id = series.get('imdbId')
if not imdb_id:
_log("DEBUG", f"Skipping series without IMDb ID: {series.get('title')}")
continue
series_id = series.get('id')
series_path = series.get('path', '')
series_title = series.get('title', 'Unknown')
# Update series record
self.db.upsert_series(imdb_id, series_path)
# Get all episodes for this series
episodes = self.sonarr.episodes_for_series(series_id)
if not episodes:
continue
_log("DEBUG", f"Processing {len(episodes)} episodes for {series_title}")
# Process each episode
for episode in episodes:
try:
season_num = episode.get('seasonNumber', 0)
episode_num = episode.get('episodeNumber', 0)
if season_num < 0 or episode_num <= 0:
continue
stats['total_episodes'] += 1
# Check if episode already exists
existing = self.db.get_episode_date(imdb_id, season_num, episode_num)
if existing and existing.get('dateadded'):
stats['skipped'] += 1
continue
# Get air date
aired = episode.get('airDate')
# Get import date from history
dateadded = None
episode_id = episode.get('id')
if episode_id and episode.get('hasFile'):
import_date = self.sonarr.get_episode_import_history(episode_id)
if import_date:
dateadded = import_date
source = 'sonarr:import_history'
# Fallback to air date if no import date
if not dateadded and aired:
dateadded = aired
source = 'sonarr:aired_fallback'
elif not dateadded:
# No date available
stats['skipped'] += 1
continue
# Insert into database
has_file = episode.get('hasFile', False)
self.db.upsert_episode_date(imdb_id, season_num, episode_num, aired, dateadded, source, has_file)
stats['added'] += 1
except Exception as e:
_log("ERROR", f"Error processing episode S{season_num:02d}E{episode_num:02d} of {series_title}: {e}")
stats['errors'] += 1
continue
except Exception as e:
_log("ERROR", f"Error processing series {series.get('title', 'unknown')}: {e}")
stats['errors'] += 1
continue
except Exception as e:
_log("ERROR", f"Error during TV episode population: {e}")
stats['errors'] += 1
stats['duration'] = time.time() - start_time
_log("INFO", f"TV episode population complete: {stats['added']} added, {stats['skipped']} skipped, {stats['errors']} errors in {stats['duration']:.2f}s")
return stats
def populate_all(self) -> Dict[str, any]:
"""
Populate both movies and TV episodes
Returns:
Combined statistics dictionary
"""
_log("INFO", "Starting full database population")
start_time = time.time()
movie_stats = self.populate_movies()
tv_stats = self.populate_tv_episodes()
combined_stats = {
'movies': movie_stats,
'tv': tv_stats,
'total_duration': time.time() - start_time
}
_log("INFO", f"Full database population complete in {combined_stats['total_duration']:.2f}s")
return combined_stats