#!/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