mirror of
https://github.com/Viren070/mediaflow-proxy.git
synced 2025-12-01 23:22:12 +01:00
Add Support for Python 3.9 (downgrade support)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -26,7 +26,7 @@ class TransportConfig(BaseSettings):
|
||||
|
||||
def get_mounts(
|
||||
self, async_http: bool = True
|
||||
) -> Dict[str, Optional[httpx.HTTPTransport | httpx.AsyncHTTPTransport]]:
|
||||
) -> Dict[str, Optional[Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport]]]:
|
||||
"""
|
||||
Get a dictionary of httpx mount points to transport instances.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
from typing import Optional, Union
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from collections import namedtuple
|
||||
@@ -17,14 +18,14 @@ class MP4Atom:
|
||||
|
||||
__slots__ = ("atom_type", "size", "data")
|
||||
|
||||
def __init__(self, atom_type: bytes, size: int, data: memoryview | bytearray):
|
||||
def __init__(self, atom_type: bytes, size: int, data: Union[memoryview, bytearray]):
|
||||
"""
|
||||
Initializes an MP4Atom instance.
|
||||
|
||||
Args:
|
||||
atom_type (bytes): The type of the atom.
|
||||
size (int): The size of the atom.
|
||||
data (memoryview | bytearray): The data contained in the atom.
|
||||
data (Union[memoryview, bytearray]): The data contained in the atom.
|
||||
"""
|
||||
self.atom_type = atom_type
|
||||
self.size = size
|
||||
@@ -58,12 +59,12 @@ class MP4Parser:
|
||||
self.data = data
|
||||
self.position = 0
|
||||
|
||||
def read_atom(self) -> MP4Atom | None:
|
||||
def read_atom(self) -> Optional[MP4Atom]:
|
||||
"""
|
||||
Reads the next atom from the data.
|
||||
|
||||
Returns:
|
||||
MP4Atom | None: MP4Atom object or None if no more atoms are available.
|
||||
Optional[MP4Atom]: MP4Atom object or None if no more atoms are available.
|
||||
"""
|
||||
pos = self.position
|
||||
if pos + 8 > len(self.data):
|
||||
@@ -103,7 +104,7 @@ class MP4Parser:
|
||||
self.position = original_position
|
||||
return atoms
|
||||
|
||||
def _read_atom_at(self, pos: int, end: int) -> MP4Atom | None:
|
||||
def _read_atom_at(self, pos: int, end: int) -> Optional[MP4Atom]:
|
||||
if pos + 8 > end:
|
||||
return None
|
||||
|
||||
@@ -169,7 +170,7 @@ class MP4Decrypter:
|
||||
|
||||
Attributes:
|
||||
key_map (dict[bytes, bytes]): Mapping of track IDs to decryption keys.
|
||||
current_key (bytes | None): Current decryption key.
|
||||
current_key (Optional[bytes]): Current decryption key.
|
||||
trun_sample_sizes (array.array): Array of sample sizes from the 'trun' box.
|
||||
current_sample_info (list): List of sample information from the 'senc' box.
|
||||
encryption_overhead (int): Total size of encryption-related boxes.
|
||||
@@ -230,17 +231,16 @@ class MP4Decrypter:
|
||||
Returns:
|
||||
MP4Atom: Processed atom.
|
||||
"""
|
||||
match atom_type:
|
||||
case b"moov":
|
||||
return self._process_moov(atom)
|
||||
case b"moof":
|
||||
return self._process_moof(atom)
|
||||
case b"sidx":
|
||||
return self._process_sidx(atom)
|
||||
case b"mdat":
|
||||
return self._decrypt_mdat(atom)
|
||||
case _:
|
||||
return atom
|
||||
if atom_type == b"moov":
|
||||
return self._process_moov(atom)
|
||||
elif atom_type == b"moof":
|
||||
return self._process_moof(atom)
|
||||
elif atom_type == b"sidx":
|
||||
return self._process_sidx(atom)
|
||||
elif atom_type == b"mdat":
|
||||
return self._decrypt_mdat(atom)
|
||||
else:
|
||||
return atom
|
||||
|
||||
def _process_moov(self, moov: MP4Atom) -> MP4Atom:
|
||||
"""
|
||||
@@ -428,7 +428,7 @@ class MP4Decrypter:
|
||||
@staticmethod
|
||||
def _process_sample(
|
||||
sample: memoryview, sample_info: CENCSampleAuxiliaryDataFormat, key: bytes
|
||||
) -> memoryview | bytearray | bytes:
|
||||
) -> Union[memoryview, bytearray, bytes]:
|
||||
"""
|
||||
Processes and decrypts a sample using the provided sample information and decryption key.
|
||||
This includes handling sub-sample encryption if present.
|
||||
@@ -439,7 +439,7 @@ class MP4Decrypter:
|
||||
key (bytes): The decryption key.
|
||||
|
||||
Returns:
|
||||
memoryview | bytearray | bytes: The decrypted sample.
|
||||
Union[memoryview, bytearray, bytes]: The decrypted sample.
|
||||
"""
|
||||
if not sample_info.is_encrypted:
|
||||
return sample
|
||||
@@ -701,7 +701,7 @@ class MP4Decrypter:
|
||||
new_type = codec_format if codec_format else entry.atom_type
|
||||
return MP4Atom(new_type, len(new_entry_data) + 8, new_entry_data)
|
||||
|
||||
def _extract_codec_format(self, sinf: MP4Atom) -> bytes | None:
|
||||
def _extract_codec_format(self, sinf: MP4Atom) -> Optional[bytes]:
|
||||
"""
|
||||
Extracts the codec format from the 'sinf' (Protection Scheme Information) atom.
|
||||
This includes information about the original format of the protected content.
|
||||
@@ -710,7 +710,7 @@ class MP4Decrypter:
|
||||
sinf (MP4Atom): The 'sinf' atom to extract from.
|
||||
|
||||
Returns:
|
||||
bytes | None: The codec format or None if not found.
|
||||
Optional[bytes]: The codec format or None if not found.
|
||||
"""
|
||||
parser = MP4Parser(sinf.data)
|
||||
for atom in iter(parser.read_atom, None):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from typing import Dict, Tuple
|
||||
from typing import Dict, Tuple, Optional
|
||||
from urllib.parse import urljoin, urlparse, unquote
|
||||
|
||||
from httpx import Response
|
||||
@@ -92,7 +92,7 @@ class LiveTVExtractor(BaseExtractor):
|
||||
except Exception as e:
|
||||
raise ExtractorError(f"Extraction failed: {str(e)}")
|
||||
|
||||
async def _extract_player_api_base(self, html_content: str) -> Tuple[str | None, str | None]:
|
||||
async def _extract_player_api_base(self, html_content: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Extract player API base URL and method."""
|
||||
admin_ajax_pattern = r'"player_api"\s*:\s*"([^"]+)".*?"play_method"\s*:\s*"([^"]+)"'
|
||||
match = re.search(admin_ajax_pattern, html_content)
|
||||
|
||||
+20
-16
@@ -1,24 +1,28 @@
|
||||
from typing import Literal, Dict, Any
|
||||
from typing import Literal, Dict, Any, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, IPvAnyAddress, ConfigDict
|
||||
|
||||
|
||||
class GenerateUrlRequest(BaseModel):
|
||||
mediaflow_proxy_url: str = Field(..., description="The base URL for the mediaflow proxy.")
|
||||
endpoint: str | None = Field(None, description="The specific endpoint to be appended to the base URL.")
|
||||
destination_url: str | None = Field(None, description="The destination URL to which the request will be proxied.")
|
||||
query_params: dict | None = Field(
|
||||
endpoint: Optional[str] = Field(None, description="The specific endpoint to be appended to the base URL.")
|
||||
destination_url: Optional[str] = Field(
|
||||
None, description="The destination URL to which the request will be proxied."
|
||||
)
|
||||
query_params: Optional[dict] = Field(
|
||||
default_factory=dict, description="Query parameters to be included in the request."
|
||||
)
|
||||
request_headers: dict | None = Field(default_factory=dict, description="Headers to be included in the request.")
|
||||
response_headers: dict | None = Field(default_factory=dict, description="Headers to be included in the response.")
|
||||
expiration: int | None = Field(
|
||||
request_headers: Optional[dict] = Field(default_factory=dict, description="Headers to be included in the request.")
|
||||
response_headers: Optional[dict] = Field(
|
||||
default_factory=dict, description="Headers to be included in the response."
|
||||
)
|
||||
expiration: Optional[int] = Field(
|
||||
None, description="Expiration time for the URL in seconds. If not provided, the URL will not expire."
|
||||
)
|
||||
api_password: str | None = Field(
|
||||
api_password: Optional[str] = Field(
|
||||
None, description="API password for encryption. If not provided, the URL will only be encoded."
|
||||
)
|
||||
ip: IPvAnyAddress | None = Field(None, description="The IP address to restrict the URL to.")
|
||||
ip: Optional[IPvAnyAddress] = Field(None, description="The IP address to restrict the URL to.")
|
||||
|
||||
|
||||
class GenericParams(BaseModel):
|
||||
@@ -27,7 +31,7 @@ class GenericParams(BaseModel):
|
||||
|
||||
class HLSManifestParams(GenericParams):
|
||||
destination: str = Field(..., description="The URL of the HLS manifest.", alias="d")
|
||||
key_url: str | None = Field(
|
||||
key_url: Optional[str] = Field(
|
||||
None,
|
||||
description="The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)",
|
||||
)
|
||||
@@ -39,23 +43,23 @@ class ProxyStreamParams(GenericParams):
|
||||
|
||||
class MPDManifestParams(GenericParams):
|
||||
destination: str = Field(..., description="The URL of the MPD manifest.", alias="d")
|
||||
key_id: str | None = Field(None, description="The DRM key ID (optional).")
|
||||
key: str | None = Field(None, description="The DRM key (optional).")
|
||||
key_id: Optional[str] = Field(None, description="The DRM key ID (optional).")
|
||||
key: Optional[str] = Field(None, description="The DRM key (optional).")
|
||||
|
||||
|
||||
class MPDPlaylistParams(GenericParams):
|
||||
destination: str = Field(..., description="The URL of the MPD manifest.", alias="d")
|
||||
profile_id: str = Field(..., description="The profile ID to generate the playlist for.")
|
||||
key_id: str | None = Field(None, description="The DRM key ID (optional).")
|
||||
key: str | None = Field(None, description="The DRM key (optional).")
|
||||
key_id: Optional[str] = Field(None, description="The DRM key ID (optional).")
|
||||
key: Optional[str] = Field(None, description="The DRM key (optional).")
|
||||
|
||||
|
||||
class MPDSegmentParams(GenericParams):
|
||||
init_url: str = Field(..., description="The URL of the initialization segment.")
|
||||
segment_url: str = Field(..., description="The URL of the media segment.")
|
||||
mime_type: str = Field(..., description="The MIME type of the segment.")
|
||||
key_id: str | None = Field(None, description="The DRM key ID (optional).")
|
||||
key: str | None = Field(None, description="The DRM key (optional).")
|
||||
key_id: Optional[str] = Field(None, description="The DRM key ID (optional).")
|
||||
key: Optional[str] = Field(None, description="The DRM key (optional).")
|
||||
|
||||
|
||||
class ExtractorURLParams(GenericParams):
|
||||
|
||||
@@ -341,7 +341,7 @@ async def get_cached_mpd(
|
||||
mpd_url: str,
|
||||
headers: dict,
|
||||
parse_drm: bool,
|
||||
parse_segment_profile_id: str | None = None,
|
||||
parse_segment_profile_id: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Get MPD from cache or download and parse it."""
|
||||
# Try cache first
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
@@ -89,7 +90,7 @@ class EncryptionMiddleware(BaseHTTPMiddleware):
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def get_client_ip(request: Request) -> str | None:
|
||||
def get_client_ip(request: Request) -> Optional[str]:
|
||||
"""
|
||||
Extract the client's real IP address from the request headers or fallback to the client host.
|
||||
"""
|
||||
|
||||
@@ -250,11 +250,11 @@ async def request_with_retry(method: str, url: str, headers: dict, **kwargs) ->
|
||||
|
||||
def encode_mediaflow_proxy_url(
|
||||
mediaflow_proxy_url: str,
|
||||
endpoint: str | None = None,
|
||||
destination_url: str | None = None,
|
||||
query_params: dict | None = None,
|
||||
request_headers: dict | None = None,
|
||||
response_headers: dict | None = None,
|
||||
endpoint: typing.Optional[str] = None,
|
||||
destination_url: typing.Optional[str] = None,
|
||||
query_params: typing.Optional[dict] = None,
|
||||
request_headers: typing.Optional[dict] = None,
|
||||
response_headers: typing.Optional[dict] = None,
|
||||
encryption_handler: EncryptionHandler = None,
|
||||
expiration: int = None,
|
||||
ip: str = None,
|
||||
@@ -416,10 +416,6 @@ class EnhancedStreamingResponse(Response):
|
||||
async def wrap(func: typing.Callable[[], typing.Awaitable[None]]) -> None:
|
||||
try:
|
||||
await func()
|
||||
except ExceptionGroup as e:
|
||||
if not any(isinstance(exc, anyio.get_cancelled_exc_class()) for exc in e.exceptions):
|
||||
logger.exception("Error in streaming task")
|
||||
raise
|
||||
except Exception as e:
|
||||
if not isinstance(e, anyio.get_cancelled_exc_class()):
|
||||
logger.exception("Error in streaming task")
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import math
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Dict
|
||||
from typing import List, Dict, Optional, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import xmltodict
|
||||
@@ -10,12 +10,12 @@ import xmltodict
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_mpd(mpd_content: str | bytes) -> dict:
|
||||
def parse_mpd(mpd_content: Union[str, bytes]) -> dict:
|
||||
"""
|
||||
Parses the MPD content into a dictionary.
|
||||
|
||||
Args:
|
||||
mpd_content (str | bytes): The MPD content to parse.
|
||||
mpd_content (Union[str, bytes]): The MPD content to parse.
|
||||
|
||||
Returns:
|
||||
dict: The parsed MPD content as a dictionary.
|
||||
@@ -24,7 +24,7 @@ def parse_mpd(mpd_content: str | bytes) -> dict:
|
||||
|
||||
|
||||
def parse_mpd_dict(
|
||||
mpd_dict: dict, mpd_url: str, parse_drm: bool = True, parse_segment_profile_id: str | None = None
|
||||
mpd_dict: dict, mpd_url: str, parse_drm: bool = True, parse_segment_profile_id: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Parses the MPD dictionary and extracts relevant information.
|
||||
@@ -122,7 +122,7 @@ def extract_drm_info(periods: List[Dict], mpd_url: str) -> Dict:
|
||||
drm_info = {"isDrmProtected": False}
|
||||
|
||||
for period in periods:
|
||||
adaptation_sets: list[dict] | dict = period.get("AdaptationSet", [])
|
||||
adaptation_sets: Union[list[dict], dict] = period.get("AdaptationSet", [])
|
||||
if not isinstance(adaptation_sets, list):
|
||||
adaptation_sets = [adaptation_sets]
|
||||
|
||||
@@ -131,7 +131,7 @@ def extract_drm_info(periods: List[Dict], mpd_url: str) -> Dict:
|
||||
process_content_protection(adaptation_set.get("ContentProtection", []), drm_info)
|
||||
|
||||
# Check ContentProtection inside each Representation
|
||||
representations: list[dict] | dict = adaptation_set.get("Representation", [])
|
||||
representations: Union[list[dict], dict] = adaptation_set.get("Representation", [])
|
||||
if not isinstance(representations, list):
|
||||
representations = [representations]
|
||||
|
||||
@@ -145,12 +145,12 @@ def extract_drm_info(periods: List[Dict], mpd_url: str) -> Dict:
|
||||
return drm_info
|
||||
|
||||
|
||||
def process_content_protection(content_protection: list[dict] | dict, drm_info: dict):
|
||||
def process_content_protection(content_protection: Union[list[dict], dict], drm_info: dict):
|
||||
"""
|
||||
Processes the ContentProtection elements to extract DRM information.
|
||||
|
||||
Args:
|
||||
content_protection (list[dict] | dict): The ContentProtection elements.
|
||||
content_protection (Union[list[dict], dict]): The ContentProtection elements.
|
||||
drm_info (dict): The dictionary to store DRM information.
|
||||
|
||||
This function updates the drm_info dictionary with DRM system information found in the ContentProtection elements.
|
||||
@@ -197,8 +197,8 @@ def parse_representation(
|
||||
adaptation: dict,
|
||||
source: str,
|
||||
media_presentation_duration: str,
|
||||
parse_segment_profile_id: str | None,
|
||||
) -> dict | None:
|
||||
parse_segment_profile_id: Optional[str],
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Parses a representation and extracts profile information.
|
||||
|
||||
@@ -211,7 +211,7 @@ def parse_representation(
|
||||
parse_segment_profile_id (str, optional): The profile ID to parse segments for. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict | None: The parsed profile information or None if not applicable.
|
||||
Optional[dict]: The parsed profile information or None if not applicable.
|
||||
"""
|
||||
mime_type = _get_key(adaptation, representation, "@mimeType") or (
|
||||
"video/mp4" if "avc" in representation["@codecs"] else "audio/mp4"
|
||||
@@ -252,7 +252,7 @@ def parse_representation(
|
||||
return profile
|
||||
|
||||
|
||||
def _get_key(adaptation: dict, representation: dict, key: str) -> str | None:
|
||||
def _get_key(adaptation: dict, representation: dict, key: str) -> Optional[str]:
|
||||
"""
|
||||
Retrieves a key from the representation or adaptation set.
|
||||
|
||||
@@ -262,7 +262,7 @@ def _get_key(adaptation: dict, representation: dict, key: str) -> str | None:
|
||||
key (str): The key to retrieve.
|
||||
|
||||
Returns:
|
||||
str | None: The value of the key or None if not found.
|
||||
Optional[str]: The value of the key or None if not found.
|
||||
"""
|
||||
return representation.get(key, adaptation.get(key, None))
|
||||
|
||||
@@ -454,7 +454,7 @@ def generate_vod_segments(profile: dict, duration: int, timescale: int, start_nu
|
||||
return [{"number": start_number + i, "duration": duration / timescale} for i in range(segment_count)]
|
||||
|
||||
|
||||
def create_segment_data(segment: Dict, item: dict, profile: dict, source: str, timescale: int | None = None) -> Dict:
|
||||
def create_segment_data(segment: Dict, item: dict, profile: dict, source: str, timescale: Optional[int] = None) -> Dict:
|
||||
"""
|
||||
Creates segment data based on the segment information. This includes the segment URL and metadata.
|
||||
|
||||
|
||||
Generated
+3
-2
@@ -634,6 +634,7 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.4.0,<5"
|
||||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||
@@ -870,5 +871,5 @@ cffi = ["cffi (>=1.11)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10"
|
||||
content-hash = "aed582c8fad0d90ed52a4d1965ea0e97a1ca138f6648f8e7abe8f530a1351274"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "34e26a9555c523a586448f1dc66e6898c5b02efcb3c6ad07e85d73381aa9f024"
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ classifiers = [
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
@@ -22,7 +23,7 @@ include = ["LICENSE", "README.md", "mediaflow_proxy/static/*"]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10"
|
||||
python = ">=3.9"
|
||||
fastapi = "0.115.6"
|
||||
httpx = {extras = ["socks", "zstd"], version = "^0.28.1"}
|
||||
tenacity = "^9.0.0"
|
||||
|
||||
Reference in New Issue
Block a user