mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-21 00:22:30 +02:00
Reformat
This commit is contained in:
@@ -83,16 +83,22 @@ class EnvironmentManager:
|
||||
|
||||
# Get settings
|
||||
default_country = self._addon.getSetting("default_country")
|
||||
self._config["default_country"] = str(default_country) if default_country else "DE"
|
||||
self._config["default_country"] = (
|
||||
str(default_country) if default_country else "DE"
|
||||
)
|
||||
|
||||
server_port = self._addon.getSetting("server_port")
|
||||
try:
|
||||
self._config["server_port"] = int(str(server_port)) if server_port else 7777
|
||||
self._config["server_port"] = (
|
||||
int(str(server_port)) if server_port else 7777
|
||||
)
|
||||
except ValueError:
|
||||
self._config["server_port"] = 7777
|
||||
|
||||
except Exception as init_error: # noqa: B902
|
||||
print(f"DEBUG: Exception type: {type(init_error).__name__}", file=sys.stderr)
|
||||
print(
|
||||
f"DEBUG: Exception type: {type(init_error).__name__}", file=sys.stderr
|
||||
)
|
||||
print(f"DEBUG: Exception message: {str(init_error)}", file=sys.stderr)
|
||||
# Log the error and fallback to standalone
|
||||
self._log_init_error("Kodi initialization failed", init_error)
|
||||
@@ -114,7 +120,9 @@ class EnvironmentManager:
|
||||
self._config["addon_path"] = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Default configuration paths
|
||||
config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(str(Path.home()), ".config")
|
||||
config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
|
||||
str(Path.home()), ".config"
|
||||
)
|
||||
self._config["config_dir"] = os.path.join(config_home, "ultimate-backend")
|
||||
self._config["profile_path"] = self._config["config_dir"]
|
||||
|
||||
@@ -240,7 +248,9 @@ class EnvironmentManager:
|
||||
else:
|
||||
raise ImportError("get_configured_manager is not callable")
|
||||
else:
|
||||
raise ImportError("get_configured_manager not found in streaming_providers module")
|
||||
raise ImportError(
|
||||
"get_configured_manager not found in streaming_providers module"
|
||||
)
|
||||
except ImportError as manager_error:
|
||||
# Log error using the logger once we have it
|
||||
logger_instance = self.get_logger()
|
||||
|
||||
@@ -50,7 +50,9 @@ class BaseLogger:
|
||||
log_message += f" - {details}"
|
||||
self.info(log_message)
|
||||
|
||||
def log_credential_event(self, provider: str, event: str, details: str = "") -> None:
|
||||
def log_credential_event(
|
||||
self, provider: str, event: str, details: str = ""
|
||||
) -> None:
|
||||
"""Log credential event"""
|
||||
log_message = f"CRED [{provider}] {event}"
|
||||
if details:
|
||||
|
||||
@@ -53,7 +53,10 @@ class ManifestParser:
|
||||
system_id = match.group(1).lower()
|
||||
|
||||
# Skip mp4protection scheme
|
||||
if "mp4protection" in manifest_content[max(0, match.start() - 100) : match.start()]:
|
||||
if (
|
||||
"mp4protection"
|
||||
in manifest_content[max(0, match.start() - 100) : match.start()]
|
||||
):
|
||||
continue
|
||||
|
||||
drm_system = DRMSystem.from_uuid(system_id)
|
||||
@@ -83,7 +86,9 @@ class ManifestParser:
|
||||
|
||||
# Filter for expected DRM systems if provided
|
||||
if expected_system_ids:
|
||||
filtered_pssh = [p for p in pssh_from_segment if p.system_id in expected_system_ids]
|
||||
filtered_pssh = [
|
||||
p for p in pssh_from_segment if p.system_id in expected_system_ids
|
||||
]
|
||||
if filtered_pssh:
|
||||
logger.debug(f"Found {len(filtered_pssh)} PSSH boxes in segment")
|
||||
return filtered_pssh
|
||||
@@ -127,7 +132,9 @@ class ManifestParser:
|
||||
|
||||
# Compile patterns once
|
||||
pssh_pattern = re.compile(r"<(?:cenc:)?pssh[^>]*>([^<]+)</(?:cenc:)?pssh>")
|
||||
default_kid_pattern = re.compile(r'(?:cenc:)?default_KID="([^"]+)"', re.IGNORECASE)
|
||||
default_kid_pattern = re.compile(
|
||||
r'(?:cenc:)?default_KID="([^"]+)"', re.IGNORECASE
|
||||
)
|
||||
system_id_pattern = re.compile(r'schemeIdUri="urn:uuid:([^"]+)"', re.IGNORECASE)
|
||||
|
||||
# Find ContentProtection blocks efficiently
|
||||
@@ -191,14 +198,18 @@ class ManifestParser:
|
||||
return list(pssh_dict.values())
|
||||
|
||||
@staticmethod
|
||||
def extract_single_init_segment_url(manifest_content: str, manifest_url: str) -> Optional[str]:
|
||||
def extract_single_init_segment_url(
|
||||
manifest_content: str, manifest_url: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Extract ONE init segment URL from DASH manifest.
|
||||
Prioritizes video representations as they typically have the same DRM as audio.
|
||||
"""
|
||||
# Parse manifest base URL
|
||||
parsed = urlparse(manifest_url)
|
||||
manifest_base = f"{parsed.scheme}://{parsed.netloc}{'/'.join(parsed.path.split('/')[:-1])}"
|
||||
manifest_base = (
|
||||
f"{parsed.scheme}://{parsed.netloc}{'/'.join(parsed.path.split('/')[:-1])}"
|
||||
)
|
||||
if not manifest_base.endswith("/"):
|
||||
manifest_base += "/"
|
||||
|
||||
@@ -288,17 +299,25 @@ class ManifestParser:
|
||||
DEPRECATED: Use extract_single_init_segment_url instead.
|
||||
This extracts ALL segments which is inefficient.
|
||||
"""
|
||||
logger.warning("extract_segment_urls is deprecated, use extract_single_init_segment_url")
|
||||
init_url = ManifestParser.extract_single_init_segment_url(manifest_content, manifest_url)
|
||||
logger.warning(
|
||||
"extract_segment_urls is deprecated, use extract_single_init_segment_url"
|
||||
)
|
||||
init_url = ManifestParser.extract_single_init_segment_url(
|
||||
manifest_content, manifest_url
|
||||
)
|
||||
return [init_url] if init_url else []
|
||||
|
||||
@staticmethod
|
||||
def extract_init_segment_urls(manifest_content: str, manifest_url: str) -> List[str]:
|
||||
def extract_init_segment_urls(
|
||||
manifest_content: str, manifest_url: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
DEPRECATED: Use extract_single_init_segment_url instead.
|
||||
"""
|
||||
logger.warning(
|
||||
"extract_init_segment_urls is deprecated, use extract_single_init_segment_url"
|
||||
)
|
||||
init_url = ManifestParser.extract_single_init_segment_url(manifest_content, manifest_url)
|
||||
init_url = ManifestParser.extract_single_init_segment_url(
|
||||
manifest_content, manifest_url
|
||||
)
|
||||
return [init_url] if init_url else []
|
||||
|
||||
@@ -44,7 +44,7 @@ class MP4PSSHExtractor:
|
||||
if offset + 4 > len(data):
|
||||
break
|
||||
|
||||
box_size = struct.unpack(">I", data[offset: offset + 4])[0]
|
||||
box_size = struct.unpack(">I", data[offset : offset + 4])[0]
|
||||
if box_size == 0:
|
||||
box_size = len(data) - offset # Box extends to end of file
|
||||
elif box_size == 1:
|
||||
@@ -55,11 +55,13 @@ class MP4PSSHExtractor:
|
||||
break
|
||||
|
||||
# Read box type (4 bytes)
|
||||
box_type = data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_type = data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "moov":
|
||||
# Look for PSSH in moov box
|
||||
moov_data = data[offset: offset + box_size]
|
||||
moov_data = data[offset : offset + box_size]
|
||||
pssh_in_moov = MP4PSSHExtractor._extract_from_moov(moov_data)
|
||||
|
||||
# Enhance PSSH data with tenc KIDs if needed
|
||||
@@ -70,7 +72,9 @@ class MP4PSSHExtractor:
|
||||
|
||||
elif box_type == "pssh":
|
||||
# Found standalone PSSH box
|
||||
pssh_box = MP4PSSHExtractor._parse_pssh_box(data[offset: offset + box_size])
|
||||
pssh_box = MP4PSSHExtractor._parse_pssh_box(
|
||||
data[offset : offset + box_size]
|
||||
)
|
||||
if pssh_box:
|
||||
# Add tenc KIDs if PSSH doesn't have its own
|
||||
if not pssh_box.key_ids and tenc_kids:
|
||||
@@ -96,28 +100,40 @@ class MP4PSSHExtractor:
|
||||
if offset + 8 > len(data):
|
||||
break
|
||||
|
||||
box_size = struct.unpack(">I", data[offset:offset + 4])[0]
|
||||
box_size = struct.unpack(">I", data[offset : offset + 4])[0]
|
||||
if box_size < 8 or offset + box_size > len(data):
|
||||
offset += 1
|
||||
continue
|
||||
|
||||
box_type = data[offset + 4:offset + 8]
|
||||
box_type = data[offset + 4 : offset + 8]
|
||||
|
||||
if box_type == b'tenc':
|
||||
if box_type == b"tenc":
|
||||
# Parse tenc box
|
||||
logger.debug(f"Found tenc box at offset {offset} (0x{offset:x}), size {box_size}")
|
||||
tenc_kid = MP4PSSHExtractor._parse_tenc_box(data[offset:offset + box_size])
|
||||
logger.debug(
|
||||
f"Found tenc box at offset {offset} (0x{offset:x}), size {box_size}"
|
||||
)
|
||||
tenc_kid = MP4PSSHExtractor._parse_tenc_box(
|
||||
data[offset : offset + box_size]
|
||||
)
|
||||
if tenc_kid and tenc_kid not in kids:
|
||||
kids.append(tenc_kid)
|
||||
elif box_size > 8:
|
||||
# Recursively search inside container boxes
|
||||
container_boxes = {
|
||||
b'moov', b'trak', b'mdia', b'minf', b'stbl',
|
||||
b'stsd', b'encv', b'sinf', b'schi', b'udta'
|
||||
b"moov",
|
||||
b"trak",
|
||||
b"mdia",
|
||||
b"minf",
|
||||
b"stbl",
|
||||
b"stsd",
|
||||
b"encv",
|
||||
b"sinf",
|
||||
b"schi",
|
||||
b"udta",
|
||||
}
|
||||
if box_type in container_boxes:
|
||||
# Recursively search inside the container
|
||||
inner_data = data[offset + 8:offset + box_size]
|
||||
inner_data = data[offset + 8 : offset + box_size]
|
||||
inner_kids = MP4PSSHExtractor._extract_all_tenc_kids(inner_data)
|
||||
for kid in inner_kids:
|
||||
if kid not in kids:
|
||||
@@ -136,14 +152,16 @@ class MP4PSSHExtractor:
|
||||
try:
|
||||
# tenc box should be at least 32 bytes for version 0 with KID
|
||||
if len(tenc_bytes) < 32:
|
||||
logger.debug(f"tenc box too small: {len(tenc_bytes)} bytes, need at least 32")
|
||||
logger.debug(
|
||||
f"tenc box too small: {len(tenc_bytes)} bytes, need at least 32"
|
||||
)
|
||||
return None
|
||||
|
||||
# Parse box header
|
||||
box_size = struct.unpack(">I", tenc_bytes[0:4])[0]
|
||||
box_type = tenc_bytes[4:8]
|
||||
|
||||
if box_type != b'tenc':
|
||||
if box_type != b"tenc":
|
||||
logger.debug(f"Not a tenc box, type: {box_type.hex()}")
|
||||
return None
|
||||
|
||||
@@ -174,7 +192,8 @@ class MP4PSSHExtractor:
|
||||
kid_offset = 16
|
||||
|
||||
logger.debug(
|
||||
f"tenc v0: is_encrypted={is_encrypted}, default_iv_size={default_iv_size}, kid_offset={kid_offset}")
|
||||
f"tenc v0: is_encrypted={is_encrypted}, default_iv_size={default_iv_size}, kid_offset={kid_offset}"
|
||||
)
|
||||
|
||||
elif version == 1:
|
||||
# Version 1 has different structure
|
||||
@@ -185,11 +204,13 @@ class MP4PSSHExtractor:
|
||||
return None
|
||||
|
||||
if kid_offset + 16 > len(tenc_bytes):
|
||||
logger.debug(f"tenc box too small for KID: {len(tenc_bytes)} bytes, need {kid_offset + 16}")
|
||||
logger.debug(
|
||||
f"tenc box too small for KID: {len(tenc_bytes)} bytes, need {kid_offset + 16}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Extract KID bytes
|
||||
kid_bytes = tenc_bytes[kid_offset:kid_offset + 16]
|
||||
kid_bytes = tenc_bytes[kid_offset : kid_offset + 16]
|
||||
hex_kid = kid_bytes.hex()
|
||||
|
||||
logger.debug(f"KID bytes at offset {kid_offset}: {hex_kid}")
|
||||
@@ -219,19 +240,21 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(moov_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", moov_data[offset: offset + 4])[0]
|
||||
box_type = moov_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", moov_data[offset : offset + 4])[0]
|
||||
box_type = moov_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "trak":
|
||||
# Parse track for PSSH
|
||||
trak_data = moov_data[offset: offset + box_size]
|
||||
trak_data = moov_data[offset : offset + box_size]
|
||||
pssh_in_trak = MP4PSSHExtractor._extract_from_trak(trak_data)
|
||||
pssh_list.extend(pssh_in_trak)
|
||||
|
||||
elif box_type == "pssh":
|
||||
# PSSH directly in moov
|
||||
pssh_box = MP4PSSHExtractor._parse_pssh_box(
|
||||
moov_data[offset: offset + box_size]
|
||||
moov_data[offset : offset + box_size]
|
||||
)
|
||||
if pssh_box:
|
||||
pssh_list.append(pssh_box)
|
||||
@@ -251,11 +274,13 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(trak_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", trak_data[offset: offset + 4])[0]
|
||||
box_type = trak_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", trak_data[offset : offset + 4])[0]
|
||||
box_type = trak_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "mdia":
|
||||
mdia_data = trak_data[offset: offset + box_size]
|
||||
mdia_data = trak_data[offset : offset + box_size]
|
||||
pssh_in_mdia = MP4PSSHExtractor._extract_from_mdia(mdia_data)
|
||||
pssh_list.extend(pssh_in_mdia)
|
||||
|
||||
@@ -274,11 +299,13 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(mdia_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", mdia_data[offset: offset + 4])[0]
|
||||
box_type = mdia_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", mdia_data[offset : offset + 4])[0]
|
||||
box_type = mdia_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "minf":
|
||||
minf_data = mdia_data[offset: offset + box_size]
|
||||
minf_data = mdia_data[offset : offset + box_size]
|
||||
pssh_in_minf = MP4PSSHExtractor._extract_from_minf(minf_data)
|
||||
pssh_list.extend(pssh_in_minf)
|
||||
|
||||
@@ -297,11 +324,13 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(minf_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", minf_data[offset: offset + 4])[0]
|
||||
box_type = minf_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", minf_data[offset : offset + 4])[0]
|
||||
box_type = minf_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "stbl":
|
||||
stbl_data = minf_data[offset: offset + box_size]
|
||||
stbl_data = minf_data[offset : offset + box_size]
|
||||
pssh_in_stbl = MP4PSSHExtractor._extract_from_stbl(stbl_data)
|
||||
pssh_list.extend(pssh_in_stbl)
|
||||
|
||||
@@ -320,11 +349,13 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(stbl_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", stbl_data[offset: offset + 4])[0]
|
||||
box_type = stbl_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", stbl_data[offset : offset + 4])[0]
|
||||
box_type = stbl_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "sinf":
|
||||
sinf_data = stbl_data[offset: offset + box_size]
|
||||
sinf_data = stbl_data[offset : offset + box_size]
|
||||
pssh_in_sinf = MP4PSSHExtractor._extract_from_sinf(sinf_data)
|
||||
pssh_list.extend(pssh_in_sinf)
|
||||
|
||||
@@ -343,11 +374,13 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(sinf_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", sinf_data[offset: offset + 4])[0]
|
||||
box_type = sinf_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", sinf_data[offset : offset + 4])[0]
|
||||
box_type = sinf_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "schi":
|
||||
schi_data = sinf_data[offset: offset + box_size]
|
||||
schi_data = sinf_data[offset : offset + box_size]
|
||||
pssh_in_schi = MP4PSSHExtractor._extract_from_schi(schi_data)
|
||||
pssh_list.extend(pssh_in_schi)
|
||||
|
||||
@@ -366,12 +399,14 @@ class MP4PSSHExtractor:
|
||||
|
||||
while offset < len(schi_data):
|
||||
try:
|
||||
box_size = struct.unpack(">I", schi_data[offset: offset + 4])[0]
|
||||
box_type = schi_data[offset + 4: offset + 8].decode("ascii", errors="ignore")
|
||||
box_size = struct.unpack(">I", schi_data[offset : offset + 4])[0]
|
||||
box_type = schi_data[offset + 4 : offset + 8].decode(
|
||||
"ascii", errors="ignore"
|
||||
)
|
||||
|
||||
if box_type == "pssh":
|
||||
pssh_box = MP4PSSHExtractor._parse_pssh_box(
|
||||
schi_data[offset: offset + box_size]
|
||||
schi_data[offset : offset + box_size]
|
||||
)
|
||||
if pssh_box:
|
||||
pssh_list.append(pssh_box)
|
||||
@@ -413,7 +448,9 @@ class MP4PSSHExtractor:
|
||||
if current_offset + 4 > len(pssh_bytes):
|
||||
return None
|
||||
|
||||
kid_count = struct.unpack(">I", pssh_bytes[current_offset: current_offset + 4])[0]
|
||||
kid_count = struct.unpack(
|
||||
">I", pssh_bytes[current_offset : current_offset + 4]
|
||||
)[0]
|
||||
current_offset += 4
|
||||
|
||||
# Read each KID
|
||||
@@ -421,12 +458,14 @@ class MP4PSSHExtractor:
|
||||
if current_offset + 16 > len(pssh_bytes):
|
||||
break
|
||||
|
||||
kid_bytes = pssh_bytes[current_offset: current_offset + 16]
|
||||
kid_bytes = pssh_bytes[current_offset : current_offset + 16]
|
||||
kid_uuid = str(uuid.UUID(bytes=kid_bytes))
|
||||
key_ids.append(kid_uuid.replace("-", "").lower())
|
||||
current_offset += 16
|
||||
else:
|
||||
logger.debug(f"Version 0 PSSH for system {system_id}, will look for KIDs in tenc box")
|
||||
logger.debug(
|
||||
f"Version 0 PSSH for system {system_id}, will look for KIDs in tenc box"
|
||||
)
|
||||
|
||||
# Encode entire PSSH box as base64
|
||||
pssh_b64 = base64.b64encode(pssh_bytes[:box_size]).decode("ascii")
|
||||
@@ -440,4 +479,4 @@ class MP4PSSHExtractor:
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse PSSH box: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -58,7 +58,9 @@ class MPDCacheManager:
|
||||
now = int(time.time())
|
||||
|
||||
if now >= expiry:
|
||||
logger.debug(f"Cache expired for {cache_key} (expired {now - expiry}s ago)")
|
||||
logger.debug(
|
||||
f"Cache expired for {cache_key} (expired {now - expiry}s ago)"
|
||||
)
|
||||
# Clean up expired cache
|
||||
self.vfs.delete(manifest_file)
|
||||
self.vfs.delete(meta_file)
|
||||
@@ -70,7 +72,9 @@ class MPDCacheManager:
|
||||
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}")
|
||||
logger.warning(
|
||||
f"Cache metadata exists but manifest file missing for {cache_key}"
|
||||
)
|
||||
self.vfs.delete(meta_file)
|
||||
return None
|
||||
|
||||
@@ -131,7 +135,9 @@ class MPDCacheManager:
|
||||
self.vfs.delete(manifest_file)
|
||||
return False
|
||||
|
||||
logger.info(f"Cached MPD for {cache_key} with TTL={ttl}s (expires at {expiry})")
|
||||
logger.info(
|
||||
f"Cached MPD for {cache_key} with TTL={ttl}s (expires at {expiry})"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -22,8 +22,12 @@ class MPDRewriter:
|
||||
# MPD namespace
|
||||
MPD_NAMESPACE = {"mpd": "urn:mpeg:dash:schema:mpd:2011"}
|
||||
|
||||
def __init__(self, media_proxy_url: str, provider_proxy_url: Optional[str] = None,
|
||||
clearkey_keyids: Optional[dict] = None):
|
||||
def __init__(
|
||||
self,
|
||||
media_proxy_url: str,
|
||||
provider_proxy_url: Optional[str] = None,
|
||||
clearkey_keyids: Optional[dict] = None,
|
||||
):
|
||||
"""
|
||||
Initialize MPD rewriter
|
||||
|
||||
@@ -59,60 +63,50 @@ class MPDRewriter:
|
||||
encoded += "=" * padding
|
||||
return base64.urlsafe_b64decode(encoded.encode("utf-8")).decode("utf-8")
|
||||
|
||||
def build_proxy_url(self, original_url: str, template_pattern: Optional[str] = None,
|
||||
segment_type: Optional[str] = None, is_encrypted: bool = True) -> str:
|
||||
def build_proxy_url(
|
||||
self,
|
||||
original_url: str,
|
||||
template_pattern: Optional[str] = None,
|
||||
segment_type: Optional[str] = None,
|
||||
is_encrypted: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Build media proxy URL for an original media URL
|
||||
Build media proxy URL with structured parameter encoding
|
||||
|
||||
Args:
|
||||
original_url: Original URL to be proxied (base path for templates)
|
||||
template_pattern: Optional template pattern to append (e.g., "segment-$Number$.m4s")
|
||||
segment_type: Optional segment type ('initialization' or 'media') for selective DRM params
|
||||
is_encrypted: Whether the segment is encrypted (has ContentProtection)
|
||||
|
||||
Returns:
|
||||
Media proxy URL
|
||||
Format: url={original}&key={key}&kid={kid}&proxy={proxy}
|
||||
Then base64 encode the entire string
|
||||
"""
|
||||
encoded = self.encode_url(original_url)
|
||||
# Start with the original URL
|
||||
param_string = f"url={original_url}"
|
||||
|
||||
# Choose endpoint based on whether we have clearkey data AND segment is encrypted
|
||||
# Add clearkey parameters if needed
|
||||
if self.clearkey_keyids and is_encrypted:
|
||||
for kid, key in self.clearkey_keyids.items():
|
||||
if segment_type == "initialization":
|
||||
param_string += f"&kid={kid}"
|
||||
elif segment_type == "media":
|
||||
param_string += f"&key={key}"
|
||||
else:
|
||||
param_string += f"&kid={kid}&key={key}"
|
||||
|
||||
# Add provider proxy parameter
|
||||
if self.provider_proxy_url:
|
||||
param_string += f"&proxy={self.provider_proxy_url}"
|
||||
|
||||
# Encode the complete parameter string
|
||||
encoded = self.encode_url(param_string)
|
||||
|
||||
# Choose endpoint
|
||||
if self.clearkey_keyids and is_encrypted:
|
||||
proxy_url = f"{self.media_proxy_url}/api/decrypt/{encoded}"
|
||||
else:
|
||||
proxy_url = f"{self.media_proxy_url}/api/proxy/{encoded}"
|
||||
|
||||
# Append template pattern if provided (keeps variables visible for client)
|
||||
# Append template pattern if provided
|
||||
if template_pattern:
|
||||
# URL encode the template pattern (same as current behavior)
|
||||
encoded_pattern = quote(template_pattern, safe=".-_$")
|
||||
proxy_url += f"/{encoded_pattern}"
|
||||
|
||||
# Build query parameters
|
||||
query_params = []
|
||||
|
||||
# Add clearkey parameters ONLY if present AND segment is encrypted
|
||||
if self.clearkey_keyids and is_encrypted:
|
||||
for kid, key in self.clearkey_keyids.items():
|
||||
# Initialization segments: only add kid
|
||||
# Media segments: only add key
|
||||
# Unknown/unspecified: add both (backward compatible)
|
||||
if segment_type == 'initialization':
|
||||
query_params.append(f"kid={kid}")
|
||||
elif segment_type == 'media':
|
||||
query_params.append(f"key={key}")
|
||||
else:
|
||||
# Default behavior: add both
|
||||
query_params.append(f"kid={kid}")
|
||||
query_params.append(f"key={key}")
|
||||
|
||||
# Add provider proxy parameter if configured (always, for all segments)
|
||||
if self.provider_proxy_url:
|
||||
query_params.append(f"proxy={self.provider_proxy_url}")
|
||||
|
||||
# Append query string if we have parameters
|
||||
if query_params:
|
||||
proxy_url += "?" + "&".join(query_params)
|
||||
|
||||
return proxy_url
|
||||
|
||||
@staticmethod
|
||||
@@ -142,7 +136,7 @@ class MPDRewriter:
|
||||
return "", url
|
||||
|
||||
base_path = url[:last_slash_before_template]
|
||||
template_pattern = url[last_slash_before_template + 1:]
|
||||
template_pattern = url[last_slash_before_template + 1 :]
|
||||
|
||||
return base_path, template_pattern
|
||||
|
||||
@@ -182,11 +176,15 @@ class MPDRewriter:
|
||||
# If we're in decryption mode, identify encrypted AdaptationSets first
|
||||
if self.clearkey_keyids:
|
||||
self._identify_encrypted_adaptation_sets(root)
|
||||
logger.debug(f"Identified {len(self.encrypted_adaptation_sets)} encrypted AdaptationSets")
|
||||
logger.debug(
|
||||
f"Identified {len(self.encrypted_adaptation_sets)} encrypted AdaptationSets"
|
||||
)
|
||||
|
||||
# Then remove ContentProtection elements
|
||||
self._remove_content_protection(root)
|
||||
logger.debug("Removed ContentProtection elements for decrypted playback")
|
||||
logger.debug(
|
||||
"Removed ContentProtection elements for decrypted playback"
|
||||
)
|
||||
|
||||
# Rewrite all URLs in the MPD
|
||||
self._rewrite_urls_recursive(root, base_url)
|
||||
@@ -234,7 +232,9 @@ class MPDRewriter:
|
||||
parsed_manifest = urlparse(manifest_url)
|
||||
manifest_dir = f"{parsed_manifest.scheme}://{parsed_manifest.netloc}{parsed_manifest.path.rsplit('/', 1)[0]}/"
|
||||
resolved_base = urljoin(manifest_dir, base_url_text)
|
||||
logger.debug(f"Resolved relative BaseURL '{base_url_text}' to: {resolved_base}")
|
||||
logger.debug(
|
||||
f"Resolved relative BaseURL '{base_url_text}' to: {resolved_base}"
|
||||
)
|
||||
return resolved_base
|
||||
else:
|
||||
# It's already an absolute URL
|
||||
@@ -256,7 +256,9 @@ class MPDRewriter:
|
||||
"""
|
||||
# Find all BaseURL elements at any level
|
||||
for parent in root.findall(".//*"):
|
||||
for base_url_elem in list(parent.findall("mpd:BaseURL", self.MPD_NAMESPACE)):
|
||||
for base_url_elem in list(
|
||||
parent.findall("mpd:BaseURL", self.MPD_NAMESPACE)
|
||||
):
|
||||
parent.remove(base_url_elem)
|
||||
logger.debug("Removed BaseURL element")
|
||||
|
||||
@@ -273,7 +275,9 @@ class MPDRewriter:
|
||||
# Check if this AdaptationSet has ContentProtection
|
||||
if adaptation_set.findall("mpd:ContentProtection", self.MPD_NAMESPACE):
|
||||
# Get AdaptationSet ID for tracking
|
||||
as_id = adaptation_set.get("id", id(adaptation_set)) # Use object id as fallback
|
||||
as_id = adaptation_set.get(
|
||||
"id", id(adaptation_set)
|
||||
) # Use object id as fallback
|
||||
self.encrypted_adaptation_sets.add(str(as_id))
|
||||
logger.debug(f"AdaptationSet id={as_id} is encrypted")
|
||||
|
||||
@@ -287,11 +291,15 @@ class MPDRewriter:
|
||||
"""
|
||||
# Find all ContentProtection elements at any level
|
||||
for parent in root.findall(".//*"):
|
||||
for cp_elem in list(parent.findall("mpd:ContentProtection", self.MPD_NAMESPACE)):
|
||||
for cp_elem in list(
|
||||
parent.findall("mpd:ContentProtection", self.MPD_NAMESPACE)
|
||||
):
|
||||
parent.remove(cp_elem)
|
||||
logger.debug("Removed ContentProtection element")
|
||||
|
||||
def _is_element_in_encrypted_adaptation_set(self, element: ET.Element, root: ET.Element) -> bool:
|
||||
def _is_element_in_encrypted_adaptation_set(
|
||||
self, element: ET.Element, root: ET.Element
|
||||
) -> bool:
|
||||
"""
|
||||
Check if an element is within an encrypted AdaptationSet
|
||||
|
||||
@@ -333,7 +341,9 @@ class MPDRewriter:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _rewrite_urls_recursive(self, element: ET.Element, base_url: str, root: Optional[ET.Element] = None) -> None:
|
||||
def _rewrite_urls_recursive(
|
||||
self, element: ET.Element, base_url: str, root: Optional[ET.Element] = None
|
||||
) -> None:
|
||||
"""
|
||||
Recursively rewrite all URLs in MPD element tree
|
||||
|
||||
@@ -353,13 +363,13 @@ class MPDRewriter:
|
||||
|
||||
# Determine segment type based on attribute name
|
||||
segment_type_map = {
|
||||
'initialization': 'initialization',
|
||||
'media': 'media',
|
||||
'sourceURL': None, # Could be either, keep default
|
||||
"initialization": "initialization",
|
||||
"media": "media",
|
||||
"sourceURL": None, # Could be either, keep default
|
||||
}
|
||||
|
||||
# Rewrite URL attributes in current element
|
||||
for attr in ['media', 'initialization', 'sourceURL']:
|
||||
for attr in ["media", "initialization", "sourceURL"]:
|
||||
if attr in element.attrib:
|
||||
original_url = element.attrib[attr]
|
||||
|
||||
@@ -387,7 +397,9 @@ class MPDRewriter:
|
||||
element.attrib[attr] = self.build_proxy_url(
|
||||
resolved, None, segment_type, is_encrypted
|
||||
)
|
||||
logger.debug(f"Rewrote URL ({attr}, encrypted={is_encrypted}): {original_url} -> media proxy")
|
||||
logger.debug(
|
||||
f"Rewrote URL ({attr}, encrypted={is_encrypted}): {original_url} -> media proxy"
|
||||
)
|
||||
|
||||
# Handle SegmentURL elements (used in SegmentList)
|
||||
if element.tag.endswith("SegmentURL"):
|
||||
@@ -399,11 +411,11 @@ class MPDRewriter:
|
||||
if "$" in resolved:
|
||||
base_path, template_pattern = self.split_template_url(resolved)
|
||||
element.attrib["media"] = self.build_proxy_url(
|
||||
base_path, template_pattern, 'media', is_encrypted
|
||||
base_path, template_pattern, "media", is_encrypted
|
||||
)
|
||||
else:
|
||||
element.attrib["media"] = self.build_proxy_url(
|
||||
resolved, None, 'media', is_encrypted
|
||||
resolved, None, "media", is_encrypted
|
||||
)
|
||||
|
||||
# Recurse to child elements
|
||||
@@ -527,4 +539,4 @@ class MPDRewriter:
|
||||
|
||||
total_seconds = int(hours * 3600 + minutes * 60 + seconds)
|
||||
logger.debug(f"Parsed ISO duration '{duration}' to {total_seconds}s")
|
||||
return total_seconds
|
||||
return total_seconds
|
||||
|
||||
@@ -56,7 +56,9 @@ class TimestampConverter:
|
||||
# Create timezone-aware datetime from epoch
|
||||
if as_utc and timezone is None:
|
||||
# Use UTC timezone
|
||||
dt = datetime.datetime.fromtimestamp(epoch_seconds, tz=TimestampConverter.UTC)
|
||||
dt = datetime.datetime.fromtimestamp(
|
||||
epoch_seconds, tz=TimestampConverter.UTC
|
||||
)
|
||||
elif timezone is not None:
|
||||
# Use specified timezone
|
||||
dt = datetime.datetime.fromtimestamp(epoch_seconds, tz=timezone)
|
||||
@@ -126,7 +128,9 @@ class TimestampConverter:
|
||||
return dt.replace(tzinfo=TimestampConverter.UTC).timestamp()
|
||||
|
||||
@staticmethod
|
||||
def _parse_custom_iso(iso_string: str, format_type: Optional[str] = None) -> datetime.datetime:
|
||||
def _parse_custom_iso(
|
||||
iso_string: str, format_type: Optional[str] = None
|
||||
) -> datetime.datetime:
|
||||
"""
|
||||
Parse custom ISO formats not handled by fromisoformat.
|
||||
|
||||
@@ -168,14 +172,18 @@ class TimestampConverter:
|
||||
except ValueError:
|
||||
# Try without microseconds if microseconds format fails
|
||||
if format_type == "microseconds":
|
||||
dt = datetime.datetime.strptime(iso_string, TimestampConverter.ISO_EXTENDED)
|
||||
dt = datetime.datetime.strptime(
|
||||
iso_string, TimestampConverter.ISO_EXTENDED
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
return dt
|
||||
|
||||
@staticmethod
|
||||
def now_iso(format_type: str = "extended", timezone: Optional[datetime.tzinfo] = None) -> str:
|
||||
def now_iso(
|
||||
format_type: str = "extended", timezone: Optional[datetime.tzinfo] = None
|
||||
) -> str:
|
||||
"""
|
||||
Get current time as ISO 8601 string.
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ class VFS:
|
||||
self._explicit_config_dir = config_dir
|
||||
self._env_manager = get_environment_manager()
|
||||
|
||||
logger.debug(f"VFS initialized with config_dir={config_dir}, addon_subdir={addon_subdir}")
|
||||
logger.debug(
|
||||
f"VFS initialized with config_dir={config_dir}, addon_subdir={addon_subdir}"
|
||||
)
|
||||
|
||||
@property
|
||||
def base_path(self) -> str:
|
||||
@@ -52,9 +54,9 @@ class VFS:
|
||||
if self.addon_subdir:
|
||||
if is_kodi_environment():
|
||||
# Kodi uses forward slashes
|
||||
self._base_path = os.path.join(profile_path, self.addon_subdir).replace(
|
||||
"\\", "/"
|
||||
)
|
||||
self._base_path = os.path.join(
|
||||
profile_path, self.addon_subdir
|
||||
).replace("\\", "/")
|
||||
else:
|
||||
# Standard filesystem
|
||||
self._base_path = os.path.join(profile_path, self.addon_subdir)
|
||||
@@ -207,7 +209,9 @@ class VFS:
|
||||
|
||||
with xbmcvfs.File(filepath, "w") as f:
|
||||
bytes_written = f.write(content)
|
||||
logger.debug(f"Kodi file write: {bytes_written} bytes to {filepath}")
|
||||
logger.debug(
|
||||
f"Kodi file write: {bytes_written} bytes to {filepath}"
|
||||
)
|
||||
return bytes_written > 0
|
||||
else:
|
||||
import pathlib
|
||||
@@ -471,12 +475,16 @@ def get_global_vfs(config_dir: Optional[str] = None, addon_subdir: str = "") ->
|
||||
|
||||
|
||||
# Convenience functions that use VFS cache
|
||||
def exists(filepath: str, config_dir: Optional[str] = None, addon_subdir: str = "") -> bool:
|
||||
def exists(
|
||||
filepath: str, config_dir: Optional[str] = None, addon_subdir: str = ""
|
||||
) -> bool:
|
||||
"""Check if file exists"""
|
||||
return get_vfs(config_dir, addon_subdir).exists(filepath)
|
||||
|
||||
|
||||
def mkdirs(dirpath: str, config_dir: Optional[str] = None, addon_subdir: str = "") -> bool:
|
||||
def mkdirs(
|
||||
dirpath: str, config_dir: Optional[str] = None, addon_subdir: str = ""
|
||||
) -> bool:
|
||||
"""Create directories"""
|
||||
return get_vfs(config_dir, addon_subdir).mkdirs(dirpath)
|
||||
|
||||
@@ -520,7 +528,9 @@ def write_json(
|
||||
return get_vfs(config_dir, addon_subdir).write_json(filepath, data, indent)
|
||||
|
||||
|
||||
def delete(filepath: str, config_dir: Optional[str] = None, addon_subdir: str = "") -> bool:
|
||||
def delete(
|
||||
filepath: str, config_dir: Optional[str] = None, addon_subdir: str = ""
|
||||
) -> bool:
|
||||
"""Delete file"""
|
||||
return get_vfs(config_dir, addon_subdir).delete(filepath)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user