Files
nfoguard/core/batch_operations.py
T
sbcrumb 94590ac070
Local Docker Build (Dev) / build-dev (push) Successful in 15s
feat:add file cache system
2025-09-25 18:58:31 -04:00

251 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""
Batch Operations for NFOGuard
Optimizes bulk file processing and NFO operations
"""
import os
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from core.logging import _log
from core.fs_cache import fs_cache
from core.xml_cache import xml_cache
class BatchNFOProcessor:
"""Handles batch NFO operations for improved performance"""
def __init__(self, max_workers: int = 4):
self.max_workers = max_workers
def batch_find_video_files(self, directories: List[Path]) -> Dict[Path, List[Path]]:
"""Find video files in multiple directories concurrently"""
results = {}
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# Submit all directory scans
future_to_dir = {
executor.submit(fs_cache.find_video_files, directory): directory
for directory in directories if directory.exists()
}
# Collect results
for future in as_completed(future_to_dir):
directory = future_to_dir[future]
try:
video_files = future.result()
results[directory] = video_files
except Exception as e:
_log("ERROR", f"Error scanning directory {directory}: {e}")
results[directory] = []
return results
def batch_check_nfo_files(self, nfo_paths: List[Path]) -> Dict[Path, bool]:
"""Check multiple NFO files for NFOGuard data concurrently"""
results = {}
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# Submit all NFO checks
future_to_path = {
executor.submit(xml_cache.check_nfo_has_nfoguard_data, nfo_path): nfo_path
for nfo_path in nfo_paths
}
# Collect results
for future in as_completed(future_to_path):
nfo_path = future_to_path[future]
try:
has_data = future.result()
results[nfo_path] = has_data
except Exception as e:
_log("ERROR", f"Error checking NFO {nfo_path}: {e}")
results[nfo_path] = False
return results
def batch_extract_nfo_dates(self, nfo_paths: List[Path]) -> Dict[Path, Optional[Dict[str, Any]]]:
"""Extract date information from multiple NFO files concurrently"""
results = {}
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# Submit all NFO extractions
future_to_path = {
executor.submit(xml_cache.extract_nfo_dates_cached, nfo_path): nfo_path
for nfo_path in nfo_paths if nfo_path.exists()
}
# Collect results
for future in as_completed(future_to_path):
nfo_path = future_to_path[future]
try:
dates_data = future.result()
results[nfo_path] = dates_data
except Exception as e:
_log("ERROR", f"Error extracting dates from NFO {nfo_path}: {e}")
results[nfo_path] = None
return results
def batch_update_file_mtimes(self, file_mtime_pairs: List[Tuple[Path, datetime]]):
"""Update file modification times in batch"""
updated_count = 0
error_count = 0
for file_path, target_datetime in file_mtime_pairs:
try:
if file_path.exists():
# Convert datetime to timestamp
timestamp = target_datetime.timestamp()
os.utime(file_path, (timestamp, timestamp))
updated_count += 1
else:
error_count += 1
_log("WARNING", f"File not found for mtime update: {file_path}")
except Exception as e:
error_count += 1
_log("ERROR", f"Error updating mtime for {file_path}: {e}")
_log("INFO", f"Batch mtime update complete: {updated_count} updated, {error_count} errors")
return {"updated": updated_count, "errors": error_count}
def scan_series_episodes_optimized(self, series_path: Path) -> Dict[Tuple[int, int], List[Path]]:
"""Optimized episode scanning using batch operations"""
disk_episodes = {}
# Get season directories
season_dirs = fs_cache.get_directory_contents(series_path)
valid_season_dirs = []
for season_dir in season_dirs:
if (season_dir.is_dir() and
season_dir.name.lower().startswith("season")):
valid_season_dirs.append(season_dir)
if not valid_season_dirs:
return disk_episodes
# Batch scan all season directories
season_video_files = self.batch_find_video_files(valid_season_dirs)
# Process results
for season_dir, video_files in season_video_files.items():
# Extract season number
try:
season_name = season_dir.name.lower()
if "season" in season_name:
season_part = season_name.replace("season", "").strip()
season_num = int(season_part)
else:
continue
except (ValueError, IndexError):
continue
# Process video files
for video_file in video_files:
from core.fs_cache import parse_episode_from_filename
episode_info = parse_episode_from_filename(video_file.name)
if episode_info:
file_season, file_episode = episode_info
key = (season_num, file_episode)
if key not in disk_episodes:
disk_episodes[key] = []
disk_episodes[key].append(video_file)
return disk_episodes
def batch_series_nfo_check(self, series_paths: List[Path]) -> Dict[Path, Dict[str, Any]]:
"""Check multiple series for NFO data in batch"""
results = {}
# Collect all NFO paths to check
nfo_paths_to_series = {}
for series_path in series_paths:
if not series_path.exists():
continue
# Check main series NFO
tvshow_nfo = series_path / "tvshow.nfo"
if tvshow_nfo.exists():
nfo_paths_to_series[tvshow_nfo] = (series_path, "tvshow")
# Check season NFOs
season_dirs = fs_cache.get_directory_contents(series_path)
for season_dir in season_dirs:
if season_dir.is_dir() and season_dir.name.lower().startswith("season"):
season_nfo = season_dir / "season.nfo"
if season_nfo.exists():
nfo_paths_to_series[season_nfo] = (series_path, f"season_{season_dir.name}")
if not nfo_paths_to_series:
return results
# Batch check all NFOs
nfo_results = self.batch_check_nfo_files(list(nfo_paths_to_series.keys()))
# Organize results by series
for nfo_path, has_nfoguard_data in nfo_results.items():
if nfo_path in nfo_paths_to_series:
series_path, nfo_type = nfo_paths_to_series[nfo_path]
if series_path not in results:
results[series_path] = {}
results[series_path][nfo_type] = has_nfoguard_data
return results
# Global batch processor instance
batch_processor = BatchNFOProcessor()
def optimize_library_scan(library_paths: List[Path]) -> Dict[str, Any]:
"""Perform optimized library scan using batch operations"""
start_time = time.time()
stats = {
"total_paths": len(library_paths),
"processed": 0,
"errors": 0,
"series_found": 0,
"movies_found": 0,
"processing_time": 0
}
series_paths = []
movie_paths = []
# Categorize paths
for lib_path in library_paths:
if not lib_path.exists():
stats["errors"] += 1
continue
# Use cached directory scanning
items = fs_cache.get_directory_contents(lib_path)
for item in items:
if item.is_dir() and "[imdb-" in item.name.lower():
# Determine if it's a series or movie based on structure
season_dirs = [d for d in fs_cache.get_directory_contents(item)
if d.is_dir() and d.name.lower().startswith("season")]
if season_dirs:
series_paths.append(item)
stats["series_found"] += 1
else:
movie_paths.append(item)
stats["movies_found"] += 1
# Batch process series
if series_paths:
_log("INFO", f"Batch processing {len(series_paths)} TV series")
series_results = batch_processor.batch_series_nfo_check(series_paths)
stats["processed"] += len(series_results)
# Movies can be processed similarly
stats["processing_time"] = round(time.time() - start_time, 2)
_log("INFO", f"Optimized library scan complete: {stats}")
return stats