251 lines
9.5 KiB
Python
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 |