This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
XML Processing Cache for NFOGuard
|
||||
Optimizes NFO file parsing and manipulation
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from core.logging import _log
|
||||
|
||||
|
||||
class XMLProcessingCache:
|
||||
"""Caches parsed XML trees and NFO data with mtime invalidation"""
|
||||
|
||||
def __init__(self, max_cache_size: int = 500):
|
||||
self.max_cache_size = max_cache_size
|
||||
self._xml_tree_cache: Dict[str, tuple] = {} # (mtime, parsed_tree)
|
||||
self._nfo_data_cache: Dict[str, tuple] = {} # (mtime, extracted_data)
|
||||
self._cache_hits = 0
|
||||
self._cache_misses = 0
|
||||
|
||||
def get_parsed_nfo(self, nfo_path: Path) -> Optional[ET.Element]:
|
||||
"""Get parsed NFO file with caching"""
|
||||
if not nfo_path.exists():
|
||||
return None
|
||||
|
||||
cache_key = str(nfo_path)
|
||||
current_mtime = nfo_path.stat().st_mtime
|
||||
|
||||
# Check cache
|
||||
if cache_key in self._xml_tree_cache:
|
||||
cached_mtime, cached_tree = self._xml_tree_cache[cache_key]
|
||||
if cached_mtime == current_mtime:
|
||||
self._cache_hits += 1
|
||||
return cached_tree
|
||||
|
||||
# Parse XML
|
||||
self._cache_misses += 1
|
||||
try:
|
||||
tree = ET.parse(nfo_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Cache the parsed tree
|
||||
self._xml_tree_cache[cache_key] = (current_mtime, root)
|
||||
self._cleanup_cache()
|
||||
|
||||
return root
|
||||
except (ET.ParseError, OSError) as e:
|
||||
_log("DEBUG", f"Error parsing NFO {nfo_path}: {e}")
|
||||
return None
|
||||
|
||||
def extract_nfo_dates_cached(self, nfo_path: Path) -> Optional[Dict[str, Any]]:
|
||||
"""Extract NFOGuard date fields from NFO with caching"""
|
||||
if not nfo_path.exists():
|
||||
return None
|
||||
|
||||
cache_key = f"dates:{nfo_path}"
|
||||
current_mtime = nfo_path.stat().st_mtime
|
||||
|
||||
# Check cache
|
||||
if cache_key in self._nfo_data_cache:
|
||||
cached_mtime, cached_data = self._nfo_data_cache[cache_key]
|
||||
if cached_mtime == current_mtime:
|
||||
self._cache_hits += 1
|
||||
return cached_data
|
||||
|
||||
# Extract data
|
||||
self._cache_misses += 1
|
||||
root = self.get_parsed_nfo(nfo_path)
|
||||
if not root:
|
||||
return None
|
||||
|
||||
dates_data = self._extract_nfoguard_dates(root)
|
||||
|
||||
# Cache the extracted data
|
||||
self._nfo_data_cache[cache_key] = (current_mtime, dates_data)
|
||||
self._cleanup_cache()
|
||||
|
||||
return dates_data
|
||||
|
||||
def _extract_nfoguard_dates(self, root: ET.Element) -> Optional[Dict[str, Any]]:
|
||||
"""Extract NFOGuard date fields from parsed XML"""
|
||||
try:
|
||||
# Look for NFOGuard date fields
|
||||
dateadded_elem = root.find(".//dateadded")
|
||||
source_elem = root.find(".//nfoguard_source")
|
||||
aired_elem = root.find(".//aired") or root.find(".//premiered")
|
||||
|
||||
if dateadded_elem is not None and dateadded_elem.text:
|
||||
result = {
|
||||
"dateadded": dateadded_elem.text.strip(),
|
||||
"source": source_elem.text.strip() if source_elem is not None and source_elem.text else "unknown",
|
||||
"aired": aired_elem.text.strip() if aired_elem is not None and aired_elem.text else None
|
||||
}
|
||||
|
||||
# Check if title exists for episodes
|
||||
title_elem = root.find(".//title")
|
||||
if title_elem is not None and title_elem.text:
|
||||
result["has_title"] = True
|
||||
result["title"] = title_elem.text.strip()
|
||||
else:
|
||||
result["has_title"] = False
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
_log("DEBUG", f"Error extracting NFOGuard dates from XML: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def check_nfo_has_nfoguard_data(self, nfo_path: Path) -> bool:
|
||||
"""Quickly check if NFO has NFOGuard data"""
|
||||
cache_key = f"has_nfoguard:{nfo_path}"
|
||||
|
||||
if not nfo_path.exists():
|
||||
return False
|
||||
|
||||
current_mtime = nfo_path.stat().st_mtime
|
||||
|
||||
# Check cache
|
||||
if cache_key in self._nfo_data_cache:
|
||||
cached_mtime, has_data = self._nfo_data_cache[cache_key]
|
||||
if cached_mtime == current_mtime:
|
||||
self._cache_hits += 1
|
||||
return has_data
|
||||
|
||||
# Check for NFOGuard markers
|
||||
self._cache_misses += 1
|
||||
try:
|
||||
# Quick text search first (faster than XML parsing)
|
||||
with open(nfo_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
has_data = '<nfoguard_source>' in content or '<!-- NFOGuard -->' in content
|
||||
|
||||
# Cache result
|
||||
self._nfo_data_cache[cache_key] = (current_mtime, has_data)
|
||||
return has_data
|
||||
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
_log("DEBUG", f"Error checking NFOGuard markers in {nfo_path}: {e}")
|
||||
return False
|
||||
|
||||
def _cleanup_cache(self):
|
||||
"""Clean up caches when they get too large"""
|
||||
if len(self._xml_tree_cache) > self.max_cache_size:
|
||||
# Remove oldest 25% of entries
|
||||
remove_count = self.max_cache_size // 4
|
||||
old_keys = list(self._xml_tree_cache.keys())[:remove_count]
|
||||
for key in old_keys:
|
||||
del self._xml_tree_cache[key]
|
||||
|
||||
if len(self._nfo_data_cache) > self.max_cache_size:
|
||||
remove_count = self.max_cache_size // 4
|
||||
old_keys = list(self._nfo_data_cache.keys())[:remove_count]
|
||||
for key in old_keys:
|
||||
del self._nfo_data_cache[key]
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear all XML caches"""
|
||||
self._xml_tree_cache.clear()
|
||||
self._nfo_data_cache.clear()
|
||||
_log("INFO", "XML processing cache cleared")
|
||||
|
||||
def get_cache_stats(self) -> Dict[str, Any]:
|
||||
"""Get XML cache performance statistics"""
|
||||
total_requests = self._cache_hits + self._cache_misses
|
||||
hit_rate = (self._cache_hits / total_requests * 100) if total_requests > 0 else 0
|
||||
|
||||
return {
|
||||
"xml_cache_hits": self._cache_hits,
|
||||
"xml_cache_misses": self._cache_misses,
|
||||
"xml_hit_rate_percent": round(hit_rate, 2),
|
||||
"xml_tree_cache_size": len(self._xml_tree_cache),
|
||||
"nfo_data_cache_size": len(self._nfo_data_cache)
|
||||
}
|
||||
|
||||
|
||||
# Global XML cache instance
|
||||
xml_cache = XMLProcessingCache()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def parse_imdb_from_filename(filename: str) -> Optional[str]:
|
||||
"""Parse IMDb ID from filename with LRU caching"""
|
||||
import re
|
||||
match = re.search(r'\[imdb-([^]]+)\]', filename, re.IGNORECASE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
@lru_cache(maxsize=200)
|
||||
def clean_title_for_search(title: str) -> str:
|
||||
"""Clean title for searching with LRU caching"""
|
||||
return title.lower().replace(" ", "").replace("-", "").replace("_", "")
|
||||
|
||||
|
||||
def clear_xml_caches():
|
||||
"""Clear all XML-related caches"""
|
||||
xml_cache.clear_cache()
|
||||
parse_imdb_from_filename.cache_clear()
|
||||
clean_title_for_search.cache_clear()
|
||||
_log("INFO", "All XML caches cleared")
|
||||
Reference in New Issue
Block a user