Files
moma/renamer/cache.py
T

187 lines
6.2 KiB
Python

import json
import os
import time
import hashlib
import pickle
from pathlib import Path
from typing import Any, Optional
class Cache:
"""File-based cache with TTL support."""
def __init__(self, cache_dir: Optional[Path] = None):
if cache_dir is None:
cache_dir = Path.home() / ".cache" / "renamer"
self.cache_dir = cache_dir
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _get_cache_file(self, key: str) -> Path:
"""Get cache file path with hashed filename and subdirs."""
# Parse key format: ClassName.method_name.param_hash
if '.' in key:
parts = key.split('.')
if len(parts) >= 3:
class_name = parts[0]
method_name = parts[1]
param_hash = parts[2]
# Use class name as subdir
cache_subdir = self.cache_dir / class_name
cache_subdir.mkdir(parents=True, exist_ok=True)
# Use method_name.param_hash as filename
return cache_subdir / f"{method_name}.{param_hash}.pkl"
# Fallback for old keys (tmdb_, poster_, etc.)
if key.startswith("tmdb_"):
subdir = "tmdb"
subkey = key[5:] # Remove "tmdb_" prefix
elif key.startswith("poster_"):
subdir = "posters"
subkey = key[7:] # Remove "poster_" prefix
else:
subdir = "general"
subkey = key
# Create subdir
cache_subdir = self.cache_dir / subdir
cache_subdir.mkdir(parents=True, exist_ok=True)
# Hash the subkey for filename
key_hash = hashlib.md5(subkey.encode('utf-8')).hexdigest()
return cache_subdir / f"{key_hash}.json"
def get(self, key: str) -> Optional[Any]:
"""Get cached value if not expired."""
cache_file = self._get_cache_file(key)
if not cache_file.exists():
return None
try:
with open(cache_file, 'r') as f:
data = json.load(f)
if time.time() > data.get('expires', 0):
# Expired, remove file
cache_file.unlink(missing_ok=True)
return None
return data.get('value')
except (json.JSONDecodeError, IOError):
# Corrupted, remove
cache_file.unlink(missing_ok=True)
return None
def set(self, key: str, value: Any, ttl_seconds: int) -> None:
"""Set cached value with TTL."""
cache_file = self._get_cache_file(key)
data = {
'value': value,
'expires': time.time() + ttl_seconds
}
try:
with open(cache_file, 'w') as f:
json.dump(data, f)
except IOError:
pass # Silently fail
def invalidate(self, key: str) -> None:
"""Remove cache entry."""
cache_file = self._get_cache_file(key)
cache_file.unlink(missing_ok=True)
def get_image(self, key: str) -> Optional[Path]:
"""Get cached image path if not expired."""
cache_file = self._get_cache_file(key)
if not cache_file.exists():
return None
try:
with open(cache_file, 'r') as f:
data = json.load(f)
if time.time() > data.get('expires', 0):
# Expired, remove file and image
image_path = data.get('image_path')
if image_path and Path(image_path).exists():
Path(image_path).unlink(missing_ok=True)
cache_file.unlink(missing_ok=True)
return None
image_path = data.get('image_path')
if image_path and Path(image_path).exists():
return Path(image_path)
return None
except (json.JSONDecodeError, IOError):
cache_file.unlink(missing_ok=True)
return None
def set_image(self, key: str, image_data: bytes, ttl_seconds: int) -> Optional[Path]:
"""Set cached image and return path."""
# Determine subdir and subkey
if key.startswith("poster_"):
subdir = "posters"
subkey = key[7:]
else:
subdir = "images"
subkey = key
# Create subdir
image_dir = self.cache_dir / subdir
image_dir.mkdir(parents=True, exist_ok=True)
# Hash for filename
key_hash = hashlib.md5(subkey.encode('utf-8')).hexdigest()
image_path = image_dir / f"{key_hash}.jpg"
try:
with open(image_path, 'wb') as f:
f.write(image_data)
# Cache metadata
data = {
'image_path': str(image_path),
'expires': time.time() + ttl_seconds
}
cache_file = self._get_cache_file(key)
with open(cache_file, 'w') as f:
json.dump(data, f)
return image_path
except IOError:
return None
def get_object(self, key: str) -> Optional[Any]:
"""Get pickled object from cache if not expired."""
cache_file = self._get_cache_file(key)
if not cache_file.exists():
return None
try:
with open(cache_file, 'rb') as f:
data = pickle.load(f)
if time.time() > data.get('expires', 0):
# Expired, remove file
cache_file.unlink(missing_ok=True)
return None
return data.get('value')
except (pickle.PickleError, IOError):
# Corrupted, remove
cache_file.unlink(missing_ok=True)
return None
def set_object(self, key: str, obj: Any, ttl_seconds: int) -> None:
"""Pickle and cache object with TTL."""
cache_file = self._get_cache_file(key)
data = {
'value': obj,
'expires': time.time() + ttl_seconds
}
try:
with open(cache_file, 'wb') as f:
pickle.dump(data, f)
except IOError:
pass # Silently fail