Compare commits

..

2 Commits

Author SHA1 Message Date
UrloMythus
1bdfe198b5 Update to the newest MFP version, including the Vixcloud PR 2025-05-12 19:41:22 +02:00
UrloMythus
2501ff4fd6 Update to the newest MFP version, including the Vixcloud PR 2025-05-12 19:40:35 +02:00
7 changed files with 61 additions and 37 deletions

View File

@@ -15,18 +15,18 @@ class VixCloudExtractor(BaseExtractor):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.mediaflow_endpoint = "hls_manifest_proxy" self.mediaflow_endpoint = "hls_manifest_proxy"
async def version(self, domain: str) -> str: async def version(self, site_url: str) -> str:
"""Get version of VixCloud Parent Site.""" """Get version of VixCloud Parent Site."""
base_url = f"https://streamingcommunity.{domain}/richiedi-un-titolo" base_url = f"{site_url}/richiedi-un-titolo"
response = await self._make_request( response = await self._make_request(
base_url, base_url,
headers={ headers={
"Referer": f"https://streamingcommunity.{domain}/", "Referer": f"{site_url}/",
"Origin": f"https://streamingcommunity.{domain}", "Origin": f"{site_url}",
}, },
) )
if response.status_code != 200: if response.status_code != 200:
raise ExtractorError("Outdated Domain") raise ExtractorError("Outdated Url")
# Soup the response # Soup the response
soup = BeautifulSoup(response.text, "lxml", parse_only=SoupStrainer("div", {"id": "app"})) soup = BeautifulSoup(response.text, "lxml", parse_only=SoupStrainer("div", {"id": "app"}))
if soup: if soup:
@@ -39,8 +39,8 @@ class VixCloudExtractor(BaseExtractor):
async def extract(self, url: str, **kwargs) -> Dict[str, Any]: async def extract(self, url: str, **kwargs) -> Dict[str, Any]:
"""Extract Vixcloud URL.""" """Extract Vixcloud URL."""
domain = url.split("://")[1].split("/")[0].split(".")[1] site_url = url.split("/iframe")[0]
version = await self.version(domain) version = await self.version(site_url)
response = await self._make_request(url, headers={"x-inertia": "true", "x-inertia-version": version}) response = await self._make_request(url, headers={"x-inertia": "true", "x-inertia-version": version})
soup = BeautifulSoup(response.text, "lxml", parse_only=SoupStrainer("iframe")) soup = BeautifulSoup(response.text, "lxml", parse_only=SoupStrainer("iframe"))
iframe = soup.find("iframe").get("src") iframe = soup.find("iframe").get("src")

View File

@@ -67,30 +67,24 @@ class EncryptionMiddleware(BaseHTTPMiddleware):
encrypted_token = None encrypted_token = None
# Check for token in path # Check for token in path
if token_marker in path and self.encryption_handler: if path.startswith(token_marker) and self.encryption_handler:
try: try:
# Extract token from path # Extract token from the beginning of the path
token_start = path.find(token_marker) + len(token_marker) token_start = len(token_marker)
token_end = path.find("/", token_start) token_end = path.find("/", token_start)
if token_end == -1: # No trailing slash (no filename after token) if token_end == -1: # No trailing slash
token_end = len(path) encrypted_token = path[token_start:]
filename_part = "" remaining_path = ""
else: else:
# There's something after the token (likely a filename)
filename_part = path[token_end:]
# Get the encrypted token
encrypted_token = path[token_start:token_end] encrypted_token = path[token_start:token_end]
remaining_path = path[token_end:]
# Modify the path to remove the token part but preserve the filename # Modify the path to remove the token part
original_path = path[: path.find(token_marker)] request.scope["path"] = remaining_path
original_path += filename_part # Add back the filename part
request.scope["path"] = original_path
# Update the raw path as well # Update the raw path as well
request.scope["raw_path"] = original_path.encode() request.scope["raw_path"] = remaining_path.encode()
except Exception as e: except Exception as e:
logging.error(f"Error processing token in path: {str(e)}") logging.error(f"Error processing token in path: {str(e)}")

View File

