Add Support for Python 3.9 (downgrade support)

This commit is contained in:
mhdzumair
2024-12-07 08:05:10 +05:30
parent ecb63f15f3
commit 78cee28128
10 changed files with 72 additions and 69 deletions
+2 -2
View File
@@ -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.
"""
+21 -21
View File
@@ -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):
+2 -2
View File
@@ -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
View File
@@ -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):
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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.
"""
+5 -9
View File
@@ -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")
+14 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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"