mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-16 14:12:20 +02:00
276 lines
9.6 KiB
Python
276 lines
9.6 KiB
Python
# streaming_providers/base/utils/mpd_cache.py
|
|
import time
|
|
from typing import Optional
|
|
|
|
from .logger import logger
|
|
from .vfs import VFS
|
|
|
|
|
|
class MPDCacheManager:
|
|
"""
|
|
Manages caching of rewritten MPD manifests with TTL support
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize MPD cache manager with VFS"""
|
|
self.vfs = VFS(addon_subdir="mpd_cache")
|
|
logger.debug(f"MPD cache initialized at: {self.vfs.base_path}")
|
|
|
|
@staticmethod
|
|
def _get_cache_key(provider: str, channel_id: str) -> str:
|
|
"""Generate cache key for provider/channel"""
|
|
return f"{provider}_{channel_id}"
|
|
|
|
@staticmethod
|
|
def _get_manifest_filename(cache_key: str) -> str:
|
|
"""Get filename for cached manifest"""
|
|
return f"{cache_key}.xml"
|
|
|
|
@staticmethod
|
|
def _get_meta_filename(cache_key: str) -> str:
|
|
"""Get filename for cache metadata"""
|
|
return f"{cache_key}.meta"
|
|
|
|
def get(self, provider: str, channel_id: str, max_stale: int = 0) -> Optional[str]:
|
|
"""
|
|
Get cached MPD manifest if valid.
|
|
|
|
Args:
|
|
provider: Provider name
|
|
channel_id: Channel ID
|
|
max_stale: If > 0, still return manifest content up to this many
|
|
seconds past expiry instead of None/deleting it. The
|
|
caller is expected to only pass this when a live
|
|
refetch has already failed — a stale manifest is a
|
|
fallback of last resort, not a normal cache hit.
|
|
Default 0 preserves existing behaviour everywhere
|
|
this is called without the new argument.
|
|
|
|
Returns:
|
|
Cached MPD content if valid (or within max_stale), None otherwise
|
|
"""
|
|
cache_key = self._get_cache_key(provider, channel_id)
|
|
meta_file = self._get_meta_filename(cache_key)
|
|
manifest_file = self._get_manifest_filename(cache_key)
|
|
|
|
try:
|
|
meta = self.vfs.read_json(meta_file)
|
|
if not meta:
|
|
logger.debug(f"No cache metadata found for {cache_key}")
|
|
return None
|
|
|
|
expiry = meta.get("expiry", 0)
|
|
now = int(time.time())
|
|
|
|
if now >= expiry:
|
|
staleness = now - expiry
|
|
if max_stale and staleness <= max_stale:
|
|
manifest_content = self.vfs.read_text(manifest_file)
|
|
if manifest_content:
|
|
logger.warning(
|
|
f"Serving STALE cache for {cache_key} "
|
|
f"({staleness}s past expiry, max_stale={max_stale}s)"
|
|
)
|
|
return manifest_content
|
|
# meta exists but file missing — nothing to serve stale
|
|
logger.debug(
|
|
f"Cache expired for {cache_key} (expired {staleness}s ago)"
|
|
)
|
|
# Deliberately NOT deleting the expired files here. This get()
|
|
# call may have been made with the default max_stale=0 (a
|
|
# normal cache lookup), but a *different* caller further down
|
|
# the request chain may retry with max_stale>0 as a
|
|
# last-resort fallback after a live refetch fails. If we
|
|
# delete on every plain expiry check, that fallback caller
|
|
# always finds nothing — the fallback only ever worked when
|
|
# it happened to be the first caller to see the expired
|
|
# entry. Expired entries are reaped on their own schedule by
|
|
# clear_expired() instead, so both call patterns keep working
|
|
# regardless of ordering.
|
|
return None
|
|
|
|
manifest_content = self.vfs.read_text(manifest_file)
|
|
if manifest_content:
|
|
logger.info(f"Cache hit for {cache_key} (expires in {expiry - now}s)")
|
|
return manifest_content
|
|
else:
|
|
logger.warning(
|
|
f"Cache metadata exists but manifest file missing for {cache_key}"
|
|
)
|
|
self.vfs.delete(meta_file)
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading cache for {cache_key}: {e}")
|
|
return None
|
|
|
|
def set(
|
|
self,
|
|
provider: str,
|
|
channel_id: str,
|
|
mpd_content: str,
|
|
ttl: int,
|
|
original_url: Optional[str] = None,
|
|
) -> bool:
|
|
"""
|
|
Store MPD manifest in cache with TTL
|
|
|
|
Args:
|
|
provider: Provider name
|
|
channel_id: Channel ID
|
|
mpd_content: Rewritten MPD content
|
|
ttl: Time to live in seconds
|
|
original_url: Original manifest URL (for debugging)
|
|
|
|
Returns:
|
|
True if successfully cached, False otherwise
|
|
"""
|
|
cache_key = self._get_cache_key(provider, channel_id)
|
|
meta_file = self._get_meta_filename(cache_key)
|
|
manifest_file = self._get_manifest_filename(cache_key)
|
|
|
|
try:
|
|
# Calculate expiry timestamp
|
|
expiry = int(time.time()) + ttl
|
|
|
|
# Create metadata
|
|
meta = {
|
|
"expiry": expiry,
|
|
"ttl": ttl,
|
|
"cached_at": int(time.time()),
|
|
"provider": provider,
|
|
"channel_id": channel_id,
|
|
}
|
|
|
|
if original_url:
|
|
meta["original_url"] = original_url
|
|
|
|
# Write manifest
|
|
if not self.vfs.write_text(manifest_file, mpd_content):
|
|
logger.error(f"Failed to write manifest cache for {cache_key}")
|
|
return False
|
|
|
|
# Write metadata
|
|
if not self.vfs.write_json(meta_file, meta):
|
|
logger.error(f"Failed to write metadata cache for {cache_key}")
|
|
# Clean up manifest if metadata write failed
|
|
self.vfs.delete(manifest_file)
|
|
return False
|
|
|
|
logger.info(
|
|
f"Cached MPD for {cache_key} with TTL={ttl}s (expires at {expiry})"
|
|
)
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error caching MPD for {cache_key}: {e}")
|
|
return False
|
|
|
|
def delete(self, provider: str, channel_id: str) -> bool:
|
|
"""
|
|
Delete cached MPD for a channel
|
|
|
|
Args:
|
|
provider: Provider name
|
|
channel_id: Channel ID
|
|
|
|
Returns:
|
|
True if deleted or didn't exist, False on error
|
|
"""
|
|
cache_key = self._get_cache_key(provider, channel_id)
|
|
meta_file = self._get_meta_filename(cache_key)
|
|
manifest_file = self._get_manifest_filename(cache_key)
|
|
|
|
try:
|
|
self.vfs.delete(manifest_file)
|
|
self.vfs.delete(meta_file)
|
|
logger.debug(f"Deleted cache for {cache_key}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error deleting cache for {cache_key}: {e}")
|
|
return False
|
|
|
|
def clear_all(self) -> bool:
|
|
"""
|
|
Clear all cached MPD files
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
try:
|
|
files = self.vfs.listdir()
|
|
deleted = 0
|
|
|
|
for file in files:
|
|
if file.endswith(".xml") or file.endswith(".meta"):
|
|
if self.vfs.delete(file):
|
|
deleted += 1
|
|
|
|
logger.info(f"Cleared {deleted} cached MPD files")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error clearing MPD cache: {e}")
|
|
return False
|
|
|
|
def clear_expired(self) -> int:
|
|
"""
|
|
Clear all expired cached MPD files
|
|
|
|
Returns:
|
|
Number of expired entries cleared
|
|
"""
|
|
try:
|
|
files = self.vfs.listdir()
|
|
now = int(time.time())
|
|
cleared = 0
|
|
|
|
# Find all meta files
|
|
meta_files = [f for f in files if f.endswith(".meta")]
|
|
|
|
for meta_file in meta_files:
|
|
try:
|
|
meta = self.vfs.read_json(meta_file)
|
|
if meta and meta.get("expiry", 0) < now:
|
|
# Expired, delete both meta and manifest
|
|
cache_key = meta_file.replace(".meta", "")
|
|
self.vfs.delete(f"{cache_key}.xml")
|
|
self.vfs.delete(meta_file)
|
|
cleared += 1
|
|
logger.debug(f"Cleared expired cache: {cache_key}")
|
|
except Exception as e:
|
|
logger.warning(f"Error checking {meta_file}: {e}")
|
|
|
|
if cleared > 0:
|
|
logger.info(f"Cleared {cleared} expired MPD cache entries")
|
|
|
|
return cleared
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error clearing expired caches: {e}")
|
|
return 0
|
|
|
|
def get_cache_info(self, provider: str, channel_id: str) -> Optional[dict]:
|
|
"""
|
|
Get cache information without reading the full manifest
|
|
|
|
Args:
|
|
provider: Provider name
|
|
channel_id: Channel ID
|
|
|
|
Returns:
|
|
Cache metadata dict or None if not cached
|
|
"""
|
|
cache_key = self._get_cache_key(provider, channel_id)
|
|
meta_file = self._get_meta_filename(cache_key)
|
|
|
|
try:
|
|
meta = self.vfs.read_json(meta_file)
|
|
if meta:
|
|
now = int(time.time())
|
|
meta["expired"] = now >= meta.get("expiry", 0)
|
|
meta["remaining_ttl"] = max(0, meta.get("expiry", 0) - now)
|
|
return meta
|
|
except Exception as e:
|
|
logger.debug(f"Error getting cache info for {cache_key}: {e}")
|
|
return None |