@@ -330,15 +330,25 @@ def encode_mediaflow_proxy_url(
# Handle encryption if needed # Handle encryption if needed
if encryption_handler: if encryption_handler:
encrypted_token = encryption_handler.encrypt_data(query_params, expiration, ip) encrypted_token = encryption_handler.encrypt_data(query_params, expiration, ip)
# Build the URL with token in path
path_parts = [base_url, f"_token_{encrypted_token}"] # Parse the base URL to get its components
parsed_url = parse.urlparse(base_url)
# Insert the token at the beginning of the path
new_path = f"/_token_{encrypted_token}{parsed_url.path}"
# Reconstruct the URL with the token at the beginning of the path
url_parts = list(parsed_url)
url_parts[2] = new_path # Update the path component
# Build the URL
url = parse.urlunparse(url_parts)
# Add filename at the end if provided # Add filename at the end if provided
if filename: if filename:
path_parts.append(parse.quote(filename)) url = f"{url}/{parse.quote(filename)}"
return "/".join(path_parts)
return url
else: else:
# No encryption, use regular query parameters # No encryption, use regular query parameters
url = base_url url = base_url

View File

@@ -233,8 +233,21 @@ def parse_representation(
profile["audioSamplingRate"] = representation.get("@audioSamplingRate") or adaptation.get("@audioSamplingRate") profile["audioSamplingRate"] = representation.get("@audioSamplingRate") or adaptation.get("@audioSamplingRate")
profile["channels"] = representation.get("AudioChannelConfiguration", {}).get("@value", "2") profile["channels"] = representation.get("AudioChannelConfiguration", {}).get("@value", "2")
else: else:
# Handle video-specific attributes, making them optional with sensible defaults
if "@width" in representation:
profile["width"] = int(representation["@width"]) profile["width"] = int(representation["@width"])
elif "@width" in adaptation:
profile["width"] = int(adaptation["@width"])
else:
profile["width"] = 0 # Default if width is missing
if "@height" in representation:
profile["height"] = int(representation["@height"]) profile["height"] = int(representation["@height"])
elif "@height" in adaptation:
profile["height"] = int(adaptation["@height"])
else:
profile["height"] = 0 # Default if height is missing
frame_rate = representation.get("@frameRate") or adaptation.get("@maxFrameRate") or "30000/1001" frame_rate = representation.get("@frameRate") or adaptation.get("@maxFrameRate") or "30000/1001"
frame_rate = frame_rate if "/" in frame_rate else f"{frame_rate}/1" frame_rate = frame_rate if "/" in frame_rate else f"{frame_rate}/1"
profile["frameRate"] = round(int(frame_rate.split("/")[0]) / int(frame_rate.split("/")[1]), 3) profile["frameRate"] = round(int(frame_rate.split("/")[0]) / int(frame_rate.split("/")[1]), 3)
@@ -317,7 +330,9 @@ def parse_segment_timeline(parsed_dict: dict, item: dict, profile: dict, source:
""" """
timelines = item["SegmentTimeline"]["S"] timelines = item["SegmentTimeline"]["S"]
timelines = timelines if isinstance(timelines, list) else [timelines] timelines = timelines if isinstance(timelines, list) else [timelines]
period_start = parsed_dict.get("availabilityStartTime", datetime.fromtimestamp(0, tz=timezone.utc)) + timedelta(seconds=parsed_dict.get("PeriodStart", 0)) period_start = parsed_dict.get("availabilityStartTime", datetime.fromtimestamp(0, tz=timezone.utc)) + timedelta(
seconds=parsed_dict.get("PeriodStart", 0)
)
presentation_time_offset = int(item.get("@presentationTimeOffset", 0)) presentation_time_offset = int(item.get("@presentationTimeOffset", 0))
start_number = int(item.get("@startNumber", 1)) start_number = int(item.get("@startNumber", 1))
@@ -354,13 +369,14 @@ def preprocess_timeline(
for _ in range(repeat + 1): for _ in range(repeat + 1):
segment_start_time = period_start + timedelta(seconds=(start_time - presentation_time_offset) / timescale) segment_start_time = period_start + timedelta(seconds=(start_time - presentation_time_offset) / timescale)
segment_end_time = segment_start_time + timedelta(seconds=duration / timescale) segment_end_time = segment_start_time + timedelta(seconds=duration / timescale)
presentation_time = start_time - presentation_time_offset
processed_data.append( processed_data.append(
{ {
"number": start_number, "number": start_number,
"start_time": segment_start_time, "start_time": segment_start_time,
"end_time": segment_end_time, "end_time": segment_end_time,
"duration": duration, "duration": duration,
"time": start_time, "time": presentation_time,
} }
) )
start_time += duration start_time += duration
@@ -475,7 +491,7 @@ def create_segment_data(segment: Dict, item: dict, profile: dict, source: str, t
media = media.replace("$Bandwidth$", str(profile["bandwidth"])) media = media.replace("$Bandwidth$", str(profile["bandwidth"]))
if "time" in segment and timescale is not None: if "time" in segment and timescale is not None:
media = media.replace("$Time$", str(int(segment["time"] * timescale))) media = media.replace("$Time$", str(int(segment["time"])))
if not media.startswith("http"): if not media.startswith("http"):
media = f"{source}/{media}" media = f"{source}/{media}"
@@ -496,16 +512,20 @@ def create_segment_data(segment: Dict, item: dict, profile: dict, source: str, t
} }
) )
elif "start_time" in segment and "duration" in segment: elif "start_time" in segment and "duration" in segment:
duration = segment["duration"] duration_seconds = segment["duration"] / timescale
segment_data.update( segment_data.update(
{ {
"start_time": segment["start_time"], "start_time": segment["start_time"],
"end_time": segment["start_time"] + timedelta(seconds=duration), "end_time": segment["start_time"] + timedelta(seconds=duration_seconds),
"extinf": duration, "extinf": duration_seconds,
"program_date_time": segment["start_time"].isoformat() + "Z", "program_date_time": segment["start_time"].isoformat() + "Z",
} }
) )
elif "duration" in segment and timescale is not None:
# Convert duration from timescale units to seconds
segment_data["extinf"] = segment["duration"] / timescale
elif "duration" in segment: elif "duration" in segment:
# If no timescale is provided, assume duration is already in seconds
segment_data["extinf"] = segment["duration"] segment_data["extinf"] = segment["duration"]
return segment_data return segment_data