Files
2026-01-16 12:24:41 +01:00

250 lines
7.5 KiB
Python

#!/usr/bin/env python3
# streaming_providers/base/epg/epg_cache.py
"""
EPG Cache Manager using VFS
Handles downloading, caching, and TTL management for EPG XML files
"""
import time
from typing import Dict, Optional
from ..utils.logger import logger
from ..utils.vfs import VFS
class EPGCache:
"""
Manages EPG XML file caching with VFS backend.
Handles both plain and gzipped XML files.
"""
# Cache TTL: 24 hours
CACHE_TTL_SECONDS = 24 * 60 * 60
# File names
EPG_FILE = "epg.xml"
EPG_GZ_FILE = "epg.xml.gz"
METADATA_FILE = "epg_metadata.json"
def __init__(self, vfs_subdir: str = "epg_cache"):
"""
Initialize EPG cache manager.
Args:
vfs_subdir: Subdirectory under addon data for EPG cache
"""
self.vfs = VFS(addon_subdir=vfs_subdir)
logger.info(f"EPGCache: Initialized with VFS path: {self.vfs.base_path}")
def _get_metadata(self) -> Optional[Dict]:
"""
Load cache metadata from VFS.
Returns:
Metadata dictionary or None if not found/invalid
"""
metadata = self.vfs.read_json(self.METADATA_FILE)
if metadata:
logger.debug(f"EPGCache: Loaded metadata: {metadata}")
return metadata
def _save_metadata(self, url: str, file_size: int, is_gzipped: bool) -> bool:
"""
Save cache metadata to VFS.
Args:
url: EPG source URL
file_size: Size of cached file in bytes
is_gzipped: Whether file is gzipped
Returns:
True if saved successfully
"""
metadata = {
"downloaded_at": int(time.time()),
"url": url,
"file_size": file_size,
"is_gzipped": is_gzipped,
}
success = self.vfs.write_json(self.METADATA_FILE, metadata)
if success:
logger.info(f"EPGCache: Saved metadata for {url}")
else:
logger.error(f"EPGCache: Failed to save metadata")
return success
def is_cache_valid(self) -> bool:
"""
Check if cached EPG is still valid (within TTL).
Returns:
True if cache exists and is valid
"""
metadata = self._get_metadata()
if not metadata:
logger.debug("EPGCache: No metadata found, cache invalid")
return False
# Check if EPG file exists
filename = self.EPG_GZ_FILE if metadata.get("is_gzipped") else self.EPG_FILE
if not self.vfs.exists(filename):
logger.debug(f"EPGCache: EPG file '{filename}' not found, cache invalid")
return False
# Check TTL
downloaded_at = metadata.get("downloaded_at", 0)
age = int(time.time()) - downloaded_at
if age > self.CACHE_TTL_SECONDS:
logger.info(f"EPGCache: Cache expired (age: {age}s, TTL: {self.CACHE_TTL_SECONDS}s)")
return False
logger.debug(f"EPGCache: Cache valid (age: {age}s)")
return True
def get_cached_file_path(self) -> Optional[str]:
"""
Get path to cached EPG file if valid.
Returns:
Full path to EPG file, or None if cache invalid
"""
if not self.is_cache_valid():
return None
metadata = self._get_metadata()
if not metadata:
return None
filename = self.EPG_GZ_FILE if metadata.get("is_gzipped") else self.EPG_FILE
file_path = self.vfs.join_path(filename)
logger.debug(f"EPGCache: Returning cached file path: {file_path}")
return file_path
def download_and_cache(self, url: str) -> Optional[str]:
"""
Download EPG file from URL and cache it.
Handles both plain and gzipped files automatically.
Args:
url: URL to download EPG from
Returns:
Path to cached file, or None on failure
"""
logger.info(f"EPGCache: Downloading EPG from {url}")
try:
import requests
# Download with streaming to handle large files
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
# Determine if content is gzipped
content_type = response.headers.get("Content-Type", "").lower()
content_encoding = response.headers.get("Content-Encoding", "").lower()
is_gzipped = "gzip" in content_encoding or url.endswith(".gz") or "gzip" in content_type
filename = self.EPG_GZ_FILE if is_gzipped else self.EPG_FILE
# Get full file path
file_path = self.vfs.join_path(filename)
# Ensure directory exists
self.vfs.ensure_directory(file_path)
# Download in chunks
chunk_size = 8192
total_size = 0
with open(file_path, "wb") as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
total_size += len(chunk)
logger.info(f"EPGCache: Downloaded {total_size} bytes to {filename}")
# Save metadata
self._save_metadata(url, total_size, is_gzipped)
return file_path
except Exception as e:
logger.error(f"EPGCache: Download failed: {e}", exc_info=True)
return None
def get_or_download(self, url: str) -> Optional[str]:
"""
Get cached EPG file path, or download if cache is invalid.
Args:
url: URL to download from if cache invalid
Returns:
Path to EPG file (cached or freshly downloaded), or None on failure
"""
# Try cache first
cached_path = self.get_cached_file_path()
if cached_path:
logger.info("EPGCache: Using cached EPG file")
return cached_path
# Cache miss or expired - download new
logger.info("EPGCache: Cache miss or expired, downloading")
return self.download_and_cache(url)
def clear_cache(self) -> bool:
"""
Clear all cached EPG files and metadata.
Returns:
True if cleared successfully
"""
logger.info("EPGCache: Clearing cache")
success = True
# Delete EPG files
for filename in [self.EPG_FILE, self.EPG_GZ_FILE, self.METADATA_FILE]:
if self.vfs.exists(filename):
if not self.vfs.delete(filename):
logger.warning(f"EPGCache: Failed to delete {filename}")
success = False
if success:
logger.info("EPGCache: Cache cleared successfully")
return success
def get_cache_info(self) -> Optional[Dict]:
"""
Get information about cached EPG.
Returns:
Dictionary with cache info, or None if no cache
"""
metadata = self._get_metadata()
if not metadata:
return None
filename = self.EPG_GZ_FILE if metadata.get("is_gzipped") else self.EPG_FILE
file_exists = self.vfs.exists(filename)
age = int(time.time()) - metadata.get("downloaded_at", 0)
is_valid = self.is_cache_valid()
return {
"url": metadata.get("url"),
"downloaded_at": metadata.get("downloaded_at"),
"age_seconds": age,
"file_size": metadata.get("file_size"),
"is_gzipped": metadata.get("is_gzipped"),
"file_exists": file_exists,
"is_valid": is_valid,
"ttl_seconds": self.CACHE_TTL_SECONDS,
}