mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-16 14:12:20 +02:00
250 lines
7.5 KiB
Python
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,
|
|
}
|