This commit is contained in:
VineFeeder
2026-01-04 14:32:23 +00:00
parent f9982f97cd
commit 2a9dee4f3b
84 changed files with 0 additions and 15174 deletions
@@ -1,419 +0,0 @@
from __future__ import annotations
import base64
import hashlib
import json
import re
import sys
from collections.abc import Generator
from datetime import datetime, timezone
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
import click
from click import Context
from Crypto.Util.Padding import unpad
from Cryptodome.Cipher import AES
from pywidevine.cdm import Cdm as WidevineCdm
from envied.core.credential import Credential
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Subtitle, Tracks
class ALL4(Service):
"""
Service code for Channel 4's All4 streaming service (https://channel4.com).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: Credentials
Robustness:
L3: 1080p, AAC2.0
\b
Tips:
- Use complete title URL or slug as input:
https://www.channel4.com/programmes/taskmaster OR taskmaster
- Use on demand URL for directly downloading episodes:
https://www.channel4.com/programmes/taskmaster/on-demand/75588-002
- Both android and web/pc endpoints are checked for quality profiles.
If android is missing 1080p, it automatically falls back to web.
"""
GEOFENCE = ("gb", "ie")
TITLE_RE = r"^(?:https?://(?:www\.)?channel4\.com/programmes/)?(?P<id>[a-z0-9-]+)(?:/on-demand/(?P<vid>[0-9-]+))?"
@staticmethod
@click.command(name="ALL4", short_help="https://channel4.com", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> ALL4:
return ALL4(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.authorization: str
self.asset_id: int
self.license_token: str
self.manifest: str
self.session.headers.update(
{
"X-C4-Platform-Name": self.config["device"]["platform_name"],
"X-C4-Device-Type": self.config["device"]["device_type"],
"X-C4-Device-Name": self.config["device"]["device_name"],
"X-C4-App-Version": self.config["device"]["app_version"],
"X-C4-Optimizely-Datafile": self.config["device"]["optimizely_datafile"],
}
)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
cache = self.cache.get(f"tokens_{credential.sha1}")
if cache and not cache.expired:
# cached
self.log.info(" + Using cached Tokens...")
tokens = cache.data
elif cache and cache.expired:
# expired, refresh
self.log.info("Refreshing cached Tokens")
r = self.session.post(
self.config["endpoints"]["login"],
headers={"authorization": f"Basic {self.config['android']['auth']}"},
data={
"grant_type": "refresh_token",
"username": credential.username,
"password": credential.password,
"refresh_token": cache.data["refreshToken"],
},
)
try:
res = r.json()
except json.JSONDecodeError:
raise ValueError(f"Failed to refresh tokens: {r.text}")
if "error" in res:
self.log.error(f"Failed to refresh tokens: {res['errorMessage']}")
sys.exit(1)
tokens = res
self.log.info(" + Refreshed")
else:
# new
headers = {"authorization": f"Basic {self.config['android']['auth']}"}
data = {
"grant_type": "password",
"username": credential.username,
"password": credential.password,
}
r = self.session.post(self.config["endpoints"]["login"], headers=headers, data=data)
try:
res = r.json()
except json.JSONDecodeError:
raise ValueError(f"Failed to log in: {r.text}")
if "error" in res:
self.log.error(f"Failed to log in: {res['errorMessage']}")
sys.exit(1)
tokens = res
self.log.info(" + Acquired tokens...")
cache.set(tokens, expiration=tokens["expiresIn"])
self.authorization = f"Bearer {tokens['accessToken']}"
def search(self) -> Generator[SearchResult, None, None]:
params = {
"expand": "default",
"q": self.title,
"limit": "100",
"offset": "0",
}
r = self.session.get(self.config["endpoints"]["search"], params=params)
r.raise_for_status()
results = r.json()
if isinstance(results["results"], list):
for result in results["results"]:
yield SearchResult(
id_=result["brand"].get("websafeTitle"),
title=result["brand"].get("title"),
description=result["brand"].get("description"),
label=result.get("label"),
url=result["brand"].get("href"),
)
def get_titles(self) -> Union[Movies, Series]:
title, on_demand = (re.match(self.TITLE_RE, self.title).group(i) for i in ("id", "vid"))
r = self.session.get(
self.config["endpoints"]["title"].format(title=title),
params={"client": "android-mod", "deviceGroup": "mobile", "include": "extended-restart"},
headers={"Authorization": self.authorization},
)
if not r.ok:
self.log.error(r.text)
sys.exit(1)
data = r.json()
if on_demand is not None:
episodes = [
Episode(
id_=episode["programmeId"],
service=self.__class__,
title=data["brand"]["title"],
season=episode["seriesNumber"],
number=episode["episodeNumber"],
name=episode["originalTitle"],
language="en",
data=episode["assetInfo"].get("streaming") or episode["assetInfo"].get("download"),
)
for episode in data["brand"]["episodes"]
if episode.get("assetInfo") and episode["programmeId"] == on_demand
]
if not episodes:
# Parse HTML of episode page to find title
data = self.get_html(self.title)
episodes = [
Episode(
id_=data["selectedEpisode"]["programmeId"],
service=self.__class__,
title=data["brand"]["title"],
season=data["selectedEpisode"]["seriesNumber"] or 0,
number=data["selectedEpisode"]["episodeNumber"] or 0,
name=data["selectedEpisode"]["originalTitle"],
language="en",
data=data["selectedEpisode"],
)
]
return Series(episodes)
elif data["brand"]["programmeType"] == "FM":
return Movies(
[
Movie(
id_=movie["programmeId"],
service=self.__class__,
name=data["brand"]["title"],
year=int(data["brand"]["summary"].split(" ")[0].strip().strip("()")),
language="en",
data=movie["assetInfo"].get("streaming") or movie["assetInfo"].get("download"),
)
for movie in data["brand"]["episodes"]
]
)
else:
return Series(
[
Episode(
id_=episode["programmeId"],
service=self.__class__,
title=data["brand"]["title"],
season=episode["seriesNumber"],
number=episode["episodeNumber"],
name=episode["originalTitle"],
language="en",
data=episode["assetInfo"].get("streaming") or episode["assetInfo"].get("download"),
)
for episode in data["brand"]["episodes"]
if episode.get("assetInfo")
]
)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
android_assets: tuple = self.android_playlist(title.id)
web_assets: tuple = self.web_playlist(title.id)
self.manifest, self.license_token, subtitle, data = self.sort_assets(title, android_assets, web_assets)
self.asset_id = int(title.data["assetId"])
tracks = DASH.from_url(self.manifest, self.session).to_tracks(title.language)
tracks.videos[0].data = data
# manifest subtitles are sometimes empty even if they exist
# so we clear them and add the subtitles manually
tracks.subtitles.clear()
if subtitle is not None:
tracks.add(
Subtitle(
id_=hashlib.md5(subtitle.encode()).hexdigest()[0:6],
url=subtitle,
codec=Subtitle.Codec.from_mime(subtitle[-3:]),
language=title.language,
is_original_lang=True,
forced=False,
sdh=False,
)
)
else:
self.log.warning("- Subtitles are either missing or empty")
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> list[Chapter]:
track = title.tracks.videos[0]
chapters = [
Chapter(
name=f"Chapter {i + 1:02}",
timestamp=datetime.fromtimestamp((ms / 1000), tz=timezone.utc).strftime("%H:%M:%S.%f")[:-3],
)
for i, ms in enumerate(x["breakOffset"] for x in track.data["adverts"]["breaks"])
]
if track.data.get("endCredits", {}).get("squeezeIn"):
chapters.append(
Chapter(
name="Credits",
timestamp=datetime.fromtimestamp(
(track.data["endCredits"]["squeezeIn"] / 1000), tz=timezone.utc
).strftime("%H:%M:%S.%f")[:-3],
)
)
return chapters
def get_widevine_service_certificate(self, **_: Any) -> str:
return WidevineCdm.common_privacy_cert
def get_widevine_license(self, challenge: bytes, **_: Any) -> str:
payload = {
"message": base64.b64encode(challenge).decode("utf8"),
"token": self.license_token,
"request_id": self.asset_id,
"video": {"type": "ondemand", "url": self.manifest},
}
r = self.session.post(self.config["endpoints"]["license"], json=payload)
if not r.ok:
raise ConnectionError(f"License request failed: {r.json()['status']['type']}")
return r.json()["license"]
# Service specific functions
def sort_assets(self, title: Union[Movie, Episode], android_assets: tuple, web_assets: tuple) -> tuple:
android_heights = None
web_heights = None
if android_assets is not None:
try:
a_manifest, a_token, a_subtitle, data = android_assets
android_tracks = DASH.from_url(a_manifest, self.session).to_tracks(title.language)
android_heights = sorted([int(track.height) for track in android_tracks.videos], reverse=True)
except Exception:
android_heights = None
if web_assets is not None:
try:
b_manifest, b_token, b_subtitle, data = web_assets
session = self.session
session.headers.update(self.config["headers"])
web_tracks = DASH.from_url(b_manifest, session).to_tracks(title.language)
web_heights = sorted([int(track.height) for track in web_tracks.videos], reverse=True)
except Exception:
web_heights = None
if not android_heights and not web_heights:
self.log.error("Failed to request manifest data. If you're behind a VPN/proxy, you might be blocked")
sys.exit(1)
if not android_heights or android_heights[0] < 1080:
lic_token = self.decrypt_token(b_token, client="WEB")
return b_manifest, lic_token, b_subtitle, data
else:
lic_token = self.decrypt_token(a_token, client="ANDROID")
return a_manifest, lic_token, a_subtitle, data
def android_playlist(self, video_id: str) -> tuple:
url = self.config["android"]["vod"].format(video_id=video_id)
headers = {"authorization": self.authorization}
r = self.session.get(url=url, headers=headers)
if not r.ok:
self.log.warning("Request for Android endpoint returned %s", r)
return None
data = json.loads(r.content)
manifest = data["videoProfiles"][0]["streams"][0]["uri"]
token = data["videoProfiles"][0]["streams"][0]["token"]
subtitle = next(
(x["url"] for x in data["subtitlesAssets"] if x["url"].endswith(".vtt")),
None,
)
return manifest, token, subtitle, data
def web_playlist(self, video_id: str) -> tuple:
url = self.config["web"]["vod"].format(programmeId=video_id)
r = self.session.get(url, headers=self.config["headers"])
if not r.ok:
self.log.warning("Request for WEB endpoint returned %s", r)
return None
data = json.loads(r.content)
for item in data["videoProfiles"]:
if item["name"] == "dashwv-dyn-stream-1":
token = item["streams"][0]["token"]
manifest = item["streams"][0]["uri"]
subtitle = next(
(x["url"] for x in data["subtitlesAssets"] if x["url"].endswith(".vtt")),
None,
)
return manifest, token, subtitle, data
def decrypt_token(self, token: str, client: str) -> tuple:
if client == "ANDROID":
key = self.config["android"]["key"]
iv = self.config["android"]["iv"]
if client == "WEB":
key = self.config["web"]["key"]
iv = self.config["web"]["iv"]
if isinstance(token, str):
token = base64.b64decode(token)
cipher = AES.new(
key=base64.b64decode(key),
iv=base64.b64decode(iv),
mode=AES.MODE_CBC,
)
data = unpad(cipher.decrypt(token), AES.block_size)
dec_token = data.decode().split("|")[1]
return dec_token.strip()
def get_html(self, url: str) -> dict:
r = self.session.get(url=url, headers=self.config["headers"])
r.raise_for_status()
init_data = re.search(
"<script>window.__PARAMS__ = (.*)</script>",
"".join(r.content.decode().replace("\u200c", "").replace("\r\n", "").replace("undefined", "null")),
)
try:
data = json.loads(init_data.group(1))
return data["initialData"]
except Exception:
self.log.error(f"Failed to get episode for {url}")
sys.exit(1)
@@ -1,27 +0,0 @@
headers:
Accept-Language: en-US,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36
endpoints:
login: https://api.channel4.com/online/v2/auth/token
title: https://api.channel4.com/online/v1/views/content-hubs/{title}.json
license: https://c4.eme.lp.aws.redbeemedia.com/wvlicenceproxy-service/widevine/acquire
search: https://all4nav.channel4.com/v1/api/search
android:
key: QVlESUQ4U0RGQlA0TThESA=="
iv: MURDRDAzODNES0RGU0w4Mg=="
auth: MzZVVUN0OThWTVF2QkFnUTI3QXU4ekdIbDMxTjlMUTE6Sllzd3lIdkdlNjJWbGlrVw==
vod: https://api.channel4.com/online/v1/vod/stream/{video_id}?client=android-mod
web:
key: bjljTGllWWtxd3pOQ3F2aQ==
iv: b2R6Y1UzV2RVaVhMdWNWZA==
vod: https://www.channel4.com/vod/stream/{programmeId}
device:
platform_name: android
device_type: mobile
device_name: "Sony C6903 (C6903)"
app_version: "android_app:9.4.2"
optimizely_datafile: "2908"
@@ -1,175 +0,0 @@
from __future__ import annotations
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
from functools import partial
from pathlib import Path
import sys
import re
import click
import webvtt
import requests
from click import Context
from bs4 import BeautifulSoup
from envied.core.credential import Credential
from envied.core.service import Service
from envied.core.titles import Movie, Movies, Episode, Series
from envied.core.tracks import Track, Chapter, Tracks, Video, Subtitle
from envied.core.manifests.hls import HLS
from envied.core.manifests.dash import DASH
class ARD(Service):
"""
Service code for ARD Mediathek (https://www.ardmediathek.de)
\b
Version: 1.0.0
Author: lambda
Authorization: None
Robustness:
Unencrypted: 2160p, AAC2.0
"""
GEOFENCE = ("de",)
TITLE_RE = r"^(https://www\.ardmediathek\.de/(?P<item_type>serie|video)/.+/)(?P<item_id>[a-zA-Z0-9]{10,})(/[0-9]{1,3})?$"
EPISODE_NAME_RE = r"^(Folge [0-9]+:)?(?P<name>[^\(]+) \(S(?P<season>[0-9]+)/E(?P<episode>[0-9]+)\)$"
@staticmethod
@click.command(name="ARD", short_help="https://www.ardmediathek.de", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> ARD:
return ARD(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
pass
def get_titles(self) -> Union[Movies, Series]:
match = re.match(self.TITLE_RE, self.title)
if not match:
return
item_id = match.group("item_id")
if match.group("item_type") == "video":
return self.load_player(item_id)
r = self.session.get(self.config["endpoints"]["grouping"].format(item_id=item_id))
item = r.json()
for widget in item["widgets"]:
if widget["type"] == "gridlist" and widget.get("compilationType") == "itemsOfShow":
episodes = Series()
for teaser in widget["teasers"]:
if teaser["coreAssetType"] != "EPISODE":
continue
if 'Hörfassung' in teaser['longTitle']:
continue
episodes += self.load_player(teaser["id"])
return episodes
def get_tracks(self, title: Union[Episode, Movie]) -> Tracks:
if title.data["blockedByFsk"]:
self.log.error(
"This content is age-restricted and not currently available. "
"Try again after 10pm German time")
sys.exit(0)
media_collection = title.data["mediaCollection"]["embedded"]
tracks = Tracks()
for stream_collection in media_collection["streams"]:
if stream_collection["kind"] != "main":
continue
for stream in stream_collection["media"]:
if stream["mimeType"] == "application/vnd.apple.mpegurl":
tracks += Tracks(HLS.from_url(stream["url"]).to_tracks(stream["audios"][0]["languageCode"]))
break
# Fetch tracks from HBBTV endpoint to check for potential H.265/2160p DASH
r = self.session.get(self.config["endpoints"]["hbbtv"].format(item_id=title.id))
hbbtv = r.json()
for stream in hbbtv["video"]["streams"]:
for media in stream["media"]:
if media["mimeType"] == "application/dash+xml" and media["audios"][0]["kind"] == "standard":
tracks += Tracks(DASH.from_url(media["url"]).to_tracks(media["audios"][0]["languageCode"]))
break
# for stream in title.data["video"]["streams"]:
# for media in stream["media"]:
# if media["mimeType"] != "video/mp4" or media["audios"][0]["kind"] != "standard":
# continue
# tracks += Video(
# codec=Video.Codec.AVC, # Should check media["videoCodec"]
# range_=Video.Range.SDR, # Should check media["isHighDynamicRange"]
# width=media["maxHResolutionPx"],
# height=media["maxVResolutionPx"],
# url=media["url"],
# language=media["audios"][0]["languageCode"],
# fps=50,
# )
for sub in media_collection["subtitles"]:
for source in sub["sources"]:
if source["kind"] == "ebutt":
tracks.add(Subtitle(
codec=Subtitle.Codec.TimedTextMarkupLang,
language=sub["languageCode"],
url=source["url"]
))
return tracks
def get_chapters(self, title: Union[Episode, Movie]) -> list[Chapter]:
return []
def load_player(self, item_id):
r = self.session.get(self.config["endpoints"]["item"].format(item_id=item_id))
item = r.json()
for widget in item["widgets"]:
if widget["type"] != "player_ondemand":
continue
common_data = {
"id_": item_id,
"data": widget,
"service": self.__class__,
"language": "de",
"year": widget["broadcastedOn"][0:4],
}
if widget["show"]["coreAssetType"] == "SINGLE" or not widget["show"].get("availableSeasons"):
return Movies([Movie(
name=widget["title"],
**common_data
)])
else:
match = re.match(self.EPISODE_NAME_RE, widget["title"])
if not match:
name = widget["title"]
season = 0
episode = 0
else:
name = match.group("name")
season = match.group("season") or 0
episode = match.group("episode") or 0
return Series([Episode(
name=name,
title=widget["show"]["title"],
#season=widget["show"]["availableSeasons"][0],
season=season,
number=episode,
**common_data
)])
@@ -1,8 +0,0 @@
headers:
Accept-Language: de-DE,de;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
endpoints:
item: https://api.ardmediathek.de/page-gateway/pages/ard/item/{item_id}?embedded=true&mcV6=true
grouping: https://api.ardmediathek.de/page-gateway/pages/ard/grouping/{item_id}?seasoned=true&embedded=true
hbbtv: https://tv.ardmediathek.de/dyn/get?id=video:{item_id}
@@ -1,248 +0,0 @@
from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Generator
from typing import Any, Optional, Union
from urllib.parse import urljoin
import click
from click import Context
from requests import Request
from envied.core.constants import AnyTrack
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Subtitle, Tracks
class AUBC(Service):
"""
\b
Service code for ABC iView streaming service (https://iview.abc.net.au/).
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: None
Robustness:
L3: 1080p, AAC2.0
\b
Tips:
- Input should be complete URL:
SHOW: https://iview.abc.net.au/show/return-to-paradise
EPISODE: https://iview.abc.net.au/video/DR2314H001S00
MOVIE: https://iview.abc.net.au/show/way-back / https://iview.abc.net.au/show/way-back/video/ZW3981A001S00
"""
GEOFENCE = ("au",)
ALIASES = ("iview", "abciview", "iv",)
@staticmethod
@click.command(name="AUBC", short_help="https://iview.abc.net.au/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> AUBC:
return AUBC(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.session.headers.update(self.config["headers"])
def search(self) -> Generator[SearchResult, None, None]:
url = (
"https://y63q32nvdl-1.algolianet.com/1/indexes/*/queries?x-algolia-agent=Algolia"
"%20for%20JavaScript%20(4.9.1)%3B%20Browser%20(lite)%3B%20react%20(17.0.2)%3B%20"
"react-instantsearch%20(6.30.2)%3B%20JS%20Helper%20(3.10.0)&x-"
"algolia-api-key=bcdf11ba901b780dc3c0a3ca677fbefc&x-algolia-application-id=Y63Q32NVDL"
)
payload = {
"requests": [
{
"indexName": "ABC_production_iview_web",
"params": f"query={self.title}&tagFilters=&userToken=anonymous-74be3cf1-1dc7-4fa1-9cff-19592162db1c",
}
],
}
results = self._request("POST", url, payload=payload)["results"]
hits = [x for x in results[0]["hits"] if x["docType"] == "Program"]
for result in hits:
yield SearchResult(
id_="https://iview.abc.net.au/show/{}".format(result.get("slug")),
title=result.get("title"),
description=result.get("synopsis"),
label=result.get("subType"),
url="https://iview.abc.net.au/show/{}".format(result.get("slug")),
)
def get_titles(self) -> Union[Movies, Series]:
title_re = r"^(?:https?://(?:www.)?iview.abc.net.au/(?P<type>show|video)/)?(?P<id>[a-zA-Z0-9_-]+)"
try:
kind, title_id = (re.match(title_re, self.title).group(i) for i in ("type", "id"))
except Exception:
raise ValueError("- Could not parse ID from title")
if kind == "show":
data = self._request("GET", "/v3/show/{}".format(title_id))
label = data.get("type")
if label.lower() in ("series", "program"):
episodes = self._series(title_id)
return Series(episodes)
elif label.lower() in ("feature", "movie"):
movie = self._movie(data)
return Movies(movie)
elif kind == "video":
episode = self._episode(title_id)
return Series([episode])
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
video = self._request("GET", "/v3/video/{}".format(title.id))
if not video.get("playable"):
raise ConnectionError(video.get("unavailableMessage"))
playlist = video.get("_embedded", {}).get("playlist", {})
if not playlist:
raise ConnectionError("Could not find a playlist for this title")
streams = next(x["streams"]["mpegdash"] for x in playlist if x["type"] == "program")
captions = next((x.get("captions") for x in playlist if x["type"] == "program"), None)
title.data["protected"] = streams.get("protected", False)
if "720" in streams:
streams["1080"] = streams["720"].replace("720", "1080")
manifest = next(
(url for key in ["1080", "720", "sd", "sd-low"] if key in streams
for url in [streams[key]]
if self.session.head(url).status_code == 200),
None
)
if not manifest:
raise ValueError("Could not find a manifest for this title")
tracks = DASH.from_url(manifest, self.session).to_tracks(title.language)
for track in tracks.audio:
role = track.data["dash"]["adaptation_set"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
if captions:
subtitles = captions.get("src-vtt")
tracks.add(
Subtitle(
id_=hashlib.md5(subtitles.encode()).hexdigest()[0:6],
url=subtitles,
codec=Subtitle.Codec.from_mime(subtitles[-3:]),
language=title.language,
forced=False,
)
)
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
if not title.data.get("cuePoints"):
return Chapters()
credits = next((x.get("start") for x in title.data["cuePoints"] if x["type"] == "end-credits"), None)
if credits:
return Chapters([Chapter(name="Credits", timestamp=credits * 1000)])
return Chapters()
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, *, challenge: bytes, title: Union[Movies, Series], track: AnyTrack) -> Optional[Union[bytes, str]]:
if not title.data.get("protected"):
return None
customdata = self._license(title.id)
headers = {"customdata": customdata}
r = self.session.post(self.config["endpoints"]["license"], headers=headers, data=challenge)
r.raise_for_status()
return r.content
# Service specific
def _series(self, title: str) -> Episode:
data = self._request("GET", "/v3/series/{}".format(title))
episodes = [
self.create_episode(episode)
for season in data
for episode in reversed(season["_embedded"]["videoEpisodes"]["items"])
if season.get("episodeCount")
]
return Series(episodes)
def _movie(self, data: dict) -> Movie:
return [
Movie(
id_=data["_embedded"]["highlightVideo"]["id"],
service=self.__class__,
name=data.get("title"),
year=data.get("productionYear"),
data=data,
language=data.get("analytics", {}).get("dataLayer", {}).get("d_language", "en"),
)
]
def _episode(self, video_id: str) -> Episode:
data = self._request("GET", "/v3/video/{}".format(video_id))
return self.create_episode(data)
def _license(self, video_id: str):
token = self._request("POST", "/v3/token/jwt", data={"clientId": self.config["client"]})["token"]
response = self._request("GET", "/v3/token/drm/{}".format(video_id), headers={"bearer": token})
return response["license"]
def create_episode(self, episode: dict) -> Episode:
title = episode["showTitle"]
series_id = episode.get("analytics", {}).get("dataLayer", {}).get("d_series_id", "")
episode_name = episode.get("analytics", {}).get("dataLayer", {}).get("d_episode_name", "")
number = re.search(r"Episode (\d+)", episode.get("displaySubtitle", ""))
name = re.search(r"S\d+\sEpisode\s\d+\s(.*)", episode_name)
language = episode.get("analytics", {}).get("dataLayer", {}).get("d_language", "en")
return Episode(
id_=episode["id"],
service=self.__class__,
title=title,
season=int(series_id.split("-")[-1]) if series_id else 0,
number=int(number.group(1)) if number else 0,
name=name.group(1) if name else None,
data=episode,
language=language,
)
def _request(self, method: str, api: str, **kwargs: Any) -> Any[dict | str]:
url = urljoin(self.config["endpoints"]["base_url"], api)
prep = self.session.prepare_request(Request(method, url, **kwargs))
response = self.session.send(prep)
if response.status_code != 200:
raise ConnectionError(f"{response.text}")
try:
return json.loads(response.content)
except json.JSONDecodeError as e:
raise ValueError(f"Failed to parse JSON: {response.text}") from e
@@ -1,9 +0,0 @@
headers:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
accept-language: en-US,en;q=0.8
endpoints:
base_url: https://api.iview.abc.net.au
license: https://wv-keyos.licensekeyserver.com/
client: "1d4b5cba-42d2-403e-80e7-34565cdf772d"
@@ -1,319 +0,0 @@
from __future__ import annotations
import json
import re
import sys
from collections.abc import Generator
from http.cookiejar import CookieJar
from typing import Any, Optional, Union
from urllib.parse import urljoin
import click
from click import Context
from requests import Request
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH, HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks
class CBC(Service):
"""
\b
Service code for CBC Gem streaming service (https://gem.cbc.ca/).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: Credentials
Robustness:
AES-128: 1080p, DDP5.1
Widevine L3: 720p, DDP5.1
\b
Tips:
- Input can be complete title URL or just the slug:
SHOW: https://gem.cbc.ca/murdoch-mysteries OR murdoch-mysteries
MOVIE: https://gem.cbc.ca/the-babadook OR the-babadook
\b
Notes:
- DRM encrypted titles max out at 720p.
- CCExtrator v0.94 will likely fail to extract subtitles. It's recommended to downgrade to v0.93.
- Some audio tracks contain invalid data, causing warning messages from mkvmerge during muxing
These can be ignored.
"""
GEOFENCE = ("ca",)
ALIASES = ("gem", "cbcgem",)
@staticmethod
@click.command(name="CBC", short_help="https://gem.cbc.ca/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> CBC:
return CBC(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title: str = title
super().__init__(ctx)
self.base_url: str = self.config["endpoints"]["base_url"]
def search(self) -> Generator[SearchResult, None, None]:
params = {
"device": "web",
"pageNumber": "1",
"pageSize": "20",
"term": self.title,
}
response: dict = self._request("GET", "/ott/catalog/v1/gem/search", params=params)
for result in response.get("result", []):
yield SearchResult(
id_="https://gem.cbc.ca/{}".format(result.get("url")),
title=result.get("title"),
description=result.get("synopsis"),
label=result.get("type"),
url="https://gem.cbc.ca/{}".format(result.get("url")),
)
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
tokens: Optional[Any] = self.cache.get(f"tokens_{credential.sha1}")
"""
All grant types for future reference:
PASSWORD("password"),
ACCESS_TOKEN("access_token"),
REFRESH_TOKEN("refresh_token"),
CLIENT_CREDENTIALS("client_credentials"),
AUTHORIZATION_CODE("authorization_code"),
CODE("code");
"""
if tokens and not tokens.expired:
# cached
self.log.info(" + Using cached tokens")
auth_token: str = tokens.data["access_token"]
elif tokens and tokens.expired:
# expired, refresh
self.log.info("Refreshing cached tokens...")
auth_url, scopes = self.settings()
params = {
"client_id": self.config["client"]["id"],
"grant_type": "refresh_token",
"refresh_token": tokens.data["refresh_token"],
"scope": scopes,
}
access: dict = self._request("POST", auth_url, params=params)
# Shorten expiration by one hour to account for clock skew
tokens.set(access, expiration=int(access["expires_in"]) - 3600)
auth_token: str = access["access_token"]
else:
# new
self.log.info("Requesting new tokens...")
auth_url, scopes = self.settings()
params = {
"client_id": self.config["client"]["id"],
"grant_type": "password",
"username": credential.username,
"password": credential.password,
"scope": scopes,
}
access: dict = self._request("POST", auth_url, params=params)
# Shorten expiration by one hour to account for clock skew
tokens.set(access, expiration=int(access["expires_in"]) - 3600)
auth_token: str = access["access_token"]
claims_token: str = self.claims_token(auth_token)
self.session.headers.update({"x-claims-token": claims_token})
def get_titles(self) -> Union[Movies, Series]:
title_re: str = r"^(?:https?://(?:www.)?gem.cbc.ca/)?(?P<id>[a-zA-Z0-9_-]+)"
try:
title_id: str = re.match(title_re, self.title).group("id")
except Exception:
raise ValueError("- Could not parse ID from title")
params = {"device": "web"}
data: dict = self._request("GET", "/ott/catalog/v2/gem/show/{}".format(title_id), params=params)
label: str = data.get("contentType", "").lower()
if label in ("film", "movie", "standalone"):
movies: list[Movie] = self._movie(data)
return Movies(movies)
else:
episodes: list[Episode] = self._show(data)
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
index: dict = self._request(
"GET", "/media/meta/v1/index.ashx", params={"appCode": "gem", "idMedia": title.id, "output": "jsonObject"}
)
title.data["extra"] = {
"chapters": index["Metas"].get("Chapitres"),
"credits": index["Metas"].get("CreditStartTime"),
}
self.drm: bool = index["Metas"].get("isDrmActive") == "true"
if self.drm:
tech: str = next(tech["name"] for tech in index["availableTechs"] if "widevine" in tech["drm"])
else:
tech: str = next(tech["name"] for tech in index["availableTechs"] if not tech["drm"])
response: dict = self._request(
"GET", self.config["endpoints"]["validation"].format("android", title.id, "smart-tv", tech)
)
manifest = response.get("url")
self.license = next((x["value"] for x in response["params"] if "widevineLicenseUrl" in x["name"]), None)
self.token = next((x["value"] for x in response["params"] if "widevineAuthToken" in x["name"]), None)
stream_type: Union[HLS, DASH] = HLS if tech == "hls" else DASH
tracks: Tracks = stream_type.from_url(manifest, self.session).to_tracks(language=title.language)
if stream_type == DASH:
for track in tracks.audio:
label = track.data["dash"]["adaptation_set"].find("Label")
if label is not None and "descriptive" in label.text.lower():
track.descriptive = True
for track in tracks:
track.language = title.language
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
extra: dict = title.data["extra"]
chapters = []
if extra.get("chapters"):
chapters = [Chapter(timestamp=x) for x in set(extra["chapters"].split(","))]
if extra.get("credits"):
chapters.append(Chapter(name="Credits", timestamp=float(extra["credits"])))
return Chapters(chapters)
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(
self, *, challenge: bytes, title: Union[Movies, Series], track: AnyTrack
) -> Optional[Union[bytes, str]]:
if not self.license or not self.token:
return None
headers = {"x-dt-auth-token": self.token}
r = self.session.post(self.license, headers=headers, data=challenge)
r.raise_for_status()
return r.content
# Service specific
def _show(self, data: dict) -> list[Episode]:
lineups: list = next((x["lineups"] for x in data["content"] if x.get("title", "").lower() == "episodes"), None)
if not lineups:
self.log.warning("No episodes found for: {}".format(data.get("title")))
return
titles = []
for season in lineups:
for episode in season["items"]:
if episode.get("mediaType", "").lower() == "episode":
parts = episode.get("title", "").split(".", 1)
episode_name = parts[1].strip() if len(parts) > 1 else parts[0].strip()
titles.append(
Episode(
id_=episode["idMedia"],
service=self.__class__,
title=data.get("title"),
season=int(season.get("seasonNumber", 0)),
number=int(episode.get("episodeNumber", 0)),
name=episode_name,
year=episode.get("metadata", {}).get("productionYear"),
language=data["structuredMetadata"].get("inLanguage", "en-CA"),
data=episode,
)
)
return titles
def _movie(self, data: dict) -> list[Movie]:
unwanted: tuple = ("episodes", "trailers", "extras")
lineups: list = next((x["lineups"] for x in data["content"] if x.get("title", "").lower() not in unwanted), None)
if not lineups:
self.log.warning("No movies found for: {}".format(data.get("title")))
return
titles = []
for season in lineups:
for movie in season["items"]:
if movie.get("mediaType", "").lower() == "episode":
parts = movie.get("title", "").split(".", 1)
movie_name = parts[1].strip() if len(parts) > 1 else parts[0].strip()
titles.append(
Movie(
id_=movie.get("idMedia"),
service=self.__class__,
name=movie_name,
year=movie.get("metadata", {}).get("productionYear"),
language=data["structuredMetadata"].get("inLanguage", "en-CA"),
data=movie,
)
)
return titles
def settings(self) -> tuple:
settings = self._request("GET", "/ott/catalog/v1/gem/settings", params={"device": "web"})
auth_url: str = settings["identityManagement"]["ropc"]["url"]
scopes: str = settings["identityManagement"]["ropc"]["scopes"]
return auth_url, scopes
def claims_token(self, token: str) -> str:
headers = {
"Authorization": "Bearer " + token,
}
params = {"device": "web"}
response: dict = self._request(
"GET", "/ott/subscription/v2/gem/Subscriber/profile", headers=headers, params=params
)
return response["claimsToken"]
def _request(self, method: str, api: str, **kwargs: Any) -> Any[dict | str]:
url: str = urljoin(self.base_url, api)
prep: Request = self.session.prepare_request(Request(method, url, **kwargs))
response = self.session.send(prep)
if response.status_code not in (200, 426):
raise ConnectionError(f"{response.status_code} - {response.text}")
try:
data = json.loads(response.content)
error_keys = ["errorMessage", "ErrorMessage", "ErrorCode", "errorCode", "error"]
error_message = next((data.get(key) for key in error_keys if key in data), None)
if error_message:
self.log.error(f"\n - Error: {error_message}\n")
sys.exit(1)
return data
except json.JSONDecodeError:
raise ConnectionError("Request for {} failed: {}".format(response.url, response.text))
@@ -1,7 +0,0 @@
endpoints:
base_url: "https://services.radio-canada.ca"
validation: "/media/validation/v2?appCode=gem&&deviceType={}&idMedia={}&manifestType={}&output=json&tech={}"
api_key: "3f4beddd-2061-49b0-ae80-6f1f2ed65b37"
client:
id: "fc05b0ee-3865-4400-a3cc-3da82c330c23"
@@ -1,242 +0,0 @@
from __future__ import annotations
import json
import re
import sys
from collections.abc import Generator
from typing import Any, Optional, Union
from urllib.parse import urljoin
import click
from requests import Request
from envied.core.constants import AnyTrack
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Series, Title_T, Titles_T
from envied.core.tracks import Chapter, Chapters, Tracks
from envied.core.utils.sslciphers import SSLCiphers
from envied.core.utils.xml import load_xml
class CBS(Service):
"""
\b
Service code for CBS.com streaming service (https://cbs.com).
Credit to @srpen6 for the tip on anonymous session
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: None
Robustness:
Widevine:
L3: 2160p, DDP5.1
\b
Tips:
- Input should be complete URLs:
SERIES: https://www.cbs.com/shows/tracker/
EPISODE: https://www.cbs.com/shows/video/E0wG_ovVMkLlHOzv7KDpUV9bjeKFFG2v/
\b
Common VPN/proxy errors:
- SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING]'))
- ConnectionError: 406 Not Acceptable, 403 Forbidden
"""
GEOFENCE = ("us",)
@staticmethod
@click.command(name="CBS", short_help="https://cbs.com", help=__doc__)
@click.argument("title", type=str, required=False)
@click.pass_context
def cli(ctx, **kwargs) -> CBS:
return CBS(ctx, **kwargs)
def __init__(self, ctx, title):
self.title = title
super().__init__(ctx)
def search(self) -> Generator[SearchResult, None, None]:
params = {
"term": self.title,
"termCount": 50,
"showCanVids": "true",
}
results = self._request("GET", "/apps-api/v3.1/androidphone/contentsearch/search.json", params=params)["terms"]
for result in results:
yield SearchResult(
id_=result.get("path"),
title=result.get("title"),
description=None,
label=result.get("term_type"),
url=result.get("path"),
)
def get_titles(self) -> Titles_T:
title_re = r"https://www\.cbs\.com/shows/(?P<video>video/)?(?P<id>[a-zA-Z0-9_-]+)/?$"
try:
video, title_id = (re.match(title_re, self.title).group(i) for i in ("video", "id"))
except Exception:
raise ValueError("- Could not parse ID from title")
if video:
episodes = self._episode(title_id)
else:
episodes = self._show(title_id)
return Series(episodes)
def get_tracks(self, title: Title_T) -> Tracks:
self.token, self.license = self.ls_session(title.id)
manifest = self.get_manifest(title)
return DASH.from_url(url=manifest).to_tracks(language=title.language)
def get_chapters(self, title: Episode) -> Chapters:
if not title.data.get("playbackEvents", {}).get("endCreditChapterTimeMs"):
return Chapters()
end_credits = title.data["playbackEvents"]["endCreditChapterTimeMs"]
return Chapters([Chapter(name="Credits", timestamp=end_credits)])
def certificate(self, **_):
return None # will use common privacy cert
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
headers = {"Authorization": f"Bearer {self.token}"}
r = self.session.post(self.license, headers=headers, data=challenge)
if not r.ok:
self.log.error(r.text)
sys.exit(1)
return r.content
# Service specific functions
def _show(self, title: str) -> Episode:
data = self._request("GET", "/apps-api/v3.0/androidphone/shows/slug/{}.json".format(title))
links = next((x.get("links") for x in data["showMenu"] if x.get("device_app_id") == "all_platforms"), None)
config = next((x.get("videoConfigUniqueName") for x in links if x.get("title").strip() == "Episodes"), None)
show = next((x for x in data["show"]["results"] if x.get("type").strip() == "show"), None)
seasons = [x.get("seasonNum") for x in data["available_video_seasons"].get("itemList", [])]
locale = show.get("locale", "en-US")
show_data = self._request(
"GET", "/apps-api/v2.0/androidphone/shows/{}/videos/config/{}.json".format(show.get("show_id"), config),
params={"platformType": "apps", "rows": "1", "begin": "0"},
)
section = next(
(x["sectionId"] for x in show_data["videoSectionMetadata"] if x["title"] == "Full Episodes"), None
)
episodes = []
for season in seasons:
res = self._request(
"GET", "/apps-api/v2.0/androidphone/videos/section/{}.json".format(section),
params={"begin": "0", "rows": "999", "params": f"seasonNum={season}", "seasonNum": season},
)
episodes.extend(res["sectionItems"].get("itemList", []))
return [
Episode(
id_=episode["contentId"],
title=episode["seriesTitle"],
season=episode["seasonNum"] if episode["fullEpisode"] else 0,
number=episode["episodeNum"] if episode["fullEpisode"] else episode["positionNum"],
name=episode["label"],
language=locale,
service=self.__class__,
data=episode,
)
for episode in episodes
if episode["fullEpisode"]
]
def _episode(self, title: str) -> Episode:
data = self._request("GET", "/apps-api/v2.0/androidphone/video/cid/{}.json".format(title))
return [
Episode(
id_=episode["contentId"],
title=episode["seriesTitle"],
season=episode["seasonNum"] if episode["fullEpisode"] else 0,
number=episode["episodeNum"] if episode["fullEpisode"] else episode["positionNum"],
name=episode["label"],
language="en-US",
service=self.__class__,
data=episode,
)
for episode in data["itemList"]
]
def ls_session(self, content_id: str) -> str:
res = self._request(
"GET", "/apps-api/v3.0/androidphone/irdeto-control/anonymous-session-token.json",
params={"contentId": content_id},
)
return res.get("ls_session"), res.get("url")
def get_manifest(self, title: Episode) -> str:
try:
res = self._request(
"GET", "http://link.theplatform.com/s/{}/media/guid/2198311517/{}".format(
title.data.get("cmsAccountId"), title.id
),
params={
"format": "SMIL",
"assetTypes": "|".join(self.config["assets"]),
"formats": "MPEG-DASH,MPEG4,M3U",
},
)
body = load_xml(res).find("body").find("seq").findall("switch")
bitrate = max(body, key=lambda x: int(x.find("video").get("system-bitrate")))
videos = [x.get("src") for x in bitrate.findall("video")]
if not videos:
raise ValueError("Could not find any streams - is the title still available?")
manifest = next(
(x for x in videos if "hdr_dash" in x.lower()),
next((x for x in videos if "cenc_dash" in x.lower()), videos[0]),
)
except Exception as e:
self.log.warning("ThePlatform request failed: {}, falling back to standard manifest".format(e))
if not title.data.get("streamingUrl"):
raise ValueError("Could not find any streams - is the title still available?")
manifest = title.data.get("streamingUrl")
return manifest
def _request(self, method: str, api: str, params: dict = None, headers: dict = None) -> Any[dict | str]:
url = urljoin(self.config["endpoints"]["base_url"], api)
self.session.headers.update(self.config["headers"])
self.session.params = {"at": self.config["endpoints"]["token"]}
for prefix in ("https://", "http://"):
self.session.mount(prefix, SSLCiphers(security_level=2))
if params:
self.session.params.update(params)
if headers:
self.session.headers.update(headers)
prep = self.session.prepare_request(Request(method, url))
response = self.session.send(prep)
if response.status_code != 200:
raise ConnectionError(f"{response.text}")
try:
data = json.loads(response.content)
if not data.get("success"):
raise ValueError(data.get("message"))
return data
except json.JSONDecodeError:
return response.text
@@ -1,10 +0,0 @@
headers:
user-agent: Mozilla/5.0 (Linux; Android 13; SM-A536E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36
endpoints:
base_url: https://cbsdigital.cbs.com
token: ABBsaBMagMmYLUc9iXB0lXEKsUQ0/MwRn6z3Tg0KKQaH7Q6QGqJcABwlBP4XiMR1b0Q=
assets: [HLS_AES, DASH_LIVE, DASH_CENC, DASH_CENC_HDR10, DASH_LIVE, DASH_TA, DASH_CENC_PS4]
@@ -1,774 +0,0 @@
import re
import time
import uuid
from threading import Lock
from typing import Generator, Optional, Union
import click
import jwt
from langcodes import Language
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.session import session
from envied.core.titles import Episode, Series
from envied.core.tracks import Attachment, Chapters, Tracks
from envied.core.tracks.chapter import Chapter
from envied.core.tracks.subtitle import Subtitle
class CR(Service):
"""
Service code for Crunchyroll streaming service (https://www.crunchyroll.com).
\b
Version: 2.0.0
Author: sp4rk.y
Date: 2025-11-01
Authorization: Credentials
Robustness:
Widevine:
L3: 1080p, AAC2.0
\b
Tips:
- Input should be complete URL or series ID
https://www.crunchyroll.com/series/GRMG8ZQZR/series-name OR GRMG8ZQZR
- Supports multiple audio and subtitle languages
- Device ID is cached for consistent authentication across runs
\b
Notes:
- Uses password-based authentication with token caching
- Manages concurrent stream limits automatically
"""
TITLE_RE = r"^(?:https?://(?:www\.)?crunchyroll\.com/(?:series|watch)/)?(?P<id>[A-Z0-9]+)"
LICENSE_LOCK = Lock()
MAX_CONCURRENT_STREAMS = 3
ACTIVE_STREAMS: list[tuple[str, str]] = []
@staticmethod
def get_session():
return session("okhttp4")
@staticmethod
@click.command(name="CR", short_help="https://crunchyroll.com")
@click.argument("title", type=str, required=True)
@click.pass_context
def cli(ctx, **kwargs) -> "CR":
return CR(ctx, **kwargs)
def __init__(self, ctx, title: str):
self.title = title
self.account_id: Optional[str] = None
self.access_token: Optional[str] = None
self.token_expiration: Optional[int] = None
self.anonymous_id = str(uuid.uuid4())
super().__init__(ctx)
device_cache_key = "cr_device_id"
cached_device = self.cache.get(device_cache_key)
if cached_device and not cached_device.expired:
self.device_id = cached_device.data["device_id"]
else:
self.device_id = str(uuid.uuid4())
cached_device.set(
data={"device_id": self.device_id},
expiration=60 * 60 * 24 * 365 * 10,
)
self.device_name = self.config.get("device", {}).get("name", "SHIELD Android TV")
self.device_type = self.config.get("device", {}).get("type", "ANDROIDTV")
self.session.headers.update(self.config.get("headers", {}))
self.session.headers["etp-anonymous-id"] = self.anonymous_id
@property
def auth_header(self) -> dict:
"""Return authorization header dict."""
return {"authorization": f"Bearer {self.access_token}"}
def ensure_authenticated(self) -> None:
"""Check if token is expired and re-authenticate if needed."""
if not self.token_expiration:
cache_key = f"cr_auth_token_{self.credential.sha1 if self.credential else 'default'}"
cached = self.cache.get(cache_key)
if cached and not cached.expired:
self.access_token = cached.data["access_token"]
self.account_id = cached.data.get("account_id")
self.token_expiration = cached.data.get("token_expiration")
self.session.headers.update(self.auth_header)
self.log.debug("Loaded authentication from cache")
else:
self.log.debug("No valid cached token, authenticating")
self.authenticate(credential=self.credential)
return
current_time = int(time.time())
if current_time >= (self.token_expiration - 60):
self.log.debug("Authentication token expired or expiring soon, re-authenticating")
self.authenticate(credential=self.credential)
def authenticate(self, cookies=None, credential=None) -> None:
"""Authenticate using username and password credentials."""
super().authenticate(cookies, credential)
cache_key = f"cr_auth_token_{credential.sha1 if credential else 'default'}"
cached = self.cache.get(cache_key)
if cached and not cached.expired:
self.access_token = cached.data["access_token"]
self.account_id = cached.data.get("account_id")
self.token_expiration = cached.data.get("token_expiration")
else:
if not credential:
raise ValueError("Username and password credential required for authentication")
response = self.session.post(
url=self.config["endpoints"]["token"],
headers={
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"request-type": "SignIn",
},
data={
"grant_type": "password",
"username": credential.username,
"password": credential.password,
"scope": "offline_access",
"client_id": self.config["client"]["id"],
"client_secret": self.config["client"]["secret"],
"device_type": self.device_type,
"device_id": self.device_id,
"device_name": self.device_name,
},
)
if response.status_code != 200:
self.log.error(f"Login failed: {response.status_code}")
try:
error_data = response.json()
error_msg = error_data.get("error", "Unknown error")
error_code = error_data.get("code", "")
self.log.error(f"Error: {error_msg} ({error_code})")
except Exception:
self.log.error(f"Response: {response.text}")
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.account_id = self.get_account_id()
try:
decoded_token = jwt.decode(self.access_token, options={"verify_signature": False})
self.token_expiration = decoded_token.get("exp")
except Exception:
self.token_expiration = int(time.time()) + token_data.get("expires_in", 3600)
cached.set(
data={
"access_token": self.access_token,
"account_id": self.account_id,
"token_expiration": self.token_expiration,
},
expiration=self.token_expiration
if isinstance(self.token_expiration, int) and self.token_expiration > int(time.time())
else 3600,
)
self.session.headers.update(self.auth_header)
if self.ACTIVE_STREAMS:
self.ACTIVE_STREAMS.clear()
try:
self.clear_all_sessions()
except Exception as e:
self.log.warning(f"Failed to clear previous sessions: {e}")
def get_titles(self) -> Union[Series]:
"""Fetch series and episode information."""
series_id = self.parse_series_id(self.title)
series_response = self.session.get(
url=self.config["endpoints"]["series"].format(series_id=series_id),
params={"locale": self.config["params"]["locale"]},
).json()
if "error" in series_response:
raise ValueError(f"Series not found: {series_id}")
series_data = (
series_response.get("data", [{}])[0] if isinstance(series_response.get("data"), list) else series_response
)
series_title = series_data.get("title", "Unknown Series")
seasons_response = self.session.get(
url=self.config["endpoints"]["seasons"].format(series_id=series_id),
params={"locale": self.config["params"]["locale"]},
).json()
seasons_data = seasons_response.get("data", [])
if not seasons_data:
raise ValueError(f"No seasons found for series: {series_id}")
all_episode_data = []
special_episodes = []
for season in seasons_data:
season_id = season["id"]
season_number = season.get("season_number", 0)
episodes_response = self.session.get(
url=self.config["endpoints"]["season_episodes"].format(season_id=season_id),
params={"locale": self.config["params"]["locale"]},
).json()
episodes_data = episodes_response.get("data", [])
for episode_data in episodes_data:
episode_number = episode_data.get("episode_number")
if episode_number is None or isinstance(episode_number, float):
special_episodes.append(episode_data)
all_episode_data.append((episode_data, season_number))
if not all_episode_data:
raise ValueError(f"No episodes found for series: {series_id}")
series_year = None
if all_episode_data:
first_episode_data = all_episode_data[0][0]
first_air_date = first_episode_data.get("episode_air_date")
if first_air_date:
series_year = int(first_air_date[:4])
special_episodes.sort(key=lambda x: x.get("episode_air_date", ""))
special_episode_numbers = {ep["id"]: idx + 1 for idx, ep in enumerate(special_episodes)}
episodes = []
season_episode_counts = {}
for episode_data, season_number in all_episode_data:
episode_number = episode_data.get("episode_number")
if episode_number is None or isinstance(episode_number, float):
final_season = 0
final_number = special_episode_numbers[episode_data["id"]]
else:
final_season = season_number
if final_season not in season_episode_counts:
season_episode_counts[final_season] = 0
season_episode_counts[final_season] += 1
final_number = season_episode_counts[final_season]
original_language = None
versions = episode_data.get("versions", [])
for version in versions:
if "main" in version.get("roles", []):
original_language = version.get("audio_locale")
break
episode = Episode(
id_=episode_data["id"],
service=self.__class__,
title=series_title,
season=final_season,
number=final_number,
name=episode_data.get("title"),
year=series_year,
language=original_language,
description=episode_data.get("description"),
data=episode_data,
)
episodes.append(episode)
return Series(episodes)
def set_track_metadata(self, tracks: Tracks, episode_id: str, is_original: bool) -> None:
"""Set metadata for video and audio tracks."""
for video in tracks.videos:
video.needs_repack = True
video.data["episode_id"] = episode_id
video.is_original_lang = is_original
for audio in tracks.audio:
audio.data["episode_id"] = episode_id
audio.is_original_lang = is_original
def get_tracks(self, title: Episode) -> Tracks:
"""Fetch video, audio, and subtitle tracks for an episode."""
self.ensure_authenticated()
episode_id = title.id
if self.ACTIVE_STREAMS:
self.ACTIVE_STREAMS.clear()
self.clear_all_sessions()
initial_response = self.get_playback_data(episode_id, track_stream=False)
versions = initial_response.get("versions", [])
if not versions:
self.log.warning("No versions found in playback response, using single version")
versions = [{"audio_locale": initial_response.get("audioLocale", "ja-JP")}]
tracks = None
for idx, version in enumerate(versions):
audio_locale = version.get("audio_locale")
version_guid = version.get("guid")
is_original = version.get("original", False)
if not audio_locale:
continue
request_episode_id = version_guid if version_guid else episode_id
if idx == 0 and not version_guid:
version_response = initial_response
version_token = version_response.get("token")
else:
if idx == 1 and not versions[0].get("guid"):
initial_token = initial_response.get("token")
if initial_token:
self.close_stream(episode_id, initial_token)
try:
version_response = self.get_playback_data(request_episode_id, track_stream=False)
except ValueError as e:
self.log.warning(f"Could not get playback info for audio {audio_locale}: {e}")
continue
version_token = version_response.get("token")
hard_subs = version_response.get("hardSubs", {})
dash_url = None
if "none" in hard_subs:
dash_url = hard_subs["none"].get("url")
elif hard_subs:
first_key = list(hard_subs.keys())[0]
dash_url = hard_subs[first_key].get("url")
if not dash_url:
self.log.warning(f"No DASH manifest found for audio {audio_locale}, skipping")
if version_token:
self.close_stream(request_episode_id, version_token)
continue
try:
version_tracks = DASH.from_url(
url=dash_url,
session=self.session,
).to_tracks(language=audio_locale)
if tracks is None:
tracks = version_tracks
self.set_track_metadata(tracks, request_episode_id, is_original)
else:
self.set_track_metadata(version_tracks, request_episode_id, is_original)
for video in version_tracks.videos:
tracks.add(video)
for audio in version_tracks.audio:
tracks.add(audio)
except Exception as e:
self.log.warning(f"Failed to parse DASH manifest for audio {audio_locale}: {e}")
if version_token:
self.close_stream(request_episode_id, version_token)
continue
if is_original:
captions = version_response.get("captions", {})
subtitles_data = version_response.get("subtitles", {})
all_subs = {**captions, **subtitles_data}
for lang_code, sub_data in all_subs.items():
if lang_code == "none":
continue
if isinstance(sub_data, dict) and "url" in sub_data:
try:
lang = Language.get(lang_code)
except (ValueError, LookupError):
lang = Language.get("en")
subtitle_format = sub_data.get("format", "vtt").lower()
if subtitle_format == "ass" or subtitle_format == "ssa":
codec = Subtitle.Codec.SubStationAlphav4
else:
codec = Subtitle.Codec.WebVTT
tracks.add(
Subtitle(
id_=f"subtitle-{audio_locale}-{lang_code}",
url=sub_data["url"],
codec=codec,
language=lang,
forced=False,
sdh=False,
),
warn_only=True,
)
if version_token:
self.close_stream(request_episode_id, version_token)
if versions and versions[0].get("guid"):
initial_token = initial_response.get("token")
if initial_token:
self.close_stream(episode_id, initial_token)
if tracks is None:
raise ValueError(f"Failed to fetch any tracks for episode: {episode_id}")
for track in tracks.audio + tracks.subtitles:
if track.language:
try:
lang_obj = Language.get(str(track.language))
base_lang = Language.get(lang_obj.language)
lang_display = base_lang.language_name()
track.name = lang_display
except (ValueError, LookupError):
pass
images = title.data.get("images", {})
thumbnails = images.get("thumbnail", [])
if thumbnails:
thumb_variants = thumbnails[0] if isinstance(thumbnails[0], list) else [thumbnails[0]]
if thumb_variants:
thumb_index = min(7, len(thumb_variants) - 1)
thumb = thumb_variants[thumb_index]
if isinstance(thumb, dict) and "source" in thumb:
thumbnail_name = f"{title.name or title.title} - S{title.season:02d}E{title.number:02d}"
tracks.add(Attachment.from_url(url=thumb["source"], name=thumbnail_name))
return tracks
def get_widevine_license(self, challenge: bytes, title: Episode, track) -> bytes:
"""
Get Widevine license for decryption.
Creates a fresh playback session for each track, gets the license, then immediately
closes the stream. This prevents hitting the 3 concurrent stream limit.
CDN authorization is embedded in the manifest URLs, not tied to active sessions.
"""
self.ensure_authenticated()
track_episode_id = track.data.get("episode_id", title.id)
with self.LICENSE_LOCK:
playback_token = None
try:
playback_data = self.get_playback_data(track_episode_id, track_stream=True)
playback_token = playback_data.get("token")
if not playback_token:
raise ValueError(f"No playback token in response for {track_episode_id}")
track.data["playback_token"] = playback_token
license_response = self.session.post(
url=self.config["endpoints"]["license_widevine"],
params={"specConform": "true"},
data=challenge,
headers={
**self.auth_header,
"content-type": "application/octet-stream",
"accept": "application/octet-stream",
"x-cr-content-id": track_episode_id,
"x-cr-video-token": playback_token,
},
)
if license_response.status_code != 200:
self.log.error(f"License request failed with status {license_response.status_code}")
self.log.error(f"Response: {license_response.text[:500]}")
self.close_stream(track_episode_id, playback_token)
raise ValueError(f"License request failed: {license_response.status_code}")
self.close_stream(track_episode_id, playback_token)
return license_response.content
except Exception:
if playback_token:
try:
self.close_stream(track_episode_id, playback_token)
except Exception:
pass
raise
def cleanup_active_streams(self) -> None:
"""
Close all remaining active streams.
Called to ensure no streams are left open.
"""
if self.ACTIVE_STREAMS:
try:
self.authenticate()
except Exception as e:
self.log.warning(f"Failed to re-authenticate during cleanup: {e}")
for episode_id, token in list(self.ACTIVE_STREAMS):
try:
self.close_stream(episode_id, token)
except Exception as e:
self.log.warning(f"Failed to close stream {episode_id}: {e}")
if (episode_id, token) in self.ACTIVE_STREAMS:
self.ACTIVE_STREAMS.remove((episode_id, token))
def __del__(self) -> None:
"""Cleanup any remaining streams when service is destroyed."""
try:
self.cleanup_active_streams()
except Exception:
pass
def get_chapters(self, title: Episode) -> Chapters:
"""Get chapters/skip events for an episode."""
chapters = Chapters()
chapter_response = self.session.get(
url=self.config["endpoints"]["skip_events"].format(episode_id=title.id),
)
if chapter_response.status_code == 200:
try:
chapter_data = chapter_response.json()
except Exception as e:
self.log.warning(f"Failed to parse chapter data: {e}")
return chapters
for chapter_type in ["intro", "recap", "credits", "preview"]:
if chapter_info := chapter_data.get(chapter_type):
try:
chapters.add(
Chapter(
timestamp=int(chapter_info["start"] * 1000),
name=chapter_info["type"].capitalize(),
)
)
except Exception as e:
self.log.debug(f"Failed to add {chapter_type} chapter: {e}")
return chapters
def search(self) -> Generator[SearchResult, None, None]:
"""Search for content on Crunchyroll."""
try:
response = self.session.get(
url=self.config["endpoints"]["search"],
params={
"q": self.title,
"type": "series",
"start": 0,
"n": 20,
"locale": self.config["params"]["locale"],
},
)
if response.status_code != 200:
self.log.error(f"Search request failed with status {response.status_code}")
return
search_data = response.json()
for result_group in search_data.get("data", []):
for series in result_group.get("items", []):
series_id = series.get("id")
if not series_id:
continue
title = series.get("title", "Unknown")
description = series.get("description", "")
year = series.get("series_launch_year")
if len(description) > 300:
description = description[:300] + "..."
url = f"https://www.crunchyroll.com/series/{series_id}"
label = f"SERIES ({year})" if year else "SERIES"
yield SearchResult(
id_=series_id,
title=title,
label=label,
description=description,
url=url,
)
except Exception as e:
self.log.error(f"Search failed: {e}")
return
def get_account_id(self) -> str:
"""Fetch and return the account ID."""
response = self.session.get(url=self.config["endpoints"]["account_me"], headers=self.auth_header)
if response.status_code != 200:
self.log.error(f"Failed to get account info: {response.status_code}")
self.log.error(f"Response: {response.text}")
response.raise_for_status()
data = response.json()
return data["account_id"]
def close_stream(self, episode_id: str, token: str) -> None:
"""Close an active playback stream to free up concurrent stream slots."""
should_remove = False
try:
response = self.session.delete(
url=self.config["endpoints"]["playback_delete"].format(episode_id=episode_id, token=token),
headers=self.auth_header,
)
if response.status_code in (200, 204, 403):
should_remove = True
else:
self.log.error(
f"Failed to close stream for {episode_id} (status {response.status_code}): {response.text[:200]}"
)
except Exception as e:
self.log.error(f"Error closing stream for {episode_id}: {e}")
finally:
if should_remove and (episode_id, token) in self.ACTIVE_STREAMS:
self.ACTIVE_STREAMS.remove((episode_id, token))
def get_active_sessions(self) -> list:
"""Get all active streaming sessions for the account."""
try:
response = self.session.get(
url=self.config["endpoints"]["playback_sessions"],
headers=self.auth_header,
)
if response.status_code == 200:
data = response.json()
return data.get("items", [])
else:
self.log.warning(f"Failed to get active sessions (status {response.status_code})")
return []
except Exception as e:
self.log.warning(f"Error getting active sessions: {e}")
return []
def clear_all_sessions(self) -> int:
"""
Clear all active streaming sessions created during this or previous runs.
Tries multiple approaches to ensure all streams are closed:
1. Clear tracked streams with known tokens
2. Query active sessions API and close all found streams
3. Try alternate token formats if needed
"""
cleared = 0
if self.ACTIVE_STREAMS:
streams_to_close = self.ACTIVE_STREAMS[:]
for episode_id, playback_token in streams_to_close:
try:
self.close_stream(episode_id, playback_token)
cleared += 1
except Exception:
if (episode_id, playback_token) in self.ACTIVE_STREAMS:
self.ACTIVE_STREAMS.remove((episode_id, playback_token))
sessions = self.get_active_sessions()
if sessions:
for session_data in sessions:
content_id = session_data.get("contentId")
session_token = session_data.get("token")
if content_id and session_token:
tokens_to_try = (
["11-" + session_token[3:], session_token]
if session_token.startswith("08-")
else [session_token]
)
session_closed = False
for token in tokens_to_try:
try:
response = self.session.delete(
url=self.config["endpoints"]["playback_delete"].format(
episode_id=content_id, token=token
),
headers=self.auth_header,
)
if response.status_code in (200, 204):
cleared += 1
session_closed = True
break
elif response.status_code == 403:
session_closed = True
break
except Exception:
pass
if not session_closed:
self.log.warning(f"Unable to close session {content_id} with any token format")
return cleared
def get_playback_data(self, episode_id: str, track_stream: bool = True) -> dict:
"""
Get playback data for an episode with automatic retry on stream limits.
Args:
episode_id: The episode ID to get playback data for
track_stream: Whether to track this stream in active_streams (False for temporary streams)
Returns:
dict: The playback response data
Raises:
ValueError: If playback request fails after retry
"""
self.ensure_authenticated()
max_retries = 2
for attempt in range(max_retries + 1):
response = self.session.get(
url=self.config["endpoints"]["playback"].format(episode_id=episode_id),
params={"queue": "false"},
).json()
if "error" in response:
error_code = response.get("code", "")
error_msg = response.get("message", response.get("error", "Unknown error"))
if error_code == "TOO_MANY_ACTIVE_STREAMS" and attempt < max_retries:
self.log.warning(f"Hit stream limit: {error_msg}")
cleared = self.clear_all_sessions()
if cleared == 0 and attempt == 0:
wait_time = 30
self.log.warning(
f"Found orphaned sessions from previous run. Waiting {wait_time}s for them to expire..."
)
time.sleep(wait_time)
continue
self.log.error(f"Playback API error: {error_msg}")
self.log.debug(f"Full response: {response}")
raise ValueError(f"Could not get playback info for episode: {episode_id} - {error_msg}")
playback_token = response.get("token")
if playback_token and track_stream:
self.ACTIVE_STREAMS.append((episode_id, playback_token))
return response
raise ValueError(f"Failed to get playback data for episode: {episode_id}")
def parse_series_id(self, title_input: str) -> str:
"""Parse series ID from URL or direct ID input."""
match = re.match(self.TITLE_RE, title_input, re.IGNORECASE)
if not match:
raise ValueError(f"Could not parse series ID from: {title_input}")
return match.group("id")
@@ -1,47 +0,0 @@
# Crunchyroll API Configuration
client:
id: "lkesi7snsy9oojmi2r9h"
secret: "-aGDXFFNTluZMLYXERngNYnEjvgH5odv"
# API Endpoints
endpoints:
# Authentication
token: "https://www.crunchyroll.com/auth/v1/token"
# Account
account_me: "https://www.crunchyroll.com/accounts/v1/me"
multiprofile: "https://www.crunchyroll.com/accounts/v1/{account_id}/multiprofile"
# Content Metadata
series: "https://www.crunchyroll.com/content/v2/cms/series/{series_id}"
seasons: "https://www.crunchyroll.com/content/v2/cms/series/{series_id}/seasons"
season_episodes: "https://www.crunchyroll.com/content/v2/cms/seasons/{season_id}/episodes"
skip_events: "https://static.crunchyroll.com/skip-events/production/{episode_id}.json"
# Playback
playback: "https://www.crunchyroll.com/playback/v2/{episode_id}/tv/android_tv/play"
playback_delete: "https://www.crunchyroll.com/playback/v1/token/{episode_id}/{token}"
playback_sessions: "https://www.crunchyroll.com/playback/v1/sessions/streaming"
license_widevine: "https://cr-license-proxy.prd.crunchyrollsvc.com/v1/license/widevine"
# Discovery
search: "https://www.crunchyroll.com/content/v2/discover/search"
# Headers for Android TV client
headers:
user-agent: "Crunchyroll/ANDROIDTV/3.49.1_22281 (Android 11; en-US; SHIELD Android TV)"
accept: "application/json"
accept-charset: "UTF-8"
accept-encoding: "gzip"
connection: "Keep-Alive"
content-type: "application/x-www-form-urlencoded; charset=UTF-8"
# Query parameters
params:
locale: "en-US"
# Device parameters for authentication
device:
type: "ANDROIDTV"
name: "SHIELD Android TV"
model: "SHIELD Android TV"
@@ -1,372 +0,0 @@
from __future__ import annotations
import hashlib
import json
import re
import sys
from collections.abc import Generator
from concurrent.futures import ThreadPoolExecutor, as_completed
from http.cookiejar import CookieJar
from typing import Any, Optional
import click
from pywidevine.cdm import Cdm as WidevineCdm
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Audio, Chapter, Subtitle, Tracks, Video
class CTV(Service):
"""
Service code for CTV.ca (https://www.ctv.ca)
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: Credentials for subscription, none for freely available titles
Robustness:
Widevine:
L3: 1080p, DD5.1
\b
Tips:
- Input can be either complete title/episode URL or just the path:
/shows/young-sheldon
/shows/young-sheldon/baptists-catholics-and-an-attempted-drowning-s7e6
/movies/war-for-the-planet-of-the-apes
"""
TITLE_RE = r"^(?:https?://(?:www\.)?ctv\.ca(?:/[a-z]{2})?)?/(?P<type>movies|shows)/(?P<id>[a-z0-9-]+)(?:/(?P<episode>[a-z0-9-]+))?$"
GEOFENCE = ("ca",)
@staticmethod
@click.command(name="CTV", short_help="https://www.ctv.ca", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx, **kwargs):
return CTV(ctx, **kwargs)
def __init__(self, ctx, title):
self.title = title
super().__init__(ctx)
self.authorization: str = None
self.api = self.config["endpoints"]["api"]
self.license_url = self.config["endpoints"]["license"]
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if credential:
cache = self.cache.get(f"tokens_{credential.sha1}")
if cache and not cache.expired:
# cached
self.log.info(" + Using cached Tokens...")
tokens = cache.data
elif cache and cache.expired:
# expired, refresh
self.log.info("Refreshing cached Tokens")
r = self.session.post(
self.config["endpoints"]["login"],
headers={"authorization": f"Basic {self.config['endpoints']['auth']}"},
data={
"grant_type": "refresh_token",
"username": credential.username,
"password": credential.password,
"refresh_token": cache.data["refresh_token"],
},
)
try:
res = r.json()
except json.JSONDecodeError:
raise ValueError(f"Failed to refresh tokens: {r.text}")
tokens = res
self.log.info(" + Refreshed")
else:
# new
r = self.session.post(
self.config["endpoints"]["login"],
headers={"authorization": f"Basic {self.config['endpoints']['auth']}"},
data={
"grant_type": "password",
"username": credential.username,
"password": credential.password,
},
)
try:
res = r.json()
except json.JSONDecodeError:
raise ValueError(f"Failed to log in: {r.text}")
tokens = res
self.log.info(" + Acquired tokens...")
cache.set(tokens, expiration=tokens["expires_in"])
self.authorization = f"Bearer {tokens['access_token']}"
def search(self) -> Generator[SearchResult, None, None]:
payload = {
"operationName": "searchMedia",
"variables": {"title": f"{self.title}"},
"query": """
query searchMedia($title: String!) {searchMedia(titleMatches: $title) {
... on Medias {page {items {title\npath}}}}}, """,
}
r = self.session.post(self.config["endpoints"]["search"], json=payload)
if r.status_code != 200:
self.log.error(r.text)
return
for result in r.json()["data"]["searchMedia"]["page"]["items"]:
yield SearchResult(
id_=result.get("path"),
title=result.get("title"),
description=result.get("description"),
label=result["path"].split("/")[1],
url="https://www.ctv.ca" + result.get("path"),
)
def get_titles(self) -> Titles_T:
title, kind, episode = (re.match(self.TITLE_RE, self.title).group(i) for i in ("id", "type", "episode"))
title_path = self.get_title_id(kind, title, episode)
if episode is not None:
data = self.get_episode_data(title_path)
return Series(
[
Episode(
id_=data["axisId"],
service=self.__class__,
title=data["axisMedia"]["title"],
season=int(data["seasonNumber"]),
number=int(data["episodeNumber"]),
name=data["title"],
year=data.get("firstAirYear"),
language=data["axisPlaybackLanguages"][0].get("language", "en"),
data=data["axisPlaybackLanguages"][0]["destinationCode"],
)
]
)
if kind == "shows":
data = self.get_series_data(title_path)
titles = self.fetch_episodes(data["contentData"]["seasons"])
return Series(
[
Episode(
id_=episode["axisId"],
service=self.__class__,
title=data["contentData"]["title"],
season=int(episode["seasonNumber"]),
number=int(episode["episodeNumber"]),
name=episode["title"],
year=data["contentData"]["firstAirYear"],
language=episode["axisPlaybackLanguages"][0].get("language", "en"),
data=episode["axisPlaybackLanguages"][0]["destinationCode"],
)
for episode in titles
]
)
if kind == "movies":
data = self.get_movie_data(title_path)
return Movies(
[
Movie(
id_=data["contentData"]["firstPlayableContent"]["axisId"],
service=self.__class__,
name=data["contentData"]["title"],
year=data["contentData"]["firstAirYear"],
language=data["contentData"]["firstPlayableContent"]["axisPlaybackLanguages"][0].get(
"language", "en"
),
data=data["contentData"]["firstPlayableContent"]["axisPlaybackLanguages"][0]["destinationCode"],
)
]
)
def get_tracks(self, title: Title_T) -> Tracks:
content = "https://capi.9c9media.com/destinations/{}/platforms/desktop/contents/{}/contentPackages".format(
title.data, title.id
)
params = {
"$include": "[Desc,Constraints,EndCreditOffset,Breaks,Stacks.ManifestHost.mpd]",
}
r = self.session.get(content, params=params)
r.raise_for_status()
pkg_id = r.json()["Items"][0]["Id"]
manifest = f"{content}/{pkg_id}/manifest.mpd"
subtitle = f"{content}/{pkg_id}/manifest.vtt"
if self.authorization:
self.session.headers.update({"authorization": self.authorization})
tracks = Tracks()
for num in ["14", "3", "25", "fe&mca=true&mta=true"]:
version = DASH.from_url(url=f"{manifest}?filter={num}", session=self.session).to_tracks(language=title.language)
tracks.videos.extend(version.videos)
tracks.audio.extend(version.audio)
tracks.add(
Subtitle(
id_=hashlib.md5(subtitle.encode()).hexdigest()[0:6],
url=subtitle,
codec=Subtitle.Codec.from_mime(subtitle[-3:]),
language=title.language,
is_original_lang=True,
forced=False,
sdh=True,
)
)
return tracks
def get_chapters(self, title: Title_T) -> list[Chapter]:
return [] # Chapters not available
def get_widevine_service_certificate(self, **_: Any) -> str:
return WidevineCdm.common_privacy_cert
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
r = self.session.post(url=self.license_url, data=challenge)
if r.status_code != 200:
self.log.error(r.text)
sys.exit(1)
return r.content
# service specific functions
def get_title_id(self, kind: str, title: tuple, episode: str) -> str:
if episode is not None:
title += f"/{episode}"
payload = {
"operationName": "resolvePath",
"variables": {"path": f"{kind}/{title}"},
"query": """
query resolvePath($path: String!) {
resolvedPath(path: $path) {
lastSegment {
content {
id
}
}
}
}
""",
}
r = self.session.post(self.api, json=payload).json()
return r["data"]["resolvedPath"]["lastSegment"]["content"]["id"]
def get_series_data(self, title_id: str) -> json:
payload = {
"operationName": "axisMedia",
"variables": {"axisMediaId": f"{title_id}"},
"query": """
query axisMedia($axisMediaId: ID!) {
contentData: axisMedia(id: $axisMediaId) {
title
description
originalSpokenLanguage
mediaType
firstAirYear
seasons {
title
id
seasonNumber
}
}
}
""",
}
return self.session.post(self.api, json=payload).json()["data"]
def get_movie_data(self, title_id: str) -> json:
payload = {
"operationName": "axisMedia",
"variables": {"axisMediaId": f"{title_id}"},
"query": """
query axisMedia($axisMediaId: ID!) {
contentData: axisMedia(id: $axisMediaId) {
title
description
firstAirYear
firstPlayableContent {
axisId
axisPlaybackLanguages {
destinationCode
}
}
}
}
""",
}
return self.session.post(self.api, json=payload).json()["data"]
def get_episode_data(self, title_path: str) -> json:
payload = {
"operationName": "axisContent",
"variables": {"id": f"{title_path}"},
"query": """
query axisContent($id: ID!) {
axisContent(id: $id) {
axisId
title
description
contentType
seasonNumber
episodeNumber
axisMedia {
title
}
axisPlaybackLanguages {
language
destinationCode
}
}
}
""",
}
return self.session.post(self.api, json=payload).json()["data"]["axisContent"]
def fetch_episode(self, episode: str) -> json:
payload = {
"operationName": "season",
"variables": {"seasonId": f"{episode}"},
"query": """
query season($seasonId: ID!) {
axisSeason(id: $seasonId) {
episodes {
axisId
title
description
contentType
seasonNumber
episodeNumber
axisPlaybackLanguages {
language
destinationCode
}
}
}
}
""",
}
response = self.session.post(self.api, json=payload)
return response.json()["data"]["axisSeason"]["episodes"]
def fetch_episodes(self, data: dict) -> list:
"""TODO: Switch to async once https proxies are fully supported"""
with ThreadPoolExecutor(max_workers=10) as executor:
tasks = [executor.submit(self.fetch_episode, x["id"]) for x in data]
titles = [future.result() for future in as_completed(tasks)]
return [episode for episodes in titles for episode in episodes]
@@ -1,6 +0,0 @@
endpoints:
login: https://account.bellmedia.ca/api/login/v2.1
auth: Y3R2LXdlYjpkZWZhdWx0
api: https://api.ctv.ca/space-graphql/graphql
license: https://license.9c9media.ca/widevine
search: https://www.ctv.ca/space-graphql/apq/graphql
@@ -1,266 +0,0 @@
from __future__ import annotations
import json
import re
from collections.abc import Generator
from datetime import timedelta
from typing import Any
from urllib.parse import quote, urljoin
import click
from click import Context
from lxml import etree
from requests import Request
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks
class CWTV(Service):
"""
\b
Service code for CWTV streaming service (https://www.cwtv.com/).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: None
Geofence: US (API and downloads)
Robustness:
L3: 1080p, AAC2.0
\b
Tips:
- Input should be complete URL:
SHOW: https://www.cwtv.com/shows/sullivans-crossing
EPISODE: https://www.cwtv.com/series/sullivans-crossing/new-beginnings/?play=7778f443-c7cc-4843-8e3c-d97d53b813d2
MOVIE: https://www.cwtv.com/movies/burnt/
"""
GEOFENCE = ("us",)
ALIASES = ("cw",)
@staticmethod
@click.command(name="CWTV", short_help="https://www.cwtv.com/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> CWTV:
return CWTV(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.session.headers.update(self.config["headers"])
def search(self) -> Generator[SearchResult, None, None]:
results = self._request(
"GET", "https://www.cwtv.com/search/",
params={
"q": quote(self.title),
"format": "json2",
"service": "t",
"cwuid": "8195356001251527455",
},
)
for result in results["items"]:
if result.get("type") not in ("shows", "series", "movies"):
continue
video_type = "shows" if result.get("type") in ("series", "shows") else "movies"
yield SearchResult(
id_=f"https://www.cwtv.com/{video_type}/{result.get('show_slug')}",
title=result.get("title"),
description=result.get("description_long"),
label=result.get("type").capitalize(),
url=f"https://www.cwtv.com/{video_type}/{result.get('show_slug')}",
)
def get_titles(self) -> Movies | Series:
url_pattern = re.compile(
r"^https:\/\/www\.cwtv\.com\/"
r"(?P<type>series|shows|movies)\/"
r"(?P<id>[\w-]+(?:\/[\w-]+)?)"
r"(?:\/?\?play=(?P<play_id>[\w-]+))?"
)
match = url_pattern.match(self.title)
if not match:
raise ValueError(f"Could not parse ID from title: {self.title}")
kind, guid, play_id = (match.group(i) for i in ("type", "id", "play_id"))
if kind in ("series", "shows") and not play_id:
episodes = self._series(guid)
return Series(episodes)
elif kind == "movies" and not play_id:
movie = self._movie(guid)
return Movies(movie)
elif kind in ("series", "shows") and play_id:
episode = self._episode(play_id)
return Series(episode)
else:
raise ValueError(f"Could not parse conent type from title: {self.title}")
def get_tracks(self, title: Movie | Episode) -> Tracks:
data = self._request(
"GET", self.config["endpoints"]["playback"].format(title.id),
headers={"accept": f'application/json;pk={self.config["policy_key"]}'},
)
has_drm = data.get("custom_fields", {}).get("is_drm") == "1"
title.data["chapters"] = data.get("cue_points")
source_manifest = next(
(source.get("src") for source in data["sources"] if source.get("type") == "application/dash+xml"),
None,
)
if not source_manifest:
raise ValueError("Could not find DASH manifest")
license_url = next((
source.get("key_systems", {}).get("com.widevine.alpha", {}).get("license_url")
for source in data["sources"] if source.get("src") == source_manifest),
None,
)
if has_drm and not license_url:
raise ValueError("Could not find license URL")
title.data["license_url"] = license_url
manifest = self.trim_duration(source_manifest)
tracks = DASH.from_text(manifest, source_manifest).to_tracks(language="en")
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Movie | Episode) -> Chapters:
if not title.data.get("chapters"):
return Chapters()
chapters = []
for cue in title.data["chapters"]:
if cue["time"] > 0:
chapters.append(Chapter(timestamp=cue["time"]))
return Chapters(chapters)
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, *, challenge: bytes, title: Movie | Episode, track: Any) -> bytes | str | None:
if license_url := title.data.get("license_url"):
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
return None
# Service specific
def _series(self, guid: str) -> list[Episode]:
series = self._request("GET", f"/feed/app-2/videos/show_{guid}/type_episodes/apiversion_24/device_androidtv")
if not series.get("items"):
raise ValueError(f"Could not find any episodes with ID {guid}")
episodes = [
Episode(
id_=episode.get("bc_video_id"),
service=self.__class__,
name=episode.get("title"),
season=int(episode.get("season") or 0),
number=int(episode.get("episode_in_season") or 0),
title=episode.get("series_name") or episode.get("show_title"),
year=episode.get("release_year"),
data=episode,
)
for episode in series.get("items")
if episode.get("fullep", 0) == 1
]
return episodes
def _movie(self, guid: str) -> Movie:
data = self._request("GET", f"/feed/app-2/videos/show_{guid}/type_episodes/apiversion_24/device_androidtv")
if not data.get("items"):
raise ValueError(f"Could not find any data for ID {guid}")
movies = [
Movie(
id_=movie.get("bc_video_id"),
service=self.__class__,
name=movie.get("series_name") or movie.get("show_title"),
year=movie.get("release_year"),
data=movie,
)
for movie in data.get("items")
if movie.get("fullep", 0) == 1
]
return movies
def _episode(self, guid: str) -> Episode:
data = self._request("GET", f"/feed/app-2/video-meta/guid_{guid}/apiversion_24/device_androidtv")
if not data.get("video"):
raise ValueError(f"Could not find any data for ID {guid}")
episodes = [
Episode(
id_=data.get("video", {}).get("bc_video_id"),
service=self.__class__,
name=data.get("video", {}).get("title"),
season=int(data.get("video", {}).get("season") or 0),
number=int(data.get("video", {}).get("episode_in_season") or 0),
title=data.get("video", {}).get("series_name") or data.get("video", {}).get("show_title"),
year=data.get("video", {}).get("release_year"),
data=data.get("video"),
)
]
return episodes
def _request(self, method: str, endpoint: str, **kwargs: Any) -> Any[dict | str]:
url = urljoin(self.config["endpoints"]["base_url"], endpoint)
prep = self.session.prepare_request(Request(method, url, **kwargs))
response = self.session.send(prep)
if response.status_code != 200:
raise ConnectionError(f"{response.text}")
try:
return json.loads(response.content)
except json.JSONDecodeError:
return response.text
@staticmethod
def trim_duration(source_manifest: str) -> str:
"""
The last segment on all tracks return a 404 for some reason, causing a failed download.
So we trim the duration by exactly one segment to account for that.
TODO: Calculate the segment duration instead of assuming length.
"""
manifest = DASH.from_url(source_manifest).manifest
period_duration = manifest.get("mediaPresentationDuration")
period_duration = DASH.pt_to_sec(period_duration)
hours, minutes, seconds = str(timedelta(seconds=period_duration - 6)).split(":")
new_duration = f"PT{hours}H{minutes}M{seconds}S"
manifest.set("mediaPresentationDuration", new_duration)
return etree.tostring(manifest, encoding="unicode")
@@ -1,9 +0,0 @@
headers:
User-Agent: Mozilla/5.0 (Linux; Android 11; Smart TV Build/AR2101; wv)
endpoints:
base_url: https://images.cwtv.com
playback: https://edge.api.brightcove.com/playback/v1/accounts/6415823816001/videos/{}
policy_key: BCpkADawqM0t2qFXB_K2XdHv2JmeRgQjpP6De9_Fl7d4akhL5aeqYwErorzsAxa7dyOF2FdxuG5wWVOREHEwb0DI-M8CGBBDpqwvDBEPfDKQg7kYGnccdNDErkvEh2O28CrGR3sEG6MZBlZ03I0xH7EflYKooIhfwvNWWw
@@ -1,583 +0,0 @@
from __future__ import annotations
import json
import re
import uuid
from collections import defaultdict
from collections.abc import Generator
from copy import deepcopy
from http.cookiejar import CookieJar
from typing import Any
from urllib.parse import urljoin
from zlib import crc32
import click
from click import Context
from langcodes import Language
from lxml import etree
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.session import session as CurlSession
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Audio, Chapter, Chapters, Subtitle, Track, Tracks
from envied.core.utilities import is_close_match
class DSCP(Service):
"""
\b
Service code for Discovery Plus streaming service (https://www.discoveryplus.com).
Credit to @sp4rk.y for the subtitle fix.
\b
Version: 1.0.0
Author: stabbedbybrick
Authorization: Cookies
Robustness:
Widevine:
L1: 2160p, 1080p
L3: 720p
PlayReady:
SL3000: 2160p
SL2000: 1080p, 720p
\b
Tips:
- Input examples:
SHOW: https://play.discoveryplus.com/show/eb26e00e-9582-4790-a61c-48d785926f58
STANDALONE: https://play.discoveryplus.com/standalone/5012ae3f-d9bd-46ec-ad42-b8116b811441
SPORT: https://play.discoveryplus.com/sport/9cc449de-2a64-524d-bcb6-cabd4ac70340
EPISODE: https://play.discoveryplus.com/video/watch/8685efdd-a3c4-4892-b1d1-5f9f071cacf1/de67ea8e-a90f-4609-81af-4f09906f60b2
\b
Notes:
- Language tags can be mislabelled or missing on some titles. List tracks with --list to verify.
- All qualities, codecs, and ranges are included when available. Use -v H.265, -r HDR10, -q 1080p, etc. to select.
\b
Bonus tip: With some minor adjustments to the code and config, you can convert this to an HMAX service.
- Replace all instances of "DSCP" with "HMAX"
- Replace all instances of "dplus" with "beam"
- Replace all instances of "discoveryplus" with "hbomax"
"""
ALIASES = ("discoveryplus",)
TITLE_RE = (
r"^(?:https?://play.discoveryplus\.com?)?/(?P<type>show|mini-series|video|movie|topical|standalone|sport)/(?P<id>[a-z0-9-/]+)"
)
@staticmethod
@click.command(name="DSCP", short_help="https://www.discoveryplus.com/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> DSCP:
return DSCP(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
super().__init__(ctx)
self.title = title
self.profile = ctx.parent.params.get("profile")
if not self.profile:
self.profile = "default"
self.cdm = ctx.obj.cdm
if self.cdm is not None:
self.drm_system = "playready"
self.security_level = "SL3000"
if self.cdm.security_level <= 3:
self.drm_system = "widevine"
self.security_level = "L1"
def get_session(self) -> CurlSession:
return CurlSession("okhttp4", status_forcelist=[429, 502, 503, 504])
def authenticate(self, cookies: CookieJar | None = None, credential: Credential | None = None) -> None:
super().authenticate(cookies, credential)
if not cookies:
raise EnvironmentError("Service requires Cookies for Authentication.")
cache = self.cache.get(f"tokens_{self.profile}")
if cache:
self.log.info(" + Using cached Tokens...")
tokens = cache.data
else:
self.log.info(" + Setting up new profile...")
st_token = self.session.cookies.get_dict().get("st")
if not st_token:
raise ValueError("- Unable to find token in cookies, try refreshing.")
profile = {"token": st_token, "device_id": str(uuid.uuid1())}
cache.set(profile)
tokens = cache.data
self.device_id = tokens["device_id"]
client_id = self.config["client_id"]
self.session.headers.update({
"user-agent": "androidtv dplus/20.8.1.2 (android/9; en-US; SHIELD Android TV-NVIDIA; Build/1)",
"x-disco-client": "ANDROIDTV:9:dplus:20.8.1.2",
"x-disco-params": "realm=bolt,bid=dplus,features=ar",
"x-device-info": f"dplus/20.8.1.2 (NVIDIA/SHIELD Android TV; android/9-mdarcy; {self.device_id}/{client_id})",
})
self.base_url = self.config["endpoints"]["base_url"].format("any", "any")
access = self._request("GET", "/token", params={"realm": "bolt", "deviceId": self.device_id})
self.access_token = access["data"]["attributes"]["token"]
config = self._request("POST", "/session-context/headwaiter/v1/bootstrap")
self.base_url = self.config["endpoints"]["base_url"].format(config["routing"]["tenant"], config["routing"]["homeMarket"])
user = self._request("GET", "/users/me")
if user["data"]["attributes"]["anonymous"]:
raise ValueError("Anonymous user - try refreshing cookies.")
def search(self) -> Generator[SearchResult, None, None]:
params = {
"include": "default",
"decorators": "viewingHistory,isFavorite,playbackAllowed,contentAction,badges",
"contentFilter[query]": self.title,
"page[items.number]": "1",
"page[items.size]": "8",
}
data = self._request("GET", "/cms/routes/search/result", params=params)
results = [x.get("attributes") for x in data["included"] if x.get("type") == "show"]
for result in results:
yield SearchResult(
id_=f"https://play.discoveryplus.com/show/{result.get('alternateId')}",
title=result.get("name"),
description=result.get("description"),
label="show",
url=f"https://play.discoveryplus.com/show/{result.get('alternateId')}",
)
def get_titles(self) -> Movies | Series:
try:
entity, content_id = (re.match(self.TITLE_RE, self.title).group(i) for i in ("type", "id"))
except Exception:
raise ValueError("Could not parse ID from title - is the URL correct?")
if entity in ("show", "mini-series", "topical"):
episodes = self._show(content_id)
return Series(episodes)
elif entity in ("movie", "standalone"):
movie = self._movie(content_id, entity)
return Movies(movie)
elif entity == "sport":
sport = self._sport(content_id)
return Movies(sport)
elif entity == "video":
episodes = self._episode(content_id)
return Series(episodes)
else:
raise ValueError(f"Unknown content: {entity}")
def get_tracks(self, title: Movie | Episode) -> Tracks:
payload = {
"appBundle": "com.wbd.stream",
"applicationSessionId": self.device_id,
"capabilities": {
"codecs": {
"audio": {
"decoders": [
{"codec": "aac", "profiles": ["lc", "he", "hev2", "xhe"]},
{"codec": "eac3", "profiles": ["atmos"]},
]
},
"video": {
"decoders": [
{
"codec": "h264",
"levelConstraints": {
"framerate": {"max": 60, "min": 0},
"height": {"max": 2160, "min": 48},
"width": {"max": 3840, "min": 48},
},
"maxLevel": "5.2",
"profiles": ["baseline", "main", "high"],
},
{
"codec": "h265",
"levelConstraints": {
"framerate": {"max": 60, "min": 0},
"height": {"max": 2160, "min": 144},
"width": {"max": 3840, "min": 144},
},
"maxLevel": "5.1",
"profiles": ["main10", "main"],
},
],
"hdrFormats": ["hdr10", "hdr10plus", "dolbyvision", "dolbyvision5", "dolbyvision8", "hlg"],
},
},
"contentProtection": {
"contentDecryptionModules": [
{"drmKeySystem": self.drm_system, "maxSecurityLevel": self.security_level}
]
},
"manifests": {"formats": {"dash": {}}},
},
"consumptionType": "streaming",
"deviceInfo": {
"player": {
"mediaEngine": {"name": "", "version": ""},
"playerView": {"height": 2160, "width": 3840},
"sdk": {"name": "", "version": ""},
}
},
"editId": title.id,
"firstPlay": False,
"gdpr": False,
"playbackSessionId": str(uuid.uuid4()),
"userPreferences": {
#'uiLanguage': 'en'
},
}
playback = self._request(
"POST", "/playback-orchestrator/any/playback-orchestrator/v1/playbackInfo",
headers={"Authorization": f"Bearer {self.access_token}"},
json=payload,
)
original_language = next((
x.get("language")
for x in playback["videos"][0]["audioTracks"]
if "Original" in x.get("displayName", "")
), "")
manifest = (
playback.get("fallback", {}).get("manifest", {}).get("url", "").replace("_fallback", "")
or playback.get("manifest", {}).get("url")
)
license_url = (
playback.get("fallback", {}).get("drm", {}).get("schemes", {}).get(self.drm_system, {}).get("licenseUrl")
or playback.get("drm", {}).get("schemes", {}).get(self.drm_system, {}).get("licenseUrl")
)
title.data["license_url"] = license_url
title.data["chapters"] = next((x.get("annotations") for x in playback["videos"] if x["type"] == "main"), None)
dash = DASH.from_url(url=manifest, session=self.session)
tracks = dash.to_tracks(language="en", period_filter=self._period_filter)
for track in tracks:
track.is_original_lang = str(track.language) == original_language
track.name = "Original" if track.is_original_lang else track.name
if isinstance(track, Audio):
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
if isinstance(track, Subtitle):
tracks.subtitles.remove(track)
subtitles = self._process_subtitles(dash, original_language)
tracks.add(subtitles)
return tracks
def get_chapters(self, title: Movie | Episode) -> Chapters:
if not title.data.get("chapters"):
return Chapters()
chapters = []
for chapter in title.data["chapters"]:
if "recap" in chapter.get("secondaryType", "").lower():
chapters.append(Chapter(name="Recap", timestamp=chapter["start"]))
if chapter.get("end"):
chapters.append(Chapter(timestamp=chapter.get("end")))
if "intro" in chapter.get("secondaryType", "").lower():
chapters.append(Chapter(name="Intro", timestamp=chapter["start"]))
if chapter.get("end"):
chapters.append(Chapter(timestamp=chapter.get("end")))
elif "credits" in chapter.get("type", "").lower():
chapters.append(Chapter(name="Credits", timestamp=chapter["start"]))
if not any(c.timestamp == 0 for c in chapters):
chapters.append(Chapter(timestamp=0))
return sorted(chapters, key=lambda x: x.timestamp)
def get_widevine_service_certificate(self, challenge: bytes, title: Episode | Movie, **_: Any) -> str:
if not (license_url := title.data.get("license_url")):
return None
return self.session.post(url=license_url, data=challenge).content
def get_widevine_license(self, *, challenge: bytes, title: Episode | Movie, track: Any) -> bytes | str | None:
if not (license_url := title.data.get("license_url")):
return None
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.status_code, r.text)
return r.content
def get_playready_license(self, *, challenge: bytes, title: Episode | Movie, track: Any) -> bytes | str | None:
if not (license_url := title.data.get("license_url")):
return None
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.status_code, r.text)
return r.content
# Service specific functions
@staticmethod
def _process_subtitles(dash: DASH, language: str) -> list[Subtitle]:
subtitle_groups = defaultdict(list)
manifest = dash.manifest
for period in manifest.findall("Period"):
for adapt_set in period.findall("AdaptationSet"):
if adapt_set.get("contentType") != "text" or not adapt_set.get("lang"):
continue
role = adapt_set.find("Role")
label = adapt_set.find("Label")
key = (
adapt_set.get("lang"),
role.get("value") if role is not None else "subtitle",
label.text if label is not None else "",
)
subtitle_groups[key].append((period, adapt_set))
final_tracks = []
for (lang, role_value, label_text), adapt_set_group in subtitle_groups.items():
first_period, first_adapt = adapt_set_group[0]
if first_adapt.find("Representation") is None:
continue
s_elements_with_context = []
for _, adapt_set in adapt_set_group:
rep = adapt_set.find("Representation")
if rep is None:
continue
template = rep.find("SegmentTemplate") or adapt_set.find("SegmentTemplate")
timeline = template.find("SegmentTimeline") if template is not None else None
if timeline is not None:
start_num = int(template.get("startNumber", 1))
s_elements_with_context.extend((start_num, s_elem) for s_elem in timeline.findall("S"))
s_elements_with_context.sort(key=lambda x: x[0])
combined_adapt = deepcopy(first_adapt)
combined_rep = combined_adapt.find("Representation")
seg_template = combined_rep.find("SegmentTemplate")
if seg_template is None:
template_at_adapt = combined_adapt.find("SegmentTemplate")
if template_at_adapt is not None:
seg_template = deepcopy(template_at_adapt)
combined_rep.append(seg_template)
combined_adapt.remove(template_at_adapt)
else:
continue
if seg_template.find("SegmentTimeline") is not None:
seg_template.remove(seg_template.find("SegmentTimeline"))
new_timeline = etree.Element("SegmentTimeline")
new_timeline.extend(deepcopy(s) for _, s in s_elements_with_context)
seg_template.append(new_timeline)
seg_template.set("startNumber", "1")
if "endNumber" in seg_template.attrib:
del seg_template.attrib["endNumber"]
track_id = hex(crc32(f"sub-{lang}-{role_value}-{label_text}".encode()) & 0xFFFFFFFF)[2:]
lang_obj = Language.get(lang)
track_name = "Original" if (language and is_close_match(lang_obj, [language])) else lang_obj.display_name()
final_tracks.append(
Subtitle(
id_=track_id,
url=dash.url,
codec=Subtitle.Codec.WebVTT,
language=lang_obj,
is_original_lang=bool(language and is_close_match(lang_obj, [language])),
descriptor=Track.Descriptor.DASH,
sdh="sdh" in label_text.lower() or role_value == "caption",
forced="forced" in label_text.lower() or "forced" in role_value.lower(),
name=track_name,
data={
"dash": {
"manifest": manifest,
"period": first_period,
"adaptation_set": combined_adapt,
"representation": combined_rep,
}
},
)
)
return final_tracks
@staticmethod
def _period_filter(period: Any) -> bool:
"""Shouldn't be needed for fallback manifest"""
if not (duration := period.get("duration")):
return False
return DASH.pt_to_sec(duration) < 120
def _show(self, title: str) -> Episode:
params = {
"include": "default",
"decorators": "viewingHistory,badges,isFavorite,contentAction",
}
data = self._request("GET", "/cms/routes/show/{}".format(title), params=params)
info = next(x for x in data["included"] if x.get("attributes", {}).get("alternateId", "") == title)
content = next((x for x in data["included"] if "show-page-rail-episodes-tabbed-content" in x["attributes"].get("alias", "")), None)
if not content:
raise ValueError("Show not found")
content_id = content.get("id")
show_id = content["attributes"]["component"].get("mandatoryParams", "")
season_params = [x.get("parameter") for x in content["attributes"]["component"]["filters"][0]["options"]]
page = next(x for x in data["included"] if x.get("type", "") == "page")
seasons = [
self._request(
"GET", "/cms/collections/{}?{}&{}".format(content_id, season, show_id),
params={"include": "default", "decorators": "viewingHistory,badges,isFavorite,contentAction"},
)
for season in season_params
]
videos = [[x for x in season["included"] if x["type"] == "video"] for season in seasons]
return [
Episode(
id_=ep["relationships"]["edit"]["data"]["id"],
service=self.__class__,
title=page["attributes"].get("title") or info["attributes"].get("originalName"),
year=ep["attributes"]["airDate"][:4] if ep["attributes"].get("airDate") else None,
season=ep["attributes"].get("seasonNumber"),
number=ep["attributes"].get("episodeNumber"),
name=ep["attributes"]["name"],
data=ep,
)
for episodes in videos
for ep in episodes
if ep.get("attributes", {}).get("videoType", "") == "EPISODE"
]
def _episode(self, title: str) -> Episode:
video_id = title.split("/")[1]
params = {"decorators": "isFavorite", "include": "show"}
content = self._request("GET", "/content/videos/{}".format(video_id), params=params)
episode = content.get("data", {}).get("attributes")
video_type = episode.get("videoType")
relationships = content.get("data", {}).get("relationships")
show = next((x for x in content["included"] if x.get("type", "") == "show"), {})
show_title = show.get("attributes", {}).get("name") or show.get("attributes", {}).get("originalName")
episode_name = episode.get("originalName") or episode.get("secondaryTitle")
if video_type.lower() in ("clip", "standalone_event"):
show_title = episode.get("originalName")
episode_name = episode.get("secondaryTitle", "")
return [
Episode(
id_=relationships.get("edit", {}).get("data", {}).get("id"),
service=self.__class__,
title=show_title,
year=int(episode.get("airDate")[:4]) if episode.get("airDate") else None,
season=episode.get("seasonNumber") or 0,
number=episode.get("episodeNumber") or 0,
name=episode_name,
data=episode,
)
]
def _sport(self, title: str) -> Movie:
params = {
"include": "default",
"decorators": "viewingHistory,badges,isFavorite,contentAction",
}
data = self._request("GET", "/cms/routes/sport/{}".format(title), params=params)
content = next((x for x in data["included"] if x.get("attributes", {}).get("alternateId", "") == title), None)
if not content:
raise ValueError(f"Content not found for title: {title}")
movie = content.get("attributes")
relationships = content.get("relationships")
name = movie.get("name") or movie.get("originalName")
year = int(movie.get("firstAvailableDate")[:4]) if movie.get("firstAvailableDate") else None
return [
Movie(
id_=relationships.get("edit", {}).get("data", {}).get("id"),
service=self.__class__,
name=name + " - " + movie.get("secondaryTitle", ""),
year=year,
data=movie,
)
]
def _movie(self, title: str, entity: str) -> Movie:
params = {
"include": "default",
"decorators": "isFavorite,playbackAllowed,contentAction,badges",
}
data = self._request("GET", "/cms/routes/movie/{}".format(title), params=params)
movie = next((
x for x in data["included"]if x.get("attributes", {}).get("videoType", "").lower() == entity), None
)
if not movie:
raise ValueError("Movie not found")
return [
Movie(
id_=movie["relationships"]["edit"]["data"]["id"],
service=self.__class__,
name=movie["attributes"].get("name") or movie["attributes"].get("originalName"),
year=int(movie["attributes"]["airDate"][:4]) if movie["attributes"].get("airDate") else None,
data=movie,
)
]
def _request(self, method: str, endpoint: str, **kwargs: Any) -> Any[dict | str]:
url = urljoin(self.base_url, endpoint)
response = self.session.request(method, url, **kwargs)
try:
data = json.loads(response.content)
if data.get("errors"):
raise ConnectionError(data["errors"])
return data
except Exception as e:
raise ConnectionError(f"Request failed for {url}: {e}")
@@ -1,4 +0,0 @@
endpoints:
base_url: "https://default.{}-{}.prd.api.discoveryplus.com"
client_id: "b6746ddc-7bc7-471f-a16c-f6aaf0c34d26" # androidtv
@@ -1,657 +0,0 @@
from __future__ import annotations
import re
import sys
import uuid
from collections.abc import Generator
from http.cookiejar import CookieJar
from typing import Any, Optional, Union
import click
from click import Context
from requests import Request
#from envied.core.downloaders import n_m3u8dl_re
from envied.core.credential import Credential
from envied.core.manifests import HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks, Video
from envied.core.utils.collections import as_list
from . import queries
class DSNP(Service):
"""
\b
Service code for DisneyPlus streaming service (https://www.disneyplus.com).
\b
Authorization: Credentials
Robustness:
Widevine:
L1: 2160p, 1080p
L3: 720p
PlayReady:
SL3: 2160p, 1080p
\b
Tips:
- Input should be only the entity ID for both series and movies:
MOVIE: entity-99e15d53-926e-4074-b9f4-6524d10c8bed
SERIES: entity-30429ad6-dd12-41bf-924e-19131fa66bb5
- Use the --lang LANG_RANGE option to request non-english tracks
- CDM level dictates playback quality (L3 == 720p, L1 == 1080p, 2160p)
\b
Notes:
- On first run, the program will look for the first account profile that doesn't
have kids mode or pin protection enabled. If none are found, the program will exit.
- The profile will be cached and re-used until cache is cleared.
"""
@staticmethod
@click.command(name="DSNP", short_help="https://www.disneyplus.com", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> DSNP:
return DSNP(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.cdm = ctx.obj.cdm
self.playback_data = {}
vcodec = ctx.parent.params.get("vcodec")
range = ctx.parent.params.get("range_")
self.range = range[0].name if range else "SDR"
self.vcodec = "H265" if vcodec and vcodec == Video.Codec.HEVC else "H264"
if self.range != "SDR" and self.vcodec != "H265":
self.vcodec = "H265"
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
self.session.headers.update(self.config["HEADERS"])
self.session.headers.update({"x-bamsdk-transaction-id": str(uuid.uuid4())})
self.prd_config = self.session.get(self.config["CONFIG_URL"]).json()
self._cache = self.cache.get(f"tokens_{credential.sha1}")
if self._cache:
self.log.info(" + Refreshing Tokens")
profile = self.refresh_token(self._cache.data["token"]["refreshToken"])
self._cache.set(profile, expiration=profile["token"]["expiresIn"] - 30)
token = self._cache.data["token"]["accessToken"]
self.session.headers.update({"Authorization": "Bearer {}".format(token)})
self.active_session = self.account()["activeSession"]
else:
self.log.info(" + Setting up new profile...")
token = self.register_device()
status = self.check_email(credential.username, token)
if status.lower() == "register":
raise ValueError("Account is not registered. Please register first.")
elif status.lower() == "otp":
self.log.error(" - Account requires passcode for login.")
sys.exit(1)
else:
tokens = self.login(credential.username, credential.password, token)
self.session.headers.update({"Authorization": "Bearer {}".format(tokens["accessToken"])})
account = self.account()
profile_id = next(
(
x.get("id")
for x in account["account"]["profiles"]
if not x["attributes"]["kidsModeEnabled"]
and not x["attributes"]["parentalControls"]["isPinProtected"]
),
None,
)
if not profile_id:
self.log.error(
" - Missing profile - you need at least one profile with kids mode and pin protection disabled"
)
sys.exit(1)
set_profile = self.switch_profile(profile_id)
profile = self.refresh_token(set_profile["token"]["refreshToken"])
self._cache.set(profile, expiration=profile["token"]["expiresIn"] - 30)
token = self._cache.data["token"]["accessToken"]
self.session.headers.update({"Authorization": "Bearer {}".format(token)})
self.active_session = self.account()["activeSession"]
self.log.info(" + Acquired tokens...")
def search(self) -> Generator[SearchResult, None, None]:
params = {
"query": self.title,
}
endpoint = self.href(
self.prd_config["services"]["explore"]["client"]["endpoints"]["search"]["href"],
version=self.config["EXPLORE_VERSION"],
)
data = self._request("GET", endpoint, params=params)["data"]["page"]
if not data.get("containers"):
return
results = data["containers"][0]["items"]
for result in results:
entity = "entity-" + result.get("id")
yield SearchResult(
id_=entity,
title=result["visuals"].get("title"),
description=result["visuals"]["description"].get("brief"),
label=result["visuals"]["metastringParts"].get("releaseYearRange", {}).get("startYear"),
url=f"https://www.disneyplus.com/browse/{entity}",
)
def get_titles(self) -> Union[Movies, Series]:
if not self.title.startswith("entity"):
raise ValueError("Invalid input - Use only entity IDs.")
content = self.get_deeplink(self.title)
_type = content["data"]["deeplink"]["actions"][0]["contentType"]
if _type == "movie":
movie = self._movie(self.title)
return Movies(movie)
elif _type == "series":
episodes = self._show(self.title)
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
resource_id = title.data.get("resourceId")
content_id = title.data["partnerFeed"].get("dmcContentId")
content = self.get_video(content_id)
playback = content["video"]["mediaMetadata"]["playbackUrls"][0]["href"]
token = self._refresh()
headers = {
"accept": "application/vnd.media-service+json; version=5",
"authorization": token,
"x-dss-feature-filtering": "true",
}
payload = {
"playbackId": resource_id,
"playback": {
"attributes": {
"codecs": {
"supportsMultiCodecMaster": False,
},
"protocol": "HTTPS",
# "ads": "",
"frameRates": [60],
"assetInsertionStrategy": "SGAI",
"playbackInitializationContext": "ONLINE",
},
},
}
video_ranges = []
audio_types = []
audio_types.append("ATMOS")
audio_types.append("DTS_X")
if not self.cdm.security_level == 3 and self.range == "DV":
video_ranges.append("DOLBY_VISION")
if not self.cdm.security_level == 3 and self.range == "HDR10":
video_ranges.append("HDR10")
if self.vcodec == "H265":
payload["playback"]["attributes"]["codecs"] = {"video": ["h264", "h265"]}
if audio_types:
payload["playback"]["attributes"]["audioTypes"] = audio_types
if video_ranges:
payload["playback"]["attributes"]["videoRanges"] = video_ranges
if self.cdm.security_level == 3:
payload["playback"]["attributes"]["resolution"] = {"max": ["1280x720"]}
scenario = "ctr-regular" if self.cdm.security_level == 3 else "ctr-high"
endpoint = playback.format(scenario=scenario)
res = self._request("POST", endpoint, payload=payload, headers=headers)
self.playback_data[title.id] = self._request(
"POST", f"https://disney.playback.edge.bamgrid.com/v7/playback/{scenario}", payload=payload, headers=headers
)
manifest = res["stream"]["complete"][0]["url"]
tracks = HLS.from_url(url=manifest, session=self.session).to_tracks(language=title.language)
for audio in tracks.audio:
bitrate = re.search(
r"(?<=r/composite_)\d+|\d+(?=_complete.m3u8)",
as_list(audio.url)[0],
)
audio.bitrate = int(bitrate.group()) * 1000
if audio.bitrate == 1000_000:
# DSNP lies about the Atmos bitrate
audio.bitrate = 768_000
for track in tracks:
if track not in tracks.attachments:
track.downloader = "N_m3u8DL-RE"
track.needs_repack = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
"""
Extract chapter information from the title data if available.
Returns chapter markers for intro, credits, and scenes.
"""
chapters = Chapters()
try:
# First try to get chapters from the new API via playback data
if title.id in self.playback_data and "stream" in self.playback_data[title.id]:
playback_res = self.playback_data[title.id]
# Check for editorial markers in playback data
if "editorial" in playback_res.get("stream", {}):
editorial = playback_res["stream"]["editorial"]
# Add "Start" chapter if not already present
if not any(item.get("offsetMillis") == 0 for item in editorial):
chapters.add(Chapter(timestamp=0, name="Start"))
# Map editorial labels to chapter names
mapping = {
"recap_start": "Recap",
"FFER": "Recap", # First Frame Episode Recap
"recap_end": "Scene",
"LFER": "Scene", # Last Frame Episode Recap
"intro_start": "Title Sequence",
"intro_end": "Scene",
"FFEI": "Title Sequence", # First Frame Episode Intro
"LFEI": "Scene", # Last Frame Episode Intro
"FFCB": None, # First Frame Credits Bumper
"LFCB": "Scene", # Last Frame Credits Bumper
"FFEC": "End Credits", # First Frame End Credits
"LFEC": None, # Last Frame End Credits
"up_next": None,
}
# Sort by timestamp to ensure proper scene numbering
editorial.sort(key=lambda x: x.get("offsetMillis", 0))
# Track chapters we've already added by timestamp to avoid duplicates
seen_timestamps = set()
scene_count = 0
for marker in editorial:
if "label" in marker and "offsetMillis" in marker:
timestamp = marker["offsetMillis"]
name = mapping.get(marker["label"])
# Skip if no mapping or already processed timestamp
if not name or timestamp in seen_timestamps:
continue
# Mark this timestamp as seen
seen_timestamps.add(timestamp)
if name == "Scene":
scene_count += 1
name = f"Scene {scene_count}"
chapters.add(Chapter(timestamp=timestamp, name=name))
# If we found chapters in the playback data, return them
if chapters:
return chapters
# If no chapters found in playback data, try the original method
content_id = title.data["partnerFeed"].get("dmcContentId")
content = self.get_video(content_id)
# Check for chapter/milestone data
video_info = content.get("video", {}).get("milestone", {})
if not video_info:
return chapters
# Mapping of milestone types to chapter names
mapping = {
"recap_start": "Recap",
"recap_end": "Scene",
"intro_start": "Title Sequence",
"intro_end": "Scene",
"FFEI": "Title Sequence", # First Frame Episode Intro
"LFEI": "Scene", # Last Frame Episode Intro
"FFCB": None, # First Frame Credits Bumper
"LFCB": "Scene", # Last Frame Credits Bumper
"FFEC": "End Credits", # First Frame End Credits
"LFEC": None, # Last Frame End Credits
"up_next": None,
}
# Flatten the milestone data and sort by start time
flattened = []
for chapter_type, items in video_info.items():
for entry in items:
if "milestoneTime" in entry and entry["milestoneTime"]:
start = entry["milestoneTime"][0]["startMillis"]
flattened.append({"type": chapter_type, "start": start})
flattened.sort(key=lambda x: x["start"])
# Create chapters
chapter_list = []
scene_count = 0
for f in flattened:
name = mapping.get(f["type"])
if not name:
continue
if name == "Scene":
scene_count += 1
name = f"Scene {scene_count}"
chapter_list.append(Chapter(timestamp=f["start"], name=name))
# Add a "Start" chapter at 0 if we have end credits
if "FFEC" in video_info and not any(ch.timestamp == 0 for ch in chapter_list):
chapter_list.insert(0, Chapter(timestamp=0, name="Start"))
# Remove duplicates (same time and name)
prev_time, prev_name = None, None
for ch in chapter_list:
# Convert timestamp to milliseconds for comparison
if isinstance(ch.timestamp, str):
ts_parts = ch.timestamp.split(":")
hour, minute, second = int(ts_parts[0]), int(ts_parts[1]), float(ts_parts[2])
ts_ms = (hour * 3600 + minute * 60 + second) * 1000
else:
ts_ms = ch.timestamp
if prev_time is None or (ts_ms != prev_time and ch.name != prev_name):
chapters.add(ch)
prev_time, prev_name = ts_ms, ch.name
return chapters
except Exception as e:
self.log.warning(f"Failed to extract chapters: {e}")
return chapters
def get_playready_license(self, *, challenge: bytes, title, track) -> bytes:
headers = {
"Authorization": f"Bearer {self._cache.data['token']['accessToken']}",
"Content-Type": "application/octet-stream",
}
r = self.session.post(url=self.config["PLAYREADY_LICENSE"], headers=headers, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
def get_widevine_license(self, *, challenge: bytes, title, track) -> None:
headers = {
"Authorization": f"Bearer {self._cache.data['token']['accessToken']}",
"Content-Type": "application/octet-stream",
}
r = self.session.post(url=self.config["LICENSE"], headers=headers, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
# Service specific functions
def _show(self, title: str) -> Episode:
page = self.get_page(title)
container = next(x for x in page["containers"] if x.get("type") == "episodes")
season_ids = [x.get("id") for x in container["seasons"] if x.get("type") == "season"]
episodes = []
for season in season_ids:
endpoint = self.href(
self.prd_config["services"]["explore"]["client"]["endpoints"]["getSeason"]["href"],
version=self.config["EXPLORE_VERSION"],
seasonId=season,
)
data = self.session.get(endpoint, params={'limit': 999}).json()["data"]["season"]["items"]
episodes.extend(data)
return [
Episode(
id_=episode.get("id"),
service=self.__class__,
title=episode["visuals"].get("title"),
year=episode["visuals"]["metastringParts"].get("releaseYearRange", {}).get("startYear"),
season=int(episode["visuals"].get("seasonNumber", 0)),
number=int(episode["visuals"].get("episodeNumber", 0)),
name=episode["visuals"].get("episodeTitle"),
language=self.get_original_lang(next(x for x in episode["actions"] if x.get("type") == "playback").get("availId")),
data=next(x for x in episode["actions"] if x.get("type") == "playback"),
)
for episode in episodes
if episode.get("type") == "view"
]
def _movie(self, title: str) -> Movie:
movie = self.get_page(title)
playback_action = next(x for x in movie["actions"] if x.get("type") == "playback")
original_lang = self.get_original_lang(playback_action.get("availId"))
return [
Movie(
id_=movie.get("id"),
service=self.__class__,
name=movie["visuals"].get("title"),
year=movie["visuals"]["metastringParts"].get("releaseYearRange", {}).get("startYear"),
language=original_lang,
data=playback_action,
)
]
def get_original_lang(self, availId):
try:
title_lang = self.session.get(f'https://disney.api.edge.bamgrid.com/explore/v1.6/playerExperience/{availId}').json()
original_lang = title_lang["data"]["playerExperience"]["targetLanguage"]
except Exception:
original_lang = "en"
return original_lang
def _request(
self,
method: str,
endpoint: str,
params: dict = None,
headers: dict = None,
payload: dict = None,
) -> Any[dict | str]:
_headers = {**self.session.headers, **(headers or {})}
prep = self.session.prepare_request(Request(method, endpoint, headers=_headers, params=params, json=payload))
response = self.session.send(prep)
try:
data = response.json()
if data.get("errors"):
code = data["errors"][0]["extensions"].get("code")
if "token.service.unauthorized.client" in code:
raise ConnectionError("Unauthorized Client/IP: " + code)
if "idp.error.identity.bad-credentials" in code:
raise ConnectionError("Bad Credentials: " + code)
else:
raise ConnectionError(data["errors"])
return data
except Exception:
raise ConnectionError("Request failed: {}".format(response.content))
def get_page(self, title):
params = {
"disableSmartFocus": "true",
"limit": 999,
"enhancedContainersLimit": 0,
}
endpoint = self.href(
self.prd_config["services"]["explore"]["client"]["endpoints"]["getPage"]["href"],
version=self.config["EXPLORE_VERSION"],
pageId=title,
)
return self._request("GET", endpoint, params=params)["data"]["page"]
def get_video(self, content_id: str) -> dict:
endpoint = self.href(
self.prd_config["services"]["content"]["client"]["endpoints"]["getDmcVideo"]["href"], contentId=content_id
)
return self._request("GET", endpoint)["data"]["DmcVideo"]
def get_deeplink(self, ref_id: str) -> str:
params = {
"refId": ref_id,
"refIdType": "deeplinkId",
}
endpoint = "https://disney.content.edge.bamgrid.com/explore/v1.0/deeplink"
return self._request("GET", endpoint, params=params)
def series_bundle(self, series_id: str) -> dict:
endpoint = self.href(
self.prd_config["services"]["content"]["client"]["endpoints"]["getDmcSeriesBundle"]["href"],
encodedSeriesId=series_id,
)
return self.session.get(endpoint).json()["data"]["DmcSeriesBundle"]
def refresh_token(self, refresh_token: str):
payload = {
"operationName": "refreshToken",
"variables": {
"input": {
"refreshToken": refresh_token,
},
},
"query": queries.REFRESH_TOKEN,
}
endpoint = self.prd_config["services"]["orchestration"]["client"]["endpoints"]["refreshToken"]["href"]
data = self._request("POST", endpoint, payload=payload, headers={"authorization": self.config["API_KEY"]})
return data["extensions"]["sdk"]
def _refresh(self):
if not self._cache.expired:
return self._cache.data["token"]["accessToken"]
profile = self.refresh_token(self._cache.data["token"]["refreshToken"])
self._cache.set(profile, expiration=profile["token"]["expiresIn"] - 30)
return self._cache.data["token"]["accessToken"]
def register_device(self) -> dict:
payload = {
"variables": {
"registerDevice": {
"applicationRuntime": self.config["APPLICATION_RUNTIME"],
"attributes": {
"operatingSystem": "Android",
"operatingSystemVersion": "8.1.0",
},
"deviceFamily": self.config["DEVICE_FAMILY"],
"deviceLanguage": "en",
"deviceProfile": self.config["DEVICE_PROFILE"],
}
},
"query": queries.REGISTER_DEVICE,
}
endpoint = self.prd_config["services"]["orchestration"]["client"]["endpoints"]["registerDevice"]["href"]
data = self._request("POST", endpoint, payload=payload, headers={"authorization": self.config["API_KEY"]})
return data["extensions"]["sdk"]["token"]["accessToken"]
def login(self, email: str, password: str, token: str) -> dict:
payload = {
"operationName": "loginTv",
"variables": {
"input": {
"email": email,
"password": password,
},
},
"query": queries.LOGIN,
}
endpoint = self.prd_config["services"]["orchestration"]["client"]["endpoints"]["query"]["href"]
data = self._request("POST", endpoint, payload=payload, headers={"authorization": token})
return data["extensions"]["sdk"]["token"]
def href(self, href, **kwargs) -> str:
_args = {
"apiVersion": "{apiVersion}",
"region": self.active_session["location"]["countryCode"],
"impliedMaturityRating": 1850,
"kidsModeEnabled": "false",
"appLanguage": "en-US",
"partner": "disney",
}
_args.update(**kwargs)
href = href.format(**_args)
# [3.0, 3.1, 3.2, 5.0, 3.3, 5.1, 6.0, 5.2, 6.1]
api_version = "6.1"
if "/search/" in href:
api_version = "5.1"
return href.format(apiVersion=api_version)
def check_email(self, email: str, token: str) -> str:
payload = {
"operationName": "Check",
"variables": {
"email": email,
},
"query": queries.CHECK_EMAIL,
}
endpoint = self.prd_config["services"]["orchestration"]["client"]["endpoints"]["query"]["href"]
data = self._request("POST", endpoint, payload=payload, headers={"authorization": token})
return data["data"]["check"]["operations"][0]
def account(self) -> dict:
endpoint = self.prd_config["services"]["orchestration"]["client"]["endpoints"]["query"]["href"]
payload = {
"operationName": "EntitledGraphMeQuery",
"variables": {},
"query": queries.ENTITLEMENTS,
}
data = self._request("POST", endpoint, payload=payload)
return data["data"]["me"]
def switch_profile(self, profile_id: str) -> dict:
payload = {
"operationName": "switchProfile",
"variables": {
"input": {
"profileId": profile_id,
},
},
"query": queries.SWITCH_PROFILE,
}
endpoint = self.prd_config["services"]["orchestration"]["client"]["endpoints"]["query"]["href"]
data = self._request("POST", endpoint, payload=payload)
return data["extensions"]["sdk"]
@@ -1,27 +0,0 @@
CLIENT_ID: disney-svod-3d9324fc
CLIENT_VERSION: "9.10.0"
API_KEY: "ZGlzbmV5JmFuZHJvaWQmMS4wLjA.bkeb0m230uUhv8qrAXuNu39tbE_mD5EEhM_NAcohjyA"
CONFIG_URL: https://bam-sdk-configs.bamgrid.com/bam-sdk/v5.0/disney-svod-3d9324fc/android/v9.10.0/google/tv/prod.json
PAGE_SIZE_SETS: 15
PAGE_SIZE_CONTENT: 30
SEARCH_QUERY_TYPE: ge
BAM_PARTNER: disney
EXPLORE_VERSION: v1.3
DEVICE_FAMILY: browser
DEVICE_PROFILE: tv
APPLICATION_RUNTIME: android
HEADERS:
User-Agent: BAMSDK/v9.10.0 (disney-svod-3d9324fc 2.26.2-rc1.0; v5.0/v9.10.0; android; tv)
x-application-version: google
x-bamsdk-platform-id: android-tv
x-bamsdk-client-id: disney-svod-3d9324fc
x-bamsdk-platform: android-tv
x-bamsdk-version: "9.10.0"
Accept-Encoding: gzip
Origin: https://www.disneyplus.com
LICENSE: https://disney.playback.edge.bamgrid.com/widevine/v1/obtain-license
PLAYREADY_LICENSE: https://disney.playback.edge.bamgrid.com/playready/v1/obtain-license.asmx
File diff suppressed because one or more lines are too long
@@ -1,327 +0,0 @@
import base64
import hashlib
import json
import re
from collections.abc import Generator
from datetime import datetime
from http.cookiejar import CookieJar
from typing import Optional, Union
import click
from langcodes import Language
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapter, Subtitle, Tracks, Video
class EXAMPLE(Service):
"""
Service code for domain.com
Version: 1.0.0
Authorization: Cookies
Security: FHD@L3
Use full URL (for example - https://domain.com/details/20914) or title ID (for example - 20914).
"""
TITLE_RE = r"^(?:https?://?domain\.com/details/)?(?P<title_id>[^/]+)"
GEOFENCE = ("US", "UK")
NO_SUBTITLES = True
@staticmethod
@click.command(name="EXAMPLE", short_help="https://domain.com")
@click.argument("title", type=str)
@click.option("-m", "--movie", is_flag=True, default=False, help="Specify if it's a movie")
@click.option("-d", "--device", type=str, default="android_tv", help="Select device from the config file")
@click.pass_context
def cli(ctx, **kwargs):
return EXAMPLE(ctx, **kwargs)
def __init__(self, ctx, title, movie, device):
super().__init__(ctx)
self.title = title
self.movie = movie
self.device = device
self.cdm = ctx.obj.cdm
# Get range parameter for HDR support
range_param = ctx.parent.params.get("range_")
self.range = range_param[0].name if range_param else "SDR"
if self.config is None:
raise Exception("Config is missing!")
else:
profile_name = ctx.parent.params.get("profile")
if profile_name is None:
profile_name = "default"
self.profile = profile_name
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not cookies:
raise EnvironmentError("Service requires Cookies for Authentication.")
jwt_token = next((cookie.value for cookie in cookies if cookie.name == "streamco_token"), None)
payload = json.loads(base64.urlsafe_b64decode(jwt_token.split(".")[1] + "==").decode("utf-8"))
profile_id = payload.get("profileId", None)
self.session.headers.update({"user-agent": self.config["client"][self.device]["user_agent"]})
cache = self.cache.get(f"tokens_{self.device}_{self.profile}")
if cache:
if cache.data["expires_in"] > int(datetime.now().timestamp()):
self.log.info("Using cached tokens")
else:
self.log.info("Refreshing tokens")
refresh = self.session.post(
url=self.config["endpoints"]["refresh"], data={"refresh_token": cache.data["refresh_data"]}
).json()
cache.set(data=refresh)
else:
self.log.info("Retrieving new tokens")
token = self.session.post(
url=self.config["endpoints"]["login"],
data={
"token": jwt_token,
"profileId": profile_id,
},
).json()
cache.set(data=token)
self.token = cache.data["token"]
self.user_id = cache.data["userId"]
def search(self) -> Generator[SearchResult, None, None]:
search = self.session.get(
url=self.config["endpoints"]["search"], params={"q": self.title, "token": self.token}
).json()
for result in search["entries"]:
yield SearchResult(
id_=result["id"],
title=result["title"],
label="SERIES" if result["programType"] == "series" else "MOVIE",
url=result["url"],
)
def get_titles(self) -> Titles_T:
self.title = re.match(self.TITLE_RE, self.title).group(1)
metadata = self.session.get(
url=self.config["endpoints"]["metadata"].format(title_id=self.title), params={"token": self.token}
).json()
if metadata["programType"] == "movie":
self.movie = True
if self.movie:
return Movies(
[
Movie(
id_=metadata["id"],
service=self.__class__,
name=metadata["title"],
description=metadata["description"],
year=metadata["releaseYear"] if metadata["releaseYear"] > 0 else None,
language=Language.find(metadata["languages"][0]),
data=metadata,
)
]
)
else:
episodes = []
for season in metadata["seasons"]:
if "Trailers" not in season["title"]:
season_data = self.session.get(url=season["url"], params={"token": self.token}).json()
for episode in season_data["entries"]:
episodes.append(
Episode(
id_=episode["id"],
service=self.__class__,
title=metadata["title"],
season=episode["season"],
number=episode["episode"],
name=episode["title"],
description=episode["description"],
year=metadata["releaseYear"] if metadata["releaseYear"] > 0 else None,
language=Language.find(metadata["languages"][0]),
data=episode,
)
)
return Series(episodes)
def get_tracks(self, title: Title_T) -> Tracks:
# Handle HYBRID mode by fetching both HDR10 and DV tracks separately
if self.range == "HYBRID" and self.cdm.security_level != 3:
tracks = Tracks()
# Get HDR10 tracks
hdr10_tracks = self._get_tracks_for_range(title, "HDR10")
tracks.add(hdr10_tracks, warn_only=True)
# Get DV tracks
dv_tracks = self._get_tracks_for_range(title, "DV")
tracks.add(dv_tracks, warn_only=True)
return tracks
else:
# Normal single-range behavior
return self._get_tracks_for_range(title, self.range)
def _get_tracks_for_range(self, title: Title_T, range_override: str = None) -> Tracks:
# Use range_override if provided, otherwise use self.range
current_range = range_override if range_override else self.range
# Build API request parameters
params = {
"token": self.token,
"guid": title.id,
}
data = {
"type": self.config["client"][self.device]["type"],
}
# Add range-specific parameters
if current_range == "HDR10":
data["video_format"] = "hdr10"
elif current_range == "DV":
data["video_format"] = "dolby_vision"
else:
data["video_format"] = "sdr"
# Only request high-quality HDR content with L1 CDM
if current_range in ("HDR10", "DV") and self.cdm.security_level == 3:
# L3 CDM - skip HDR content
return Tracks()
streams = self.session.post(
url=self.config["endpoints"]["streams"],
params=params,
data=data,
).json()["media"]
self.license = {
"url": streams["drm"]["url"],
"data": streams["drm"]["data"],
"session": streams["drm"]["session"],
}
manifest_url = streams["url"].split("?")[0]
self.log.debug(f"Manifest URL: {manifest_url}")
tracks = DASH.from_url(url=manifest_url, session=self.session).to_tracks(language=title.language)
# Set range attributes on video tracks
for video in tracks.videos:
if current_range == "HDR10":
video.range = Video.Range.HDR10
elif current_range == "DV":
video.range = Video.Range.DV
else:
video.range = Video.Range.SDR
# Remove DRM-free ("clear") audio tracks
tracks.audio = [
track for track in tracks.audio if "clear" not in track.data["dash"]["representation"].get("id")
]
for track in tracks.audio:
if track.channels == 6.0:
track.channels = 5.1
track_label = track.data["dash"]["adaptation_set"].get("label")
if track_label and "Audio Description" in track_label:
track.descriptive = True
tracks.subtitles.clear()
if streams.get("captions"):
for subtitle in streams["captions"]:
tracks.add(
Subtitle(
id_=hashlib.md5(subtitle["url"].encode()).hexdigest()[0:6],
url=subtitle["url"],
codec=Subtitle.Codec.from_mime("vtt"),
language=Language.get(subtitle["language"]),
# cc=True if '(cc)' in subtitle['name'] else False,
sdh=True,
)
)
if not self.movie:
title.data["chapters"] = self.session.get(
url=self.config["endpoints"]["metadata"].format(title_id=title.id), params={"token": self.token}
).json()["chapters"]
return tracks
def get_chapters(self, title: Title_T) -> list[Chapter]:
chapters = []
if title.data.get("chapters", []):
for chapter in title.data["chapters"]:
if chapter["name"] == "Intro":
chapters.append(Chapter(timestamp=chapter["start"], name="Opening"))
chapters.append(Chapter(timestamp=chapter["end"]))
if chapter["name"] == "Credits":
chapters.append(Chapter(timestamp=chapter["start"], name="Credits"))
return chapters
def get_widevine_service_certificate(self, **_: any) -> str:
"""Return the Widevine service certificate from config, if available."""
return self.config.get("certificate")
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]:
"""Retrieve a PlayReady license for a given track."""
license_url = self.config["endpoints"].get("playready_license")
if not license_url:
raise ValueError("PlayReady license endpoint not configured")
response = self.session.post(
url=license_url,
data=challenge,
headers={
"user-agent": self.config["client"][self.device]["license_user_agent"],
},
)
response.raise_for_status()
return response.content
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
license_url = self.license.get("url") or self.config["endpoints"].get("widevine_license")
if not license_url:
raise ValueError("Widevine license endpoint not configured")
response = self.session.post(
url=license_url,
data=challenge,
params={
"session": self.license.get("session"),
"userId": self.user_id,
},
headers={
"dt-custom-data": self.license.get("data"),
"user-agent": self.config["client"][self.device]["license_user_agent"],
},
)
response.raise_for_status()
try:
return response.json().get("license")
except ValueError:
return response.content
@@ -1,12 +0,0 @@
endpoints:
login: https://api.domain.com/v1/login
metadata: https://api.domain.com/v1/metadata/{title_id}.json
streams: https://api.domain.com/v1/streams
playready_license: https://api.domain.com/v1/license/playready
widevine_license: https://api.domain.com/v1/license/widevine
client:
android_tv:
user_agent: USER_AGENT
license_user_agent: LICENSE_USER_AGENT
type: DATA
@@ -1,361 +0,0 @@
from __future__ import annotations
import hashlib
import json
import re
import sys
from collections.abc import Generator
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
import click
from bs4 import BeautifulSoup
from click import Context
from envied.core.credential import Credential
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Subtitle, Tracks
class ITV(Service):
"""
Service code for ITVx streaming service (https://www.itv.com/).
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: Cookies (Optional for free content | Required for premium content)
Robustness:
L3: 1080p
\b
Tips:
- Use complete title URL as input (pay attention to the URL format):
SERIES: https://www.itv.com/watch/bay-of-fires/10a5270
EPISODE: https://www.itv.com/watch/bay-of-fires/10a5270/10a5270a0001
FILM: https://www.itv.com/watch/mad-max-beyond-thunderdome/2a7095
- Some shows aren't listed as series, only as "Latest episodes"
Download by SERIES URL for those titles, not by EPISODE URL
\b
Examples:
- SERIES: devine dl -w s01e01 itv https://www.itv.com/watch/bay-of-fires/10a5270
- EPISODE: devine dl itv https://www.itv.com/watch/bay-of-fires/10a5270/10a5270a0001
- FILM: devine dl itv https://www.itv.com/watch/mad-max-beyond-thunderdome/2a7095
\b
Notes:
ITV seem to detect and throttle multiple connections against the server.
It's recommended to use requests as downloader, with few workers.
"""
GEOFENCE = ("gb",)
ALIASES = ("itvx",)
@staticmethod
@click.command(name="ITV", short_help="https://www.itv.com/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> ITV:
return ITV(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.profile = ctx.parent.params.get("profile")
if not self.profile:
self.profile = "default"
self.session.headers.update(self.config["headers"])
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
self.authorization = None
if credential and not cookies:
self.log.error(" - Error: This service requires cookies for authentication.")
sys.exit(1)
if cookies is not None:
self.log.info(f"\n + Cookies for '{self.profile}' profile found, authenticating...")
itv_session = next((cookie.value for cookie in cookies if cookie.name == "Itv.Session"), None)
if not itv_session:
self.log.error(" - Error: Session cookie not found. Cookies may be invalid.")
sys.exit(1)
itv_session = json.loads(itv_session)
refresh_token = itv_session["tokens"]["content"].get("refresh_token")
if not refresh_token:
self.log.error(" - Error: Access tokens not found. Try refreshing your cookies.")
sys.exit(1)
cache = self.cache.get(f"tokens_{self.profile}")
headers = {
"Host": "auth.prd.user.itv.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
"Accept": "application/vnd.user.auth.v2+json",
"Accept-Language": "en-US,en;q=0.8",
"Origin": "https://www.itv.com",
"Connection": "keep-alive",
"Referer": "https://www.itv.com/",
}
params = {"refresh": cache.data["refresh_token"]} if cache else {"refresh": refresh_token}
r = self.session.get(
self.config["endpoints"]["refresh"],
headers=headers,
params=params,
)
if r.status_code != 200:
raise ConnectionError(f"Failed to refresh tokens: {r.text}")
tokens = r.json()
cache.set(tokens)
self.log.info(" + Tokens refreshed and placed in cache\n")
self.authorization = tokens["access_token"]
def search(self) -> Generator[SearchResult, None, None]:
params = {
"broadcaster": "itv",
"featureSet": "clearkey,outband-webvtt,hls,aes,playready,widevine,fairplay,bbts,progressive,hd,rtmpe",
"onlyFree": "false",
"platform": "dotcom",
"query": self.title,
}
r = self.session.get(self.config["endpoints"]["search"], params=params)
r.raise_for_status()
results = r.json()["results"]
if isinstance(results, list):
for result in results:
special = result["data"].get("specialTitle")
standard = result["data"].get("programmeTitle")
film = result["data"].get("filmTitle")
title = special if special else standard if standard else film
tier = result["data"].get("tier")
slug = self._sanitize(title)
_id = result["data"]["legacyId"]["apiEncoded"]
_id = "_".join(_id.split("_")[:2]).replace("_", "a")
_id = re.sub(r"a000\d+", "", _id)
yield SearchResult(
id_=f"https://www.itv.com/watch/{slug}/{_id}",
title=title,
description=result["data"].get("synopsis"),
label=result.get("entityType") + f" {tier}",
url=f"https://www.itv.com/watch/{slug}/{_id}",
)
def get_titles(self) -> Union[Movies, Series]:
data = self.get_data(self.title)
kind = next(
(x.get("seriesType") for x in data.get("seriesList") if x.get("seriesType") in ["SERIES", "FILM"]), None
)
# Some shows are not listed as "SERIES" or "FILM", only as "Latest episodes"
if not kind and next(
(x for x in data.get("seriesList") if x.get("seriesLabel").lower() in ("latest episodes", "other episodes")), None
):
titles = data["seriesList"][0]["titles"]
episodes =[
Episode(
id_=episode["episodeId"],
service=self.__class__,
title=data["programme"]["title"],
season=episode.get("series") if isinstance(episode.get("series"), int) else 0,
number=episode.get("episode") if isinstance(episode.get("episode"), int) else 0,
name=episode["episodeTitle"],
language="en", # TODO: language detection
data=episode,
)
for episode in titles
]
# Assign episode numbers to special seasons
counter = 1
for episode in episodes:
if episode.season == 0 and episode.number == 0:
episode.number = counter
counter += 1
return Series(episodes)
if kind == "SERIES" and data.get("episode"):
episode = data.get("episode")
return Series(
[
Episode(
id_=episode["episodeId"],
service=self.__class__,
title=data["programme"]["title"],
season=episode.get("series") if isinstance(episode.get("series"), int) else 0,
number=episode.get("episode") if isinstance(episode.get("episode"), int) else 0,
name=episode["episodeTitle"],
language="en", # TODO: language detection
data=episode,
)
]
)
elif kind == "SERIES":
return Series(
[
Episode(
id_=episode["episodeId"],
service=self.__class__,
title=data["programme"]["title"],
season=episode.get("series") if isinstance(episode.get("series"), int) else 0,
number=episode.get("episode") if isinstance(episode.get("episode"), int) else 0,
name=episode["episodeTitle"],
language="en", # TODO: language detection
data=episode,
)
for series in data["seriesList"]
if "Latest episodes" not in series["seriesLabel"]
for episode in series["titles"]
]
)
elif kind == "FILM":
return Movies(
[
Movie(
id_=movie["episodeId"],
service=self.__class__,
name=data["programme"]["title"],
year=movie.get("productionYear"),
language="en", # TODO: language detection
data=movie,
)
for movies in data["seriesList"]
for movie in movies["titles"]
]
)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
playlist = title.data.get("playlistUrl")
headers = {
"Accept": "application/vnd.itv.vod.playlist.v4+json",
"Accept-Language": "en-US,en;q=0.9,da;q=0.8",
"Connection": "keep-alive",
"Content-Type": "application/json",
}
payload = {
"client": {
"id": "lg",
},
"device": {
"deviceGroup": "ctv",
},
"variantAvailability": {
"player": "dash",
"featureset": [
"mpeg-dash",
"widevine",
"outband-webvtt",
"hd",
"single-track",
],
"platformTag": "ctv",
"drm": {
"system": "widevine",
"maxSupported": "L3",
},
},
}
if self.authorization:
payload["user"] = {"token": self.authorization}
r = self.session.post(playlist, headers=headers, json=payload)
if r.status_code != 200:
raise ConnectionError(r.text)
data = r.json()
video = data["Playlist"]["Video"]
subtitles = video.get("Subtitles")
self.manifest = video["MediaFiles"][0].get("Href")
self.license = video["MediaFiles"][0].get("KeyServiceUrl")
tracks = DASH.from_url(self.manifest, self.session).to_tracks(title.language)
tracks.videos[0].data = data
if subtitles is not None:
for subtitle in subtitles:
tracks.add(
Subtitle(
id_=hashlib.md5(subtitle.get("Href", "").encode()).hexdigest()[0:6],
url=subtitle.get("Href", ""),
codec=Subtitle.Codec.from_mime(subtitle.get("Href", "")[-3:]),
language=title.language,
forced=False,
)
)
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
track = title.tracks.videos[0]
if not track.data["Playlist"].get("ContentBreaks"):
return Chapters()
breaks = track.data["Playlist"]["ContentBreaks"]
timecodes = [".".join(x.get("TimeCode").rsplit(":", 1)) for x in breaks if x.get("TimeCode") != "00:00:00:000"]
# End credits are sometimes listed before the last chapter, so we skip those for now
return Chapters([Chapter(timecode) for timecode in timecodes])
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
r = self.session.post(url=self.license, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
# Service specific functions
def get_data(self, url: str) -> dict:
# TODO: Find a proper endpoint for this
r = self.session.get(url)
if r.status_code != 200:
raise ConnectionError(r.text)
soup = BeautifulSoup(r.text, "html.parser")
props = soup.select_one("#__NEXT_DATA__").text
try:
data = json.loads(props)
except Exception as e:
raise ValueError(f"Failed to parse JSON: {e}")
return data["props"]["pageProps"]
@staticmethod
def _sanitize(title: str) -> str:
title = title.lower()
title = title.replace("&", "and")
title = re.sub(r"[:;/()]", "", title)
title = re.sub(r"[ ]", "-", title)
title = re.sub(r"[\\*!?¿,'\"<>|$#`]", "", title)
title = re.sub(rf"[{'.'}]{{2,}}", ".", title)
title = re.sub(rf"[{'_'}]{{2,}}", "_", title)
title = re.sub(rf"[{'-'}]{{2,}}", "-", title)
title = re.sub(rf"[{' '}]{{2,}}", " ", title)
return title
@@ -1,7 +0,0 @@
headers:
User-Agent: okhttp/4.9.3
endpoints:
login: https://auth.prd.user.itv.com/v2/auth
refresh: https://auth.prd.user.itv.com/token
search: https://textsearch.prd.oasvc.itv.com/search
@@ -1,650 +0,0 @@
import hashlib
import json
import re
import uuid
from datetime import datetime
from hashlib import md5
from typing import Optional, Union, Generator
from http.cookiejar import CookieJar
import click
import requests
import xmltodict
from langcodes import Language
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapter, Chapters, Subtitle, Tracks, Video
class MAX(Service):
"""
Service code for MAX's streaming service (https://max.com).
Version: 1.0.0
Authorization: Cookies
Security: UHD@L1 FHD@L1 HD@L3
Use full URL or title ID with type.
Examples:
- https://play.hbomax.com/movie/urn:hbo:movie:GUID
- https://play.hbomax.com/show/urn:hbo:series:GUID
- movie/GUID
- show/GUID
Note: This service is designed for users who have legal access to MAX content.
Ensure you have proper subscription and authentication before use.
"""
ALIASES = ("MAX", "max", "hbomax")
GEOFENCE = ("US",)
TITLE_RE = r"^(?:https?://(?:www\.|play\.)?hbomax\.com/)?(?P<type>[^/]+)/(?P<id>[^/]+)"
VIDEO_CODEC_MAP = {
"H264": ["avc1"],
"H265": ["hvc1", "dvh1"]
}
AUDIO_CODEC_MAP = {
"AAC": "mp4a",
"AC3": "ac-3",
"EC3": "ec-3"
}
@staticmethod
@click.command(name="MAX", short_help="https://max.com")
@click.argument("title", type=str)
@click.option("-vcodec", "--video-codec", default=None, help="Video codec preference")
@click.option("-acodec", "--audio-codec", default=None, help="Audio codec preference")
@click.pass_context
def cli(ctx, **kwargs):
return MAX(ctx, **kwargs)
def __init__(self, ctx, title, video_codec, audio_codec):
super().__init__(ctx)
self.title = title
self.vcodec = video_codec
self.acodec = audio_codec
# Get range parameter for HDR support
range_param = ctx.parent.params.get("range_")
self.range = range_param[0].name if range_param else "SDR"
if self.range == 'HDR10':
self.vcodec = "H265"
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not cookies:
raise EnvironmentError("Service requires Cookies for Authentication.")
# Extract authentication tokens from cookies
try:
token = next(cookie.value for cookie in cookies if cookie.name == "st")
session_data = next(cookie.value for cookie in cookies if cookie.name == "session")
device_id = json.loads(session_data)
except (StopIteration, json.JSONDecodeError):
raise EnvironmentError("Required authentication cookies not found.")
# Configure headers based on device type
self.session.headers.update({
'User-Agent': 'BEAM-Android/1.0.0.104 (SONY/XR-75X95EL)',
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'x-disco-client': 'SAMSUNGTV:124.0.0.0:beam:4.0.0.118',
'x-disco-params': 'realm=bolt,bid=beam,features=ar',
'x-device-info': 'beam/4.0.0.118 (Samsung/Samsung-Unknown; Tizen/124.0.0.0; f198a6c1-c582-4725-9935-64eb6b17c3cd/87a996fa-4917-41ae-9b6d-c7f521f0cb78)',
'traceparent': '00-315ac07a3de9ad1493956cf1dd5d1313-988e057938681391-01',
'tracestate': f'wbd=session:{device_id}',
'Origin': 'https://play.hbomax.com',
'Referer': 'https://play.hbomax.com/',
})
# Get device token
auth_token = self._get_device_token()
self.session.headers.update({
"x-wbd-session-state": auth_token
})
def search(self) -> Generator[SearchResult, None, None]:
"""
Search for content on MAX platform.
Note: This is a basic implementation - MAX's search API may require additional parameters.
"""
# Basic search implementation - you may need to adjust based on actual API
search_url = "https://default.prd.api.hbomax.com/search"
try:
response = self.session.get(search_url, params={"q": self.title})
response.raise_for_status()
search_data = response.json()
# Parse search results - adjust based on actual API response structure
for result in search_data.get("results", []):
yield SearchResult(
id_=result.get("id"),
title=result.get("title", "Unknown"),
label=result.get("type", "UNKNOWN").upper(),
url=f"https://play.hbomax.com/{result.get('type', 'content')}/{result.get('id')}"
)
except Exception as e:
self.log.warning(f"Search functionality not fully implemented: {e}")
# Return empty generator if search fails
return
yield # This makes it a generator function
def get_titles(self) -> Titles_T:
# Parse title input
match = re.match(self.TITLE_RE, self.title)
if not match:
raise ValueError("Invalid title format. Expected format: type/id or full URL")
content_type = match.group('type')
external_id = match.group('id')
response = self.session.get(
self.config['endpoints']['contentRoutes'] % (content_type, external_id)
)
response.raise_for_status()
try:
content_data = [x for x in response.json()["included"] if "attributes" in x and "title" in
x["attributes"] and x["attributes"]["alias"] == "generic-%s-blueprint-page" % (re.sub(r"-", "", content_type))][0]["attributes"]
content_title = content_data["title"]
except:
content_data = [x for x in response.json()["included"] if "attributes" in x and "alternateId" in
x["attributes"] and x["attributes"]["alternateId"] == external_id and x["attributes"].get("originalName")][0]["attributes"]
content_title = content_data["originalName"]
if content_type == "sport" or content_type == "event":
included_dt = response.json()["included"]
for included in included_dt:
for key, data in included.items():
if key == "attributes":
for k, d in data.items():
if d == "VOD":
event_data = included
release_date = event_data["attributes"].get("airDate") or event_data["attributes"].get("firstAvailableDate")
year = datetime.strptime(release_date, '%Y-%m-%dT%H:%M:%SZ').year
return Movies([
Movie(
id_=external_id,
service=self.__class__,
name=content_title.title(),
year=year,
data=event_data,
)
])
if content_type == "movie" or content_type == "standalone":
metadata = self.session.get(
url=self.config['endpoints']['moviePages'] % external_id
).json()['data']
try:
edit_id = metadata['relationships']['edit']['data']['id']
except:
for x in response.json()["included"]:
if x.get("type") == "video" and x.get("relationships", {}).get("show", {}).get("data", {}).get("id") == external_id:
metadata = x
release_date = metadata["attributes"].get("airDate") or metadata["attributes"].get("firstAvailableDate")
year = datetime.strptime(release_date, '%Y-%m-%dT%H:%M:%SZ').year
return Movies([
Movie(
id_=external_id,
service=self.__class__,
name=content_title,
year=year,
data=metadata,
)
])
if content_type in ["show", "mini-series", "topical"]:
episodes = []
if content_type == "mini-series":
alias = "generic-miniseries-page-rail-episodes"
elif content_type == "topical":
alias = "generic-topical-show-page-rail-episodes"
else:
alias = "-%s-page-rail-episodes-tabbed-content" % (content_type)
included_dt = response.json()["included"]
season_data = [data for included in included_dt for key, data in included.items()
if key == "attributes" for k, d in data.items() if alias in str(d).lower()][0]
season_data = season_data["component"]["filters"][0]
seasons = [int(season["value"]) for season in season_data["options"]]
season_parameters = [(int(season["value"]), season["parameter"]) for season in season_data["options"]
for season_number in seasons if int(season["value"]) == int(season_number)]
if not season_parameters:
raise ValueError("No seasons found")
for (value, parameter) in season_parameters:
data = self.session.get(
url=self.config['endpoints']['showPages'] % (external_id, parameter)
).json()
try:
episodes_dt = sorted([dt for dt in data["included"] if "attributes" in dt and "videoType" in
dt["attributes"] and dt["attributes"]["videoType"] == "EPISODE"
and int(dt["attributes"]["seasonNumber"]) == int(parameter.split("=")[-1])],
key=lambda x: x["attributes"]["episodeNumber"])
except KeyError:
raise ValueError("Season episodes were not found")
episodes.extend(episodes_dt)
episode_titles = []
release_date = episodes[0]["attributes"].get("airDate") or episodes[0]["attributes"].get("firstAvailableDate")
year = datetime.strptime(release_date, '%Y-%m-%dT%H:%M:%SZ').year
season_map = {int(item[1].split("=")[-1]): item[0] for item in season_parameters}
for episode in episodes:
episode_titles.append(
Episode(
id_=episode['id'],
service=self.__class__,
title=content_title,
season=season_map.get(episode['attributes'].get('seasonNumber')),
number=episode['attributes']['episodeNumber'],
name=episode['attributes']['name'],
year=year,
data=episode
)
)
return Series(episode_titles)
def get_tracks(self, title: Title_T) -> Tracks:
edit_id = title.data['relationships']['edit']['data']['id']
response = self.session.post(
url=self.config['endpoints']['playbackInfo'],
json={
'appBundle': 'beam',
'consumptionType': 'streaming',
'deviceInfo': {
'deviceId': '2dec6cb0-eb34-45f9-bbc9-a0533597303c',
'browser': {
'name': 'chrome',
'version': '113.0.0.0',
},
'make': 'Microsoft',
'model': 'XBOX-Unknown',
'os': {
'name': 'Windows',
'version': '113.0.0.0',
},
'platform': 'XBOX',
'deviceType': 'xbox',
'player': {
'sdk': {
'name': 'Beam Player Console',
'version': '1.0.2.4',
},
'mediaEngine': {
'name': 'GLUON_BROWSER',
'version': '1.20.1',
},
'playerView': {
'height': 1080,
'width': 1920,
},
},
},
'editId': edit_id,
'capabilities': {
'manifests': {
'formats': {
'dash': {},
},
},
'codecs': {
'video': {
'hdrFormats': [
'hlg',
'hdr10',
'dolbyvision5',
'dolbyvision8',
],
'decoders': [
{
'maxLevel': '6.2',
'codec': 'h265',
'levelConstraints': {
'width': {
'min': 1920,
'max': 3840,
},
'height': {
'min': 1080,
'max': 2160,
},
'framerate': {
'min': 15,
'max': 60,
},
},
'profiles': [
'main',
'main10',
],
},
{
'maxLevel': '4.2',
'codec': 'h264',
'levelConstraints': {
'width': {
'min': 640,
'max': 3840,
},
'height': {
'min': 480,
'max': 2160,
},
'framerate': {
'min': 15,
'max': 60,
},
},
'profiles': [
'high',
'main',
'baseline',
],
},
],
},
'audio': {
'decoders': [
{
'codec': 'aac',
'profiles': [
'lc',
'he',
'hev2',
'xhe',
],
},
],
},
},
'devicePlatform': {
'network': {
'lastKnownStatus': {
'networkTransportType': 'unknown',
},
'capabilities': {
'protocols': {
'http': {
'byteRangeRequests': True,
},
},
},
},
'videoSink': {
'lastKnownStatus': {
'width': 1290,
'height': 2796,
},
'capabilities': {
'colorGamuts': [
'standard',
'wide',
],
'hdrFormats': [
'dolbyvision',
'hdr10plus',
'hdr10',
'hlg',
],
},
},
},
},
'gdpr': False,
'firstPlay': False,
'playbackSessionId': str(uuid.uuid4()),
'applicationSessionId': str(uuid.uuid4()),
'userPreferences': {},
'features': [],
}
)
response.raise_for_status()
playback_data = response.json()
# Get video info for language
video_info = next(x for x in playback_data['videos'] if x['type'] == 'main')
title.language = Language.get(video_info['defaultAudioSelection']['language'])
fallback_url = playback_data["fallback"]["manifest"]["url"]
fallback_url = fallback_url.replace('fly', 'akm').replace('gcp', 'akm')
try:
self.wv_license_url = playback_data["drm"]["schemes"]["widevine"]["licenseUrl"]
except (KeyError, IndexError):
self.wv_license_url = None
try:
self.pr_license_url = playback_data["drm"]["schemes"]["playready"]["licenseUrl"]
except (KeyError, IndexError):
self.pr_license_url = None
manifest_url = fallback_url.replace('_fallback', '')
self.log.debug(f"MPD URL: {manifest_url}")
self.log.debug(f"Fallback URL: {fallback_url}")
self.log.debug(f"Widevine License URL: {self.wv_license_url}")
self.log.debug(f"PlayReady License URL: {self.pr_license_url}")
tracks = DASH.from_url(url=manifest_url, session=self.session).to_tracks(language=title.language)
self.log.debug(tracks)
tracks.videos = self._dedupe(tracks.videos)
tracks.audio = self._dedupe(tracks.audio)
# Remove partial subs and get VTT subtitles
tracks.subtitles.clear()
subtitles = self._get_subtitles(manifest_url, fallback_url)
for subtitle in subtitles:
tracks.add(
Subtitle(
id_=md5(subtitle["url"].encode()).hexdigest()[0:6],
url=subtitle["url"],
codec=Subtitle.Codec.from_mime(subtitle['format']),
language=Language.get(subtitle["language"]),
forced=subtitle['name'] == 'Forced',
sdh=subtitle['name'] == 'SDH'
)
)
# Apply codec filters
if self.vcodec:
tracks.videos = [x for x in tracks.videos if (x.codec or "")[:4] in self.VIDEO_CODEC_MAP[self.vcodec]]
if self.acodec:
tracks.audio = [x for x in tracks.audio if (x.codec or "")[:4] == self.AUDIO_CODEC_MAP[self.acodec]]
# Set track properties
for track in tracks:
if isinstance(track, Video):
codec = track.data.get("dash", {}).get("representation", {}).get("codecs", "")
track.hdr10 = track.range == Video.Range.HDR10
track.dv = codec[:4] in ("dvh1", "dvhe")
if isinstance(track, Subtitle) and not track.codec:
track.codec = Subtitle.Codec.WebVTT
# Store video info for chapters
title.data['info'] = video_info
# Mark descriptive audio tracks
for track in tracks.audio:
if hasattr(track, 'data') and track.data.get("dash", {}).get("adaptation_set"):
role = track.data["dash"]["adaptation_set"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
self.log.debug(tracks)
return tracks
def get_chapters(self, title: Title_T) -> Chapters:
chapters = []
video_info = title.data.get('info', {})
if 'annotations' in video_info:
chapters.append(Chapter(timestamp=0.0, name='Chapter 1'))
chapters.append(Chapter(timestamp=self._convert_timecode(video_info['annotations'][0]['start']), name='Credits'))
chapters.append(Chapter(timestamp=self._convert_timecode(video_info['annotations'][0]['end']), name='Chapter 2'))
return Chapters(chapters)
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
if not self.wv_license_url:
return None
response = self.session.post(
url=self.wv_license_url,
data=challenge
)
response.raise_for_status()
return response.content
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]:
if not self.pr_license_url:
return None
# Handle both bytes and string challenge formats
if isinstance(challenge, bytes):
decoded_challenge = challenge.decode('utf-8')
else:
decoded_challenge = str(challenge)
response = self.session.post(
url=self.pr_license_url,
data=decoded_challenge,
headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense'
}
)
response.raise_for_status()
return response.content
def _get_device_token(self):
response = self.session.post(self.config['endpoints']['bootstrap'])
response.raise_for_status()
return response.headers.get('x-wbd-session-state')
@staticmethod
def _convert_timecode(time_seconds):
"""Convert seconds to timestamp."""
return float(time_seconds)
def _get_subtitles(self, mpd_url, fallback_url):
base_url = "/".join(fallback_url.split("/")[:-1]) + "/"
xml = xmltodict.parse(requests.get(mpd_url).text)
try:
tracks = xml["MPD"]["Period"][0]["AdaptationSet"]
except KeyError:
tracks = xml["MPD"]["Period"]["AdaptationSet"]
subs_tracks_js = []
for subs_tracks in tracks:
if subs_tracks.get('@contentType') == 'text':
for x in self._force_instance(subs_tracks, "Representation"):
try:
path = re.search(r'(t/\w+/)', x["SegmentTemplate"]["@media"])[1]
except (AttributeError, KeyError):
path = 't/sub/'
is_sdh = False
is_forced = False
role_value = subs_tracks.get("Role", {}).get("@value", "")
if role_value == "caption":
url = base_url + path + subs_tracks['@lang'] + ('_sdh.vtt' if 'sdh' in subs_tracks.get("Label", "").lower() else '_cc.vtt')
is_sdh = True
elif role_value == "forced-subtitle":
url = base_url + path + subs_tracks['@lang'] + '_forced.vtt'
is_forced = True
elif role_value == "subtitle":
url = base_url + path + subs_tracks['@lang'] + '_sub.vtt'
else:
continue
subs_tracks_js.append({
"url": url,
"format": "vtt",
"language": subs_tracks["@lang"],
"name": "SDH" if is_sdh else "Forced" if is_forced else "Full",
})
return self._remove_dupe(subs_tracks_js)
@staticmethod
def _force_instance(data, variable):
if isinstance(data[variable], list):
return data[variable]
else:
return [data[variable]]
@staticmethod
def _remove_dupe(items):
seen = set()
new_items = []
for item in items:
url = item['url']
if url not in seen:
new_items.append(item)
seen.add(url)
return new_items
@staticmethod
def _dedupe(items: list) -> list:
if not items:
return items
if isinstance(items[0].url, list):
return items
# Create a more specific key for deduplication that includes resolution/bitrate
seen = {}
for item in items:
# For video tracks, use codec + resolution + bitrate as key
if hasattr(item, 'width') and hasattr(item, 'height'):
key = f"{item.codec}_{item.width}x{item.height}_{item.bitrate}"
# For audio tracks, use codec + language + bitrate + channels as key
elif hasattr(item, 'channels'):
key = f"{item.codec}_{item.language}_{item.bitrate}_{item.channels}"
# Fallback to URL for other track types
else:
key = item.url
# Keep the item if we haven't seen this exact combination
if key not in seen:
seen[key] = item
return list(seen.values())
@@ -1,6 +0,0 @@
endpoints:
contentRoutes: 'https://default.prd.api.hbomax.com/cms/routes/%s/%s?include=default'
moviePages: 'https://default.prd.api.hbomax.com/content/videos/%s/activeVideoForShow?&include=edit'
playbackInfo: 'https://default.prd.api.hbomax.com/any/playback/v1/playbackInfo'
showPages: 'https://default.prd.api.hbomax.com/cms/collections/generic-show-page-rail-episodes-tabbed-content?include=default&pf[show.id]=%s&%s'
bootstrap: 'https://default.prd.api.hbomax.com/session-context/headwaiter/v1/bootstrap'
@@ -1,101 +0,0 @@
from __future__ import annotations
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional
import re
import click
from click import Context
from bs4 import BeautifulSoup
from envied.core.credential import Credential
from envied.core.service import Service
from envied.core.titles import Movie, Movies
from envied.core.tracks import Chapter, Tracks
from envied.core.manifests.dash import DASH
class MTSP(Service):
TITLE_RE = r"^(?:https?://(?:www\.)?magentasport\.de/event/[^/]+)?/[0-9]+/(?P<video_id>[0-9]+)"
@staticmethod
@click.command(name="MTSP", short_help="https://magentasport.de", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> MTSP:
return MTSP(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
cache = self.cache.get(f"session_{credential.sha1}")
if cache and not cache.expired:
self.session.cookies.update({
"session": cache.data,
"entitled": "1",
})
return
self.log.info("No cached session cookie, logging in...")
r = self.session.get(self.config["endpoints"]["login_form"])
r.raise_for_status()
tid, xsrf_name, xsrf_value = self.get_login_tid_xsrf(r.text)
data = {
"tid": tid,
xsrf_name: xsrf_value,
"pkc": "",
"webauthn_supported": "false",
"pw_usr": credential.username
}
r = self.session.post(self.config["endpoints"]["login_post"], data=data)
r.raise_for_status()
tid, xsrf_name, xsrf_value = self.get_login_tid_xsrf(r.text)
data = {
"tid": tid,
xsrf_name: xsrf_value,
"hidden_usr": credential.username,
"pw_pwd": credential.password,
"persist_session_displayed": "1",
"persist_session": "on"
}
r = self.session.post(self.config["endpoints"]["login_post"], data=data)
r.raise_for_status()
session = self.session.cookies.get_dict().get('session')
cache.set(session)
def get_titles(self) -> Movies:
video_id = re.match(self.TITLE_RE, self.title).group("video_id")
r = self.session.get(self.config["endpoints"]["video_config"].format(video_id=video_id))
config = r.json()
return Movies([Movie(
id_=video_id,
service=self.__class__,
name=config["title"],
language="de",
data=config,
)])
def get_tracks(self, title: Movie) -> Tracks:
r = self.session.post(title.data['streamAccess'])
access = r.json()
tracks = DASH.from_url(access["data"]["stream"]["dash"]).to_tracks(title.language)
return tracks
def get_chapters(self, title: Movie) -> list[Chapter]:
return [
]
def get_login_tid_xsrf(self, html):
soup = BeautifulSoup(html, "html.parser")
form = soup.find("form", id="login")
xsrf = form.find("input", {"name": re.compile("^xsrf_")})
tid = form.find("input", {"name": "tid"})
return tid.get("value"), xsrf.get('name'), xsrf.get("value")
@@ -1,8 +0,0 @@
headers:
Accept-Language: de-DE,de;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
endpoints:
login_form: https://www.magentasport.de/service/auth/web/login?headto=https://www.magentasport.de/home
login_post: https://accounts.login.idm.telekom.com/factorx
video_config: https://www.magentasport.de/service/player/v2/videoConfig?videoid={video_id}&partnerid=0&language=de&format=iphone&device=desktop&platform=web&cdn=telekom_cdn&userType=loggedin-entitled
@@ -1,208 +0,0 @@
from __future__ import annotations
import base64
import os
import re
import tempfile
from collections.abc import Generator
from typing import Any, Union
from urllib.parse import urlparse, urlunparse
import click
import requests
from click import Context
from pywidevine.cdm import Cdm as WidevineCdm
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Tracks
from envied.core.utils.sslciphers import SSLCiphers
class MY5(Service):
"""
\b
Service code for Channel 5's My5 streaming service (https://channel5.com).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: None
Robustness:
L3: 1080p, AAC2.0
\b
Tips:
- Input for series/films/episodes can be either complete URL or just the slug/path:
https://www.channel5.com/the-cuckoo OR the-cuckoo OR the-cuckoo/season-1/episode-1
\b
Known bugs:
- The progress bar is broken for certain DASH manifests
See issue: https://github.com/devine-dl/devine/issues/106
"""
ALIASES = ("channel5", "ch5", "c5")
GEOFENCE = ("gb",)
TITLE_RE = r"^(?:https?://(?:www\.)?channel5\.com(?:/show)?/)?(?P<id>[a-z0-9-]+)(?:/(?P<sea>[a-z0-9-]+))?(?:/(?P<ep>[a-z0-9-]+))?"
@staticmethod
@click.command(name="MY5", short_help="https://channel5.com", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> MY5:
return MY5(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.session.headers.update({"user-agent": self.config["user_agent"]})
def search(self) -> Generator[SearchResult, None, None]:
params = {
"platform": "my5desktop",
"friendly": "1",
"query": self.title,
}
r = self.session.get(self.config["endpoints"]["search"], params=params)
r.raise_for_status()
results = r.json()
for result in results["shows"]:
yield SearchResult(
id_=result.get("f_name"),
title=result.get("title"),
description=result.get("s_desc"),
label=result.get("genre"),
url="https://www.channel5.com/show/" + result.get("f_name"),
)
def get_titles(self) -> Union[Movies, Series]:
title, season, episode = (re.match(self.TITLE_RE, self.title).group(i) for i in ("id", "sea", "ep"))
if not title:
raise ValueError("Could not parse ID from title - is the URL correct?")
if season and episode:
r = self.session.get(
self.config["endpoints"]["single"].format(
show=title,
season=season,
episode=episode,
)
)
r.raise_for_status()
episode = r.json()
return Series(
[
Episode(
id_=episode.get("id"),
service=self.__class__,
title=episode.get("sh_title"),
season=int(episode.get("sea_num")) if episode.get("sea_num") else 0,
number=int(episode.get("ep_num")) if episode.get("ep_num") else 0,
name=episode.get("sh_title"),
language="en",
)
]
)
r = self.session.get(self.config["endpoints"]["episodes"].format(show=title))
r.raise_for_status()
data = r.json()
if data["episodes"][0]["genre"] == "Film":
return Movies(
[
Movie(
id_=movie.get("id"),
service=self.__class__,
year=None,
name=movie.get("sh_title"),
language="en", # TODO: don't assume
)
for movie in data.get("episodes")
]
)
else:
return Series(
[
Episode(
id_=episode.get("id"),
service=self.__class__,
title=episode.get("sh_title"),
season=int(episode.get("sea_num")) if episode.get("sea_num") else 0,
number=int(episode.get("ep_num")) if episode.get("sea_num") else 0,
name=episode.get("title"),
language="en", # TODO: don't assume
)
for episode in data["episodes"]
]
)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
self.manifest, self.license = self.get_playlist(title.id)
tracks = DASH.from_url(self.manifest, self.session).to_tracks(title.language)
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> list[Chapter]:
return []
def get_widevine_service_certificate(self, **_: Any) -> str:
return WidevineCdm.common_privacy_cert
def get_widevine_license(self, challenge: bytes, **_: Any) -> str:
r = self.session.post(self.license, data=challenge)
r.raise_for_status()
return r.content
# Service specific functions
def get_playlist(self, asset_id: str) -> tuple:
session = self.session
for prefix in ("https://", "http://"):
session.mount(prefix, SSLCiphers())
cert_binary = base64.b64decode(self.config["certificate"])
with tempfile.NamedTemporaryFile(delete=False, suffix=".pem") as cert_file:
cert_file.write(cert_binary)
cert_path = cert_file.name
try:
r = session.get(url=self.config["endpoints"]["auth"].format(title_id=asset_id), cert=cert_path)
except requests.RequestException as e:
if "Max retries exceeded" in str(e):
raise ConnectionError(
"Permission denied. If you're behind a VPN/proxy, you might be blocked"
)
else:
raise ConnectionError(f"Failed to request assets: {str(e)}")
finally:
os.remove(cert_path)
data = r.json()
if not data.get("assets"):
raise ValueError(f"Could not find asset: {data}")
asset = [x for x in data["assets"] if x["drm"] == "widevine"][0]
rendition = asset["renditions"][0]
mpd_url = rendition["url"]
lic_url = asset["keyserver"]
parse = urlparse(mpd_url)
path = parse.path.split("/")
path[-1] = path[-1].split("-")[0].split("_")[0]
manifest = urlunparse(parse._replace(path="/".join(path)))
manifest += ".mpd" if not manifest.endswith("mpd") else ""
return manifest, lic_url
@@ -1,38 +0,0 @@
user_agent: Dalvik/2.1.0 (Linux; U; Android 14; SM-S901B Build/UP1A.231005.007)
endpoints:
base: https://corona.channel5.com
content: https://corona.channel5.com/shows/{show}.json?platform=my5android
episodes: https://corona.channel5.com/shows/{show}/episodes.json?platform=my5android
single: https://corona.channel5.com/shows/{show}/seasons/{season}/episodes/{episode}.json?platform=my5android
auth: https://cassie-auth.channel5.com/api/v2/media/my5androidhydradash/{title_id}.json
search: https://corona.channel5.com/shows/search.json
certificate: |
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlDdXpDQ0FpU2dBd0lCQWdJRVhMU1BGVEFOQmdrcWhraUc5dzBCQVFVRkFE
QmxNUXN3Q1FZRFZRUUdFd0pIDQpRakVWTUJNR0ExVUVCd3dNUkdWbVlYVnNkQ0JEYVhSNU1SSXdFQVlEVlFRS0RBbERhR0Z1Ym1W
c0lEVXhEekFODQpCZ05WQkFzTUJrTmhjM05wWlRFYU1CZ0dBMVVFQXd3UlEyRnpjMmxsSUUxbFpHbGhJRUYxZEdnd0hoY05NVGt3
DQpOREUxTVRRd016QXhXaGNOTWprd05ERTFNVFF3TXpBeFdqQ0JqakVMTUFrR0ExVUVCaE1DUjBJeEVqQVFCZ05WDQpCQW9NQ1VO
b1lXNXVaV3dnTlRFWE1CVUdBMVVFQ3d3T1EyRnpjMmxsSUdOc2FXVnVkSE14VWpCUUJnTlZCQU1NDQpTVU5oYzNOcFpTQlRaV3ht
TFhOcFoyNWxaQ0JEWlhKMGFXWnBZMkYwWlNCbWIzSWdUWGsxSUVGdVpISnZhV1FnDQpUbVY0ZENCSFpXNGdZMnhwWlc1MElERTFO
VFV6TXpZNU9ERXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBDQpNSUdKQW9HQkFNbVVTSHFCZ3pwbThXelVHZ2VDSWZvSTI3
QlovQmNmWktpbnl5dXFNVlpDNXRLaUtaRWpydFV4DQpoMXFVcDJSSkN3Ui9RcENPQ2RQdFhzMENzekZvd1ByTlY4RHFtUXZqbzY5
dlhvTEM3c2RLUjQ1cEFUQU8vY3JLDQorTUFPUXo1VWEyQ1ZrYnY1SCtaMVhWWndqbm1qNGJHZEJHM005b0NzQlVqTEh0bm1nQSty
QWdNQkFBR2pUakJNDQpNQjBHQTFVZERnUVdCQlNVVUhrY3JKNUVkVTVWM2ZJbXQra1ljdkdnZFRBTEJnTlZIUThFQkFNQ0E3Z3dD
UVlEDQpWUjBUQkFJd0FEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFOQmdrcWhraUc5dzBCQVFVRkFBT0JnUUFpDQpHNi84
OUFEaDhEOUs0OXZjeklMQ2pqbGh6bG5US09GM2l1Um0vSjZYaWtxY3RxSDF0a01na0FXcHAwQldBRm9IDQpJbU5WSEtKdTRnZXgy
cEtLejNqOVlRNG5EWENQVTdVb0N2aDl5TTNYT0RITWZRT01sZkRtMU9GZkh2QkJvSHNVDQpHSE9EQTkwQi8xcU0xSlFaZzBOVjZi
UllrUytCOWdtSFI4dXhtZktrL0E9PQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0KLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0t
LS0tDQpNSUlDZHdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0FtRXdnZ0pkQWdFQUFvR0JBTW1VU0hxQmd6cG04V3pVDQpHZ2VD
SWZvSTI3QlovQmNmWktpbnl5dXFNVlpDNXRLaUtaRWpydFV4aDFxVXAyUkpDd1IvUXBDT0NkUHRYczBDDQpzekZvd1ByTlY4RHFt
UXZqbzY5dlhvTEM3c2RLUjQ1cEFUQU8vY3JLK01BT1F6NVVhMkNWa2J2NUgrWjFYVlp3DQpqbm1qNGJHZEJHM005b0NzQlVqTEh0
bm1nQStyQWdNQkFBRUNnWUFjTVY4SnN6OTFWWnlDaWcreDZTTnpZdlhHDQo3bTd4bFBSeEdqYXlQclZ6eVJ1YmJnNitPKzFoNS9G
MFc4SWxwb21oOFdLUDhTMnl0RXBFQmhLbDRHN001WXdqDQp0SCtCVXFNMTNjbFdiQkxuQTZMT2RVeEVDTVhIUktjdHk5UE52UlJQ
cU9aV0YycDc5U1BFdFY5Q2o1SXNaVUdNDQpRcHYybk5oN1M2MUZGRVRuSVFKQkFPTXJNd2tnOGQzbksyS0lnVUNrcEtCRHlGTUJj
UXN0NG82VkxvVjNjenBwDQpxMW5FWGx4WnduMFh6Ni9GVjRWdTZYTjJLLzQxL2pCeWdTUlFXa05YVThNQ1FRRGpLYXVpdE1UajBM
ajU3QkJ3DQppNkNON0VFeUJSSkZaVGRSMDM4ZzkxSEFoUkVXVWpuQ0Vrc1UwcTl4TUNOdnM3OFN4RmQ1ODg5RUJQTnd1RDdvDQor
NTM1QWtFQTNwVTNYbHh2WUhQZktKNkR0cWtidlFSdFJoZUZnZVNsdGZzcUtCQVFVVTIwWFRKeEdwL0FWdjE3DQp1OGZxcDQwekpM
VEhDa0F4SFpzME9qYVpHcDU0TFFKQWJtM01iUjA1ZFpINnlpdlMxaE5hYW9QR01iMjdZeGJRDQpMS3dHNmd5d3BrbEp4RE1XdHR4
VHVYeXVJdlVHMVA5cFRJTThEeUhSeVR3cTU4bjVjeU1XYVFKQkFMVFRwZkVtDQoxdWhCeUd0NEtab3dYM2dhREpVZGU0ZjBwN3Ry
RFZGcExDNVJYcVVBQXNBQ2pzTHNYaEFadlovUEEwUDBiU2hmDQp4cUFRa2lnYmNKRXdxdjQ9DQotLS0tLUVORCBQUklWQVRFIEtF
WS0tLS0t
@@ -1,297 +0,0 @@
from __future__ import annotations
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
from functools import partial
from pathlib import Path
import sys
import re
import click
import webvtt
import requests
from click import Context
from bs4 import BeautifulSoup
from envied.core.credential import Credential
from envied.core.service import Service
from envied.core.titles import Movie, Movies, Episode, Series
from envied.core.tracks import Track, Chapter, Tracks, Subtitle
from envied.core.manifests.hls import HLS
class NebulaSubtitle(Subtitle):
STYLE_RE = re.compile('::cue\\(v\\[voice="(.+)"\\]\\) { color: ([^;]+); (.*)}')
RGB_RE = re.compile("rgb\\((.+), ?(.+), ?(.+)\\)")
def download(
self,
session: requests.Session,
prepare_drm: partial,
max_workers: Optional[int] = None,
progress: Optional[partial] = None
):
# Track.download chooses file extension based on class name so use
# this hack to keep it happy
self.__class__.__name__ = "Subtitle"
# Skip Subtitle.download and use Track.download directly. The pycaption
# calls in Subtitle.download are not needed here and mangle the WebVTT
# styling Nebula uses
Track.download(self, session, prepare_drm, max_workers, progress)
def convert(self, codec: Subtitle.Codec) -> Path:
if codec != Subtitle.Codec.SubRip:
return super().convert(codec)
output_path = self.path.with_suffix(f".{codec.value.lower()}")
vtt = webvtt.read(self.path)
styles = dict()
for group in vtt.styles:
for style in group.text.splitlines():
if match := self.STYLE_RE.match(style):
name, color, extra = match.groups()
if "rgb" in color:
r, g, b = self.RGB_RE.match(color).groups()
color = "#{0:02x}{1:02x}{2:02x}".format(int(r), int(g), int(b))
bold = "bold" in extra
styles[name.lower()] = {"color": color, "bold": bold}
count = 1
new_subs = []
for caption in vtt:
soup = BeautifulSoup(caption.raw_text, features="html.parser")
for tag in soup.find_all("v"):
name = " ".join(tag.attrs.keys())
# Work around a few broken "Abolish Everything" subtitles
if ((name == "spectator" and "spectator" not in styles) or
(name == "spectators" and "spectators" not in styles)):
name = "audience"
style = styles[name]
tag.name = "font"
tag.attrs = {"color": style["color"]}
if style["bold"]:
tag.wrap(soup.new_tag("b"))
text = str(soup)
new_subs.append(f"{count}")
new_subs.append(f"{caption.start} --> {caption.end}")
new_subs.append(f"{text}\n")
count += 1
output_path.write_text("\n".join(new_subs), encoding="utf8")
self.path = output_path
self.codec = codec
if callable(self.OnConverted):
self.OnConverted(codec)
return output_path
class NBLA(Service):
"""
Service code for Nebula (https://nebula.tv)
\b
Version: 1.0.0
Author: lambda
Authorization: Credentials
Robustness:
Unencrypted: 2160p, AAC2.0
"""
VIDEO_RE = r"https?://(?:www\.)?nebula\.tv/videos/(?P<slug>.+)"
CHANNEL_RE = r"^https?://(?:www\.)?nebula\.tv/(?P<slug>.+)"
@staticmethod
@click.command(name="NBLA", short_help="https://nebula.tv", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> NBLA:
return NBLA(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
cache = self.cache.get(f"key_{credential.sha1}")
if not cache or cache.expired:
self.log.info("Key is missing or expired, logging in...")
data = {
"email": credential.username,
"password": credential.password,
}
r = self.session.post(self.config["endpoints"]["login"], json=data)
r.raise_for_status()
key = r.json().get("key")
cache.set(key)
else:
key = cache.data
r = self.session.post(self.config["endpoints"]["authorization"], headers={"Authorization": f"Token {key}"})
r.raise_for_status()
self.jwt = r.json()["token"]
self.session.headers.update({"Authorization": f"Bearer {self.jwt}"})
def get_titles(self) -> Union[Movies, Series]:
if video_match := re.match(self.VIDEO_RE, self.title):
r = self.session.get(self.config["endpoints"]["video"].format(slug=video_match.group("slug")))
video = r.json()
# Simplest scenario: This is a video on a non-episodic channel, return it as movie
if video["channel_type"] != "episodic":
return Movies([
Movie(
id_=video["id"],
service=self.__class__,
name=video["title"],
year=video["published_at"][0:4],
language="en"
)
])
# For episodic videos, things are trickier: There is no way to get the season
# and episode number from the video endpoint, so we instead have to iterate
# through all seasons and filter for the video id.
return self.get_content(video["channel_slug"], video_id_filter=video["id"])
# If the link did not match the video regex, try using it as slug for the content
# API to fetch a whole channel/season
elif channel_match := re.match(self.CHANNEL_RE, self.title):
return self.get_content(channel_match.group("slug"))
def get_tracks(self, title: Union[Episode, Movie]) -> Tracks:
r = self.session.get(self.config["endpoints"]["manifest"].format(video_id=title.id, jwt=self.jwt), allow_redirects=False)
manifest_url = r.headers["Location"]
tracks = HLS.from_url(manifest_url).to_tracks(title.language)
subs = []
for subtitle in tracks.subtitles:
subs.append(NebulaSubtitle(
id_=subtitle.id,
url=subtitle.url,
language=subtitle.language,
is_original_lang=subtitle.is_original_lang,
descriptor=subtitle.descriptor,
name=subtitle.name,
codec=subtitle.codec,
forced=subtitle.forced,
sdh=subtitle.sdh,
))
tracks.subtitles = subs
return tracks
def get_chapters(self, title: Union[Episode, Movie]) -> list[Chapter]:
return []
def search(self) -> Generator[SearchResult, None, None]:
pass
#self.title
r = self.session.get(self.config["endpoints"]["search"], params=params)
r.raise_for_status()
# for result in results["results"]:
# yield SearchResult(
# id_=result["brand"].get("websafeTitle"),
# title=result["brand"].get("title"),
# description=result["brand"].get("description"),
# label=result.get("label"),
# url=result["brand"].get("href"),
# )
### Service specific functions
def season_to_episodes(self, channel, season, video_id_filter):
try:
season_number = int(season["label"])
except ValueError:
# Some shows such have some non-integer season numbers (Such as
# Jet Lag: The Game season 13.5). These are generally listed as specials
# (Season 0) on TMDB, so treat them the same way.
#
# Specials episode numbers will then likely be off, use caution and
# check TMDB for manual corrections.
season_number = 0
self.log.warn(f"Could not extract season information, guessing season {season_number}")
for episode_number, episode in enumerate(season["episodes"], start=1):
if not episode["video"] or (video_id_filter and video_id_filter != episode["video"]["id"]):
continue
yield Episode(
id_=episode["video"]["id"],
service=self.__class__,
title=channel["title"],
name=episode["title"],
language="en",
year=episode["video"]["published_at"][0:4],
season=season_number,
number=episode_number,
)
def get_content(self, slug, video_id_filter=None):
r = self.session.get(self.config["endpoints"]["content"].format(slug=slug))
content = r.json()
if content["type"] == "season":
r = self.session.get(self.config["endpoints"]["content"].format(slug=content["video_channel_slug"]))
channel = r.json()
return Series(self.season_to_episodes(channel, content, video_id_filter))
elif content["type"] == "video_channel" and content["channel_type"] == "episodic":
episodes = []
for season_data in content["episodic"]["seasons"]:
# We could also use the generic content endpoint to retrieve
# seasons, but this is how the nebula web app does it.
r = self.session.get(self.config["endpoints"]["season"].format(id=season_data["id"]))
episodes.extend(self.season_to_episodes(content, r.json(), video_id_filter))
return Series(episodes)
elif content["type"] == "video_channel":
self.log.error("Non-episodic channel URL passed. Treating it as a show with a single season. If you want to download non-episodic content as a movie, pass the direct video URL instead.")
r = self.session.get(self.config["endpoints"]["video_channel_episodes"].format(id=content["id"]))
episodes = r.json()['results']
# Non-episodic channel names tend to have a format of "Creator Name — Show Name"
if "" in content["title"]:
show_title = content["title"].split("", maxsplit=1)[1]
else:
show_title = content["title"]
season = []
episode_number = 0
for episode in episodes:
if 'trailer' in episode['title'].lower():
continue
episode_number += 1
season.append(Episode(
id_=episode["id"],
service=self.__class__,
title=show_title,
name=episode["title"],
language="en",
year=episode["published_at"][0:4],
season=1,
number=episode_number,
))
return Series(season)
else:
self.log.error("Unsupported content type")
sys.exit(1)
@@ -1,13 +0,0 @@
headers:
Accept-Language: en-US,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
endpoints:
login: https://nebula.tv/auth/login/
authorization: https://users.api.nebula.app/api/v1/authorization/
content: https://content.api.nebula.app/content/{slug}/
season: https://content.api.nebula.app/seasons/{id}/
video: https://content.api.nebula.app/content/videos/{slug}/
video_channel: https://content.api.nebula.app/video_channels/{id}/
video_channel_episodes: https://content.api.nebula.app/video_channels/{id}/video_episodes/?ordering=published_at
manifest: https://content.api.nebula.app/video_episodes/{video_id}/manifest.m3u8?token={jwt}&app_version=25.2.1&platform=web
@@ -1,10 +0,0 @@
from .MSLObject import MSLObject
class MSLKeys(MSLObject):
def __init__(self, encryption=None, sign=None, rsa=None, mastertoken=None, cdm_session=None):
self.encryption = encryption
self.sign = sign
self.rsa = rsa
self.mastertoken = mastertoken
self.cdm_session = cdm_session
@@ -1,6 +0,0 @@
import jsonpickle
class MSLObject:
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, jsonpickle.encode(self, unpicklable=False))
@@ -1,408 +0,0 @@
import base64
import gzip
import json
import logging
import os
import random
import re
import sys
import time
import zlib
from datetime import datetime
from io import BytesIO
import jsonpickle
import requests
from Cryptodome.Cipher import AES, PKCS1_OAEP
from Cryptodome.Hash import HMAC, SHA256
from Cryptodome.PublicKey import RSA
from Cryptodome.Random import get_random_bytes
from Cryptodome.Util import Padding
from envied.core.cacher import Cacher
from .MSLKeys import MSLKeys
from .schemes import EntityAuthenticationSchemes # noqa: F401
from .schemes import KeyExchangeSchemes
from .schemes.EntityAuthentication import EntityAuthentication
from .schemes.KeyExchangeRequest import KeyExchangeRequest
# from vinetrimmer.utils.widevine.device import RemoteDevice
class MSL:
log = logging.getLogger("MSL")
def __init__(self, session, endpoint, sender, keys, message_id, user_auth=None):
self.session = session
self.endpoint = endpoint
self.sender = sender
self.keys = keys
self.user_auth = user_auth
self.message_id = message_id
@classmethod
def handshake(cls, scheme: KeyExchangeSchemes, session: requests.Session, endpoint: str, sender: str, cache: Cacher):
cache = cache.get(sender)
message_id = random.randint(0, pow(2, 52))
msl_keys = MSL.load_cache_data(cache)
if msl_keys is not None:
cls.log.info("Using cached MSL data")
else:
msl_keys = MSLKeys()
if scheme != KeyExchangeSchemes.Widevine:
msl_keys.rsa = RSA.generate(2048)
# if not cdm:
# raise cls.log.exit("- No cached data and no CDM specified")
# if not msl_keys_path:
# raise cls.log.exit("- No cached data and no MSL key path specified")
# Key Exchange Scheme Widevine currently not implemented
# if scheme == KeyExchangeSchemes.Widevine:
# msl_keys.cdm_session = cdm.open(
# pssh=b"\x0A\x7A\x00\x6C\x38\x2B",
# raw=True,
# offline=True
# )
# keyrequestdata = KeyExchangeRequest.Widevine(
# keyrequest=cdm.get_license_challenge(msl_keys.cdm_session)
# )
# else:
keyrequestdata = KeyExchangeRequest.AsymmetricWrapped(
keypairid="superKeyPair",
mechanism="JWK_RSA",
publickey=msl_keys.rsa.publickey().exportKey(format="DER")
)
data = jsonpickle.encode({
"entityauthdata": EntityAuthentication.Unauthenticated(sender),
"headerdata": base64.b64encode(MSL.generate_msg_header(
message_id=message_id,
sender=sender,
is_handshake=True,
keyrequestdata=keyrequestdata
).encode("utf-8")).decode("utf-8"),
"signature": ""
}, unpicklable=False)
data += json.dumps({
"payload": base64.b64encode(json.dumps({
"messageid": message_id,
"data": "",
"sequencenumber": 1,
"endofmsg": True
}).encode("utf-8")).decode("utf-8"),
"signature": ""
})
try:
r = session.post(
url=endpoint,
data=data
)
except requests.HTTPError as e:
raise cls.log.exit(f"- Key exchange failed, response data is unexpected: {e.response.text}")
key_exchange = r.json() # expecting no payloads, so this is fine
if "errordata" in key_exchange:
raise cls.log.exit("- Key exchange failed: " + json.loads(base64.b64decode(
key_exchange["errordata"]
).decode())["errormsg"])
# parse the crypto keys
key_response_data = json.JSONDecoder().decode(base64.b64decode(
key_exchange["headerdata"]
).decode("utf-8"))["keyresponsedata"]
if key_response_data["scheme"] != str(scheme):
raise cls.log.exit("- Key exchange scheme mismatch occurred")
key_data = key_response_data["keydata"]
# if scheme == KeyExchangeSchemes.Widevine:
# if isinstance(cdm.device, RemoteDevice):
# msl_keys.encryption, msl_keys.sign = cdm.device.exchange(
# cdm.sessions[msl_keys.cdm_session],
# license_res=key_data["cdmkeyresponse"],
# enc_key_id=base64.b64decode(key_data["encryptionkeyid"]),
# hmac_key_id=base64.b64decode(key_data["hmackeyid"])
# )
# cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"])
# else:
# cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"])
# keys = cdm.get_keys(msl_keys.cdm_session)
# msl_keys.encryption = MSL.get_widevine_key(
# kid=base64.b64decode(key_data["encryptionkeyid"]),
# keys=keys,
# permissions=["AllowEncrypt", "AllowDecrypt"]
# )
# msl_keys.sign = MSL.get_widevine_key(
# kid=base64.b64decode(key_data["hmackeyid"]),
# keys=keys,
# permissions=["AllowSign", "AllowSignatureVerify"]
# )
# else:
cipher_rsa = PKCS1_OAEP.new(msl_keys.rsa)
msl_keys.encryption = MSL.base64key_decode(
json.JSONDecoder().decode(cipher_rsa.decrypt(
base64.b64decode(key_data["encryptionkey"])
).decode("utf-8"))["k"]
)
msl_keys.sign = MSL.base64key_decode(
json.JSONDecoder().decode(cipher_rsa.decrypt(
base64.b64decode(key_data["hmackey"])
).decode("utf-8"))["k"]
)
msl_keys.mastertoken = key_response_data["mastertoken"]
MSL.cache_keys(msl_keys, cache)
cls.log.info("MSL handshake successful")
return cls(
session=session,
endpoint=endpoint,
sender=sender,
keys=msl_keys,
message_id=message_id
)
@staticmethod
def load_cache_data(cacher: Cacher):
if not cacher or cacher == {}:
return None
# with open(msl_keys_path, encoding="utf-8") as fd:
# msl_keys = jsonpickle.decode(fd.read())
msl_keys = jsonpickle.decode(cacher.data)
if msl_keys.rsa:
# noinspection PyTypeChecker
# expects RsaKey, but is a string, this is because jsonpickle can't pickle RsaKey object
# so as a workaround it exports to PEM, and then when reading, it imports that PEM back
# to an RsaKey :)
msl_keys.rsa = RSA.importKey(msl_keys.rsa)
# If it's expired or close to, return None as it's unusable
if msl_keys.mastertoken and ((datetime.utcfromtimestamp(int(json.JSONDecoder().decode(
base64.b64decode(msl_keys.mastertoken["tokendata"]).decode("utf-8")
)["expiration"])) - datetime.now()).total_seconds() / 60 / 60) < 10:
return None
return msl_keys
@staticmethod
def cache_keys(msl_keys, cache: Cacher):
# os.makedirs(os.path.dirname(cache), exist_ok=True)
if msl_keys.rsa:
# jsonpickle can't pickle RsaKey objects :(
msl_keys.rsa = msl_keys.rsa.export_key()
# with open(cache, "w", encoding="utf-8") as fd:
# fd.write()
cache.set(jsonpickle.encode(msl_keys))
if msl_keys.rsa:
# re-import now
msl_keys.rsa = RSA.importKey(msl_keys.rsa)
@staticmethod
def generate_msg_header(message_id, sender, is_handshake, userauthdata=None, keyrequestdata=None,
compression="GZIP"):
"""
The MSL header carries all MSL data used for entity and user authentication, message encryption
and verification, and service tokens. Portions of the MSL header are encrypted.
https://github.com/Netflix/msl/wiki/Messages#header-data
:param message_id: number against which payload chunks are bound to protect against replay.
:param sender: ESN
:param is_handshake: This flag is set true if the message is a handshake message and will not include any
payload chunks. It will include keyrequestdata.
:param userauthdata: UserAuthData
:param keyrequestdata: KeyRequestData
:param compression: Supported compression algorithms.
:return: The base64 encoded JSON String of the header
"""
header_data = {
"messageid": message_id,
"renewable": True, # MUST be True if is_handshake
"handshake": is_handshake,
"capabilities": {
"compressionalgos": [compression] if compression else [],
"languages": ["en-US"], # bcp-47
"encoderformats": ["JSON"]
},
"timestamp": int(time.time()),
# undocumented or unused:
"sender": sender,
"nonreplayable": False,
"recipient": "Netflix",
}
if userauthdata:
header_data["userauthdata"] = userauthdata
if keyrequestdata:
header_data["keyrequestdata"] = [keyrequestdata]
return jsonpickle.encode(header_data, unpicklable=False)
@classmethod
def get_widevine_key(cls, kid, keys, permissions):
for key in keys:
if key.kid != kid:
continue
if key.type != "OPERATOR_SESSION":
cls.log.warning(f"Widevine Key Exchange: Wrong key type (not operator session) key {key}")
continue
if not set(permissions) <= set(key.permissions):
cls.log.warning(f"Widevine Key Exchange: Incorrect permissions, key {key}, needed perms {permissions}")
continue
return key.key
return None
def send_message(self, endpoint, params, application_data, userauthdata=None):
message = self.create_message(application_data, userauthdata)
res = self.session.post(url=endpoint, data=message, params=params)
header, payload_data = self.parse_message(res.text)
if "errordata" in header:
raise self.log.exit(
"- MSL response message contains an error: {}".format(
json.loads(base64.b64decode(header["errordata"].encode("utf-8")).decode("utf-8"))
)
)
return header, payload_data
def create_message(self, application_data, userauthdata=None):
self.message_id += 1 # new message must ue a new message id
headerdata = self.encrypt(self.generate_msg_header(
message_id=self.message_id,
sender=self.sender,
is_handshake=False,
userauthdata=userauthdata
))
header = json.dumps({
"headerdata": base64.b64encode(headerdata.encode("utf-8")).decode("utf-8"),
"signature": self.sign(headerdata).decode("utf-8"),
"mastertoken": self.keys.mastertoken
})
payload_chunks = [self.encrypt(json.dumps({
"messageid": self.message_id,
"data": self.gzip_compress(json.dumps(application_data).encode("utf-8")).decode("utf-8"),
"compressionalgo": "GZIP",
"sequencenumber": 1, # todo ; use sequence_number from master token instead?
"endofmsg": True
}))]
message = header
for payload_chunk in payload_chunks:
message += json.dumps({
"payload": base64.b64encode(payload_chunk.encode("utf-8")).decode("utf-8"),
"signature": self.sign(payload_chunk).decode("utf-8")
})
return message
def decrypt_payload_chunks(self, payload_chunks):
"""
Decrypt and extract data from payload chunks
:param payload_chunks: List of payload chunks
:return: json object
"""
raw_data = ""
for payload_chunk in payload_chunks:
# todo ; verify signature of payload_chunk["signature"] against payload_chunk["payload"]
# expecting base64-encoded json string
payload_chunk = json.loads(base64.b64decode(payload_chunk["payload"]).decode("utf-8"))
# decrypt the payload
payload_decrypted = AES.new(
key=self.keys.encryption,
mode=AES.MODE_CBC,
iv=base64.b64decode(payload_chunk["iv"])
).decrypt(base64.b64decode(payload_chunk["ciphertext"]))
payload_decrypted = Padding.unpad(payload_decrypted, 16)
payload_decrypted = json.loads(payload_decrypted.decode("utf-8"))
# decode and uncompress data if compressed
payload_data = base64.b64decode(payload_decrypted["data"])
if payload_decrypted.get("compressionalgo") == "GZIP":
payload_data = zlib.decompress(payload_data, 16 + zlib.MAX_WBITS)
raw_data += payload_data.decode("utf-8")
data = json.loads(raw_data)
if "error" in data:
error = data["error"]
error_display = error.get("display")
error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", ""))
if error_display:
self.log.critical(f"- {error_display}")
if error_detail:
self.log.critical(f"- {error_detail}")
if not (error_display or error_detail):
self.log.critical(f"- {error}")
# sys.exit(1)
return data["result"]
def parse_message(self, message):
"""
Parse an MSL message into a header and list of payload chunks
:param message: MSL message
:returns: a 2-item tuple containing message and list of payload chunks if available
"""
parsed_message = json.loads("[{}]".format(message.replace("}{", "},{")))
header = parsed_message[0]
encrypted_payload_chunks = parsed_message[1:] if len(parsed_message) > 1 else []
if encrypted_payload_chunks:
payload_chunks = self.decrypt_payload_chunks(encrypted_payload_chunks)
else:
payload_chunks = {}
return header, payload_chunks
@staticmethod
def gzip_compress(data):
out = BytesIO()
with gzip.GzipFile(fileobj=out, mode="w") as fd:
fd.write(data)
return base64.b64encode(out.getvalue())
@staticmethod
def base64key_decode(payload):
length = len(payload) % 4
if length == 2:
payload += "=="
elif length == 3:
payload += "="
elif length != 0:
raise ValueError("Invalid base64 string")
return base64.urlsafe_b64decode(payload.encode("utf-8"))
def encrypt(self, plaintext):
"""
Encrypt the given Plaintext with the encryption key
:param plaintext:
:return: Serialized JSON String of the encryption Envelope
"""
iv = get_random_bytes(16)
return json.dumps({
"ciphertext": base64.b64encode(
AES.new(
self.keys.encryption,
AES.MODE_CBC,
iv
).encrypt(
Padding.pad(plaintext.encode("utf-8"), 16)
)
).decode("utf-8"),
"keyid": "{}_{}".format(self.sender, json.loads(
base64.b64decode(self.keys.mastertoken["tokendata"]).decode("utf-8")
)["sequencenumber"]),
"sha256": "AA==",
"iv": base64.b64encode(iv).decode("utf-8")
})
def sign(self, text):
"""
Calculates the HMAC signature for the given text with the current sign key and SHA256
:param text:
:return: Base64 encoded signature
"""
return base64.b64encode(HMAC.new(self.keys.sign, text.encode("utf-8"), SHA256).digest())
@@ -1,59 +0,0 @@
from .. import EntityAuthenticationSchemes
from ..MSLObject import MSLObject
# noinspection PyPep8Naming
class EntityAuthentication(MSLObject):
def __init__(self, scheme, authdata):
"""
Data used to identify and authenticate the entity associated with a message.
https://github.com/Netflix/msl/wiki/Entity-Authentication-%28Configuration%29
:param scheme: Entity Authentication Scheme identifier
:param authdata: Entity Authentication data
"""
self.scheme = str(scheme)
self.authdata = authdata
@classmethod
def Unauthenticated(cls, identity):
"""
The unauthenticated entity authentication scheme does not provide encryption or authentication and only
identifies the entity. Therefore entity identities can be harvested and spoofed. The benefit of this
authentication scheme is that the entity has control over its identity. This may be useful if the identity is
derived from or related to other data, or if retaining the identity is desired across state resets or in the
event of MSL errors requiring entity re-authentication.
"""
return cls(
scheme=EntityAuthenticationSchemes.Unauthenticated,
authdata={"identity": identity}
)
@classmethod
def Widevine(cls, devtype, keyrequest):
"""
The Widevine entity authentication scheme is used by devices with the Widevine CDM. It does not provide
encryption or authentication and only identifies the entity. Therefore entity identities can be harvested
and spoofed. The entity identity is composed from the provided device type and Widevine key request data. The
Widevine CDM properties can be extracted from the key request data.
When coupled with the Widevine key exchange scheme, the entity identity can be cryptographically validated by
comparing the entity authentication key request data against the key exchange key request data.
Note that the local entity will not know its entity identity when using this scheme.
> Devtype
An arbitrary value identifying the device type the local entity wishes to assume. The data inside the Widevine
key request may be optionally used to validate the claimed device type.
:param devtype: Local entity device type
:param keyrequest: Widevine key request
"""
return cls(
scheme=EntityAuthenticationSchemes.Widevine,
authdata={
"devtype": devtype,
"keyrequest": keyrequest
}
)
@@ -1,80 +0,0 @@
import base64
from .. import KeyExchangeSchemes
from ..MSLObject import MSLObject
# noinspection PyPep8Naming
class KeyExchangeRequest(MSLObject):
def __init__(self, scheme, keydata):
"""
Session key exchange data from a requesting entity.
https://github.com/Netflix/msl/wiki/Key-Exchange-%28Configuration%29
:param scheme: Key Exchange Scheme identifier
:param keydata: Key Request data
"""
self.scheme = str(scheme)
self.keydata = keydata
@classmethod
def AsymmetricWrapped(cls, keypairid, mechanism, publickey):
"""
Asymmetric wrapped key exchange uses a generated ephemeral asymmetric key pair for key exchange. It will
typically be used when there is no other data or keys from which to base secure key exchange.
This mechanism provides perfect forward secrecy but does not guarantee that session keys will only be available
to the requesting entity if the requesting MSL stack has been modified to perform the operation on behalf of a
third party.
> Key Pair ID
The key pair ID is included as a sanity check.
> Mechanism & Public Key
The following mechanisms are associated public key formats are currently supported.
Field Public Key Format Description
RSA SPKI RSA-OAEP encrypt/decrypt
ECC SPKI ECIES encrypt/decrypt
JWEJS_RSA SPKI RSA-OAEP JSON Web Encryption JSON Serialization
JWE_RSA SPKI RSA-OAEP JSON Web Encryption Compact Serialization
JWK_RSA SPKI RSA-OAEP JSON Web Key
JWK_RSAES SPKI RSA PKCS#1 JSON Web Key
:param keypairid: key pair ID
:param mechanism: asymmetric key type
:param publickey: public key
"""
return cls(
scheme=KeyExchangeSchemes.AsymmetricWrapped,
keydata={
"keypairid": keypairid,
"mechanism": mechanism,
"publickey": base64.b64encode(publickey).decode("utf-8")
}
)
@classmethod
def Widevine(cls, keyrequest):
"""
Google Widevine provides a secure key exchange mechanism. When requested the Widevine component will issue a
one-time use key request. The Widevine server library can be used to authenticate the request and return
randomly generated symmetric keys in a protected key response bound to the request and Widevine client library.
The key response also specifies the key identities, types and their permitted usage.
The Widevine key request also contains a model identifier and a unique device identifier with an expectation of
long-term persistence. These values are available from the Widevine client library and can be retrieved from
the key request by the Widevine server library.
The Widevine client library will protect the returned keys from inspection or misuse.
:param keyrequest: Base64-encoded Widevine CDM license challenge (PSSH: b'\x0A\x7A\x00\x6C\x38\x2B')
"""
if not isinstance(keyrequest, str):
keyrequest = base64.b64encode(keyrequest).decode()
return cls(
scheme=KeyExchangeSchemes.Widevine,
keydata={"keyrequest": keyrequest}
)
@@ -1,59 +0,0 @@
from ..MSLObject import MSLObject
from . import UserAuthenticationSchemes
# noinspection PyPep8Naming
class UserAuthentication(MSLObject):
def __init__(self, scheme, authdata):
"""
Data used to identify and authenticate the user associated with a message.
https://github.com/Netflix/msl/wiki/User-Authentication-%28Configuration%29
:param scheme: User Authentication Scheme identifier
:param authdata: User Authentication data
"""
self.scheme = str(scheme)
self.authdata = authdata
@classmethod
def EmailPassword(cls, email, password):
"""
Email and password is a standard user authentication scheme in wide use.
:param email: user email address
:param password: user password
"""
return cls(
scheme=UserAuthenticationSchemes.EmailPassword,
authdata={
"email": email,
"password": password
}
)
@classmethod
def NetflixIDCookies(cls, netflixid, securenetflixid):
"""
Netflix ID HTTP cookies are used when the user has previously logged in to a web site. Possession of the
cookies serves as proof of user identity, in the same manner as they do when communicating with the web site.
The Netflix ID cookie and Secure Netflix ID cookie are HTTP cookies issued by the Netflix web site after
subscriber login. The Netflix ID cookie is encrypted and identifies the subscriber and analogous to a
subscribers username. The Secure Netflix ID cookie is tied to a Netflix ID cookie and only sent over HTTPS
and analogous to a subscribers password.
In some cases the Netflix ID and Secure Netflix ID cookies will be unavailable to the MSL stack or application.
If either or both of the Netflix ID or Secure Netflix ID cookies are absent in the above data structure the
HTTP cookie headers will be queried for it; this is only acceptable when HTTPS is used as the underlying
transport protocol.
:param netflixid: Netflix ID cookie
:param securenetflixid: Secure Netflix ID cookie
"""
return cls(
scheme=UserAuthenticationSchemes.NetflixIDCookies,
authdata={
"netflixid": netflixid,
"securenetflixid": securenetflixid
}
)
@@ -1,24 +0,0 @@
from enum import Enum
class Scheme(Enum):
def __str__(self):
return str(self.value)
class EntityAuthenticationSchemes(Scheme):
"""https://github.com/Netflix/msl/wiki/Entity-Authentication-%28Configuration%29"""
Unauthenticated = "NONE"
Widevine = "WIDEVINE"
class UserAuthenticationSchemes(Scheme):
"""https://github.com/Netflix/msl/wiki/User-Authentication-%28Configuration%29"""
EmailPassword = "EMAIL_PASSWORD"
NetflixIDCookies = "NETFLIXID"
class KeyExchangeSchemes(Scheme):
"""https://github.com/Netflix/msl/wiki/Key-Exchange-%28Configuration%29"""
AsymmetricWrapped = "ASYMMETRIC_WRAPPED"
Widevine = "WIDEVINE"
@@ -1,978 +0,0 @@
import base64
from datetime import datetime
import json
from math import e
import random
import sys
import time
import typing
from uuid import UUID
import click
import re
from typing import List, Literal, Optional, Set, Union, Tuple
from http.cookiejar import CookieJar
from itertools import zip_longest
from Crypto.Random import get_random_bytes
import jsonpickle
from pymp4.parser import Box
from pywidevine import PSSH, Cdm
import requests
from langcodes import Language
from pathlib import Path
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.drm.widevine import Widevine
from envied.core.service import Service
from envied.core.titles import Titles_T, Title_T
from envied.core.titles.episode import Episode, Series
from envied.core.titles.movie import Movie, Movies
from envied.core.titles.title import Title
from envied.core.tracks import Tracks, Chapters, Hybrid
from envied.core.tracks.audio import Audio
from envied.core.tracks.chapter import Chapter
from envied.core.tracks.subtitle import Subtitle
from envied.core.tracks.track import Track
from envied.core.tracks.video import Video
from envied.core.utils.collections import flatten, as_list
from envied.core.tracks.attachment import Attachment
from envied.core.drm.playready import PlayReady
from envied.core.titles.song import Song
from envied.utils.base62 import decode
from .MSL import MSL, KeyExchangeSchemes
from .MSL.schemes.UserAuthentication import UserAuthentication
class NF(Service):
"""
Service for https://netflix.com
Version: 1.0.0
Authorization: Cookies
Security: UHD@SL3000/L1 FHD@SL3000/L1
"""
TITLE_RE = [
r"^(?:https?://(?:www\.)?netflix\.com(?:/[a-z0-9]{2})?/(?:title/|watch/|.+jbv=))?(?P<id>\d+)",
r"^https?://(?:www\.)?unogs\.com/title/(?P<id>\d+)",
]
ALIASES= ("NF", "Netflix")
NF_LANG_MAP = {
"es": "es-419",
"pt": "pt-PT",
}
@staticmethod
@click.command(name="Netflix", short_help="https://netflix.com")
@click.argument("title", type=str)
@click.option("-drm", "--drm-system", type=click.Choice(["widevine", "playready"], case_sensitive=False),
default="widevine",
help="which drm system to use")
@click.option("-p", "--profile", type=click.Choice(["MPL", "HPL", "QC", "MPL+HPL", "MPL+HPL+QC", "MPL+QC"], case_sensitive=False),
default=None,
help="H.264 profile to use. Default is best available.")
@click.option("--meta-lang", type=str, help="Language to use for metadata")
@click.option("-ht","--hydrate-track", is_flag=True, default=False, help="Hydrate missing audio and subtitle.")
@click.option("-hb", "--high-bitrate", is_flag=True, default=False, help="Get more video bitrate")
@click.pass_context
def cli(ctx, **kwargs):
return NF(ctx, **kwargs)
def __init__(self, ctx: click.Context, title: str, drm_system: Literal["widevine", "playready"], profile: str, meta_lang: str, hydrate_track: bool, high_bitrate: bool):
super().__init__(ctx)
# General
self.title = title
self.profile = profile
self.meta_lang = meta_lang
self.hydrate_track = hydrate_track
self.drm_system = drm_system
self.profiles: List[str] = []
self.requested_profiles: List[str] = []
self.high_bitrate = high_bitrate
# MSL
self.esn = self.cache.get("ESN")
self.msl: Optional[MSL] = None
self.userauthdata = None
# Download options
self.range = ctx.parent.params.get("range_") or [Video.Range.SDR]
self.vcodec = ctx.parent.params.get("vcodec") or Video.Codec.AVC # Defaults to H264
self.acodec : Audio.Codec = ctx.parent.params.get("acodec") or Audio.Codec.EC3
self.quality: List[int] = ctx.parent.params.get("quality")
self.audio_only = ctx.parent.params.get("audio_only")
self.subs_only = ctx.parent.params.get("subs_only")
self.chapters_only = ctx.parent.params.get("chapters_only")
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
# Configure first before download
self.log.debug("Authenticating Netflix service")
auth = super().authenticate(cookies, credential)
if not cookies:
raise EnvironmentError("Service requires Cookies for Authentication.")
self.configure()
return auth
def get_titles(self) -> Titles_T:
metadata = self.get_metadata(self.title)
# self.log.info(f"Metadata: {jsonpickle.encode(metadata, indent=2)}")
if "video" not in metadata:
self.log.error(f"Failed to get metadata: {metadata}")
sys.exit(1)
titles: Titles_T | None = None
if metadata["video"]["type"] == "movie":
movie = Movie(
id_=self.title,
name=metadata["video"]["title"],
year=metadata["video"]["year"],
service=self.__class__,
data=metadata["video"],
description=metadata["video"]["synopsis"]
)
titles = Movies([
movie
])
else:
# self.log.info(f"Episodes: {jsonpickle.encode(episodes, indent=2)}")
episode_list: List[Episode] = []
for season in metadata["video"]["seasons"]:
for episode in season["episodes"]:
episode_list.append(
Episode(
id_=self.title,
title=metadata["video"]["title"],
year=season["year"],
service=self.__class__,
season=season["seq"],
number=episode["seq"],
name=episode["title"],
data=episode,
description=episode["synopsis"],
)
)
titles = Series(episode_list)
return titles
def get_tracks(self, title: Title_T) -> Tracks:
tracks = Tracks()
def mark_repack(track_group):
# mark videos + audio
for t in track_group.videos + track_group.audio:
t.needs_repack = True
# mark subtitles
for t in getattr(track_group, "subtitles", []):
t.needs_repack = True
# -------------------------------
# Parse manifests / fetch tracks
# -------------------------------
if self.vcodec == Video.Codec.AVC:
try:
manifest = self.get_manifest(title, self.profiles)
movie_track = self.manifest_as_tracks(manifest, title, self.hydrate_track)
mark_repack(movie_track)
tracks.add(movie_track)
if self.profile is not None:
self.log.info(f"Requested profiles: {self.profile}")
else:
qc_720_profile = [
x for x in self.config["profiles"]["video"][self.vcodec.extension.upper()]["QC"]
if "l40" not in x and 720 in self.quality
]
# QC profiles
qc_manifest = self.get_manifest(
title,
qc_720_profile if 720 in self.quality
else self.config["profiles"]["video"][self.vcodec.extension.upper()]["QC"]
)
qc_tracks = self.manifest_as_tracks(qc_manifest, title, False)
mark_repack(qc_tracks)
tracks.add(qc_tracks.videos)
# MPL Profiles
mpl_manifest = self.get_manifest(
title,
[x for x in self.config["profiles"]["video"][self.vcodec.extension.upper()]["MPL"]
if "l40" not in x]
)
mpl_tracks = self.manifest_as_tracks(mpl_manifest, title, False)
mark_repack(mpl_tracks)
tracks.add(mpl_tracks.videos)
except Exception as e:
self.log.error(e)
else:
# HEVC / DV / HDR mode
if self.high_bitrate:
splitted_profiles = self.split_profiles(self.profiles)
for index, profile_list in enumerate(splitted_profiles):
try:
self.log.debug(f"Index: {index}. Getting profiles: {profile_list}")
manifest = self.get_manifest(title, profile_list)
manifest_tracks = self.manifest_as_tracks(
manifest,
title,
self.hydrate_track if index == 0 else False
)
mark_repack(manifest_tracks)
tracks.add(manifest_tracks if index == 0 else manifest_tracks.videos)
except Exception:
self.log.error(f"Error getting profile: {profile_list}. Skipping")
continue
else:
try:
manifest = self.get_manifest(title, self.profiles)
manifest_tracks = self.manifest_as_tracks(manifest, title, self.hydrate_track)
mark_repack(manifest_tracks)
tracks.add(manifest_tracks)
except Exception as e:
self.log.error(e)
# --------------------------------------------------------
# 🧩 HYBRID DV+HDR Injection (copied from 1st script)
# --------------------------------------------------------
video_ranges = [v.range for v in tracks.videos]
has_dv = Video.Range.DV in video_ranges
has_hdr10 = Video.Range.HDR10 in video_ranges
has_hdr10p = Video.Range.HDR10P in video_ranges
if self.range[0] == Video.Range.HYBRID and has_hdr10 and (has_dv or has_hdr10p):
try:
self.log.info("Performing HYBRID DV+HDR injection...")
hdr_video = next((v for v in tracks.videos if v.range == Video.Range.HDR10), None)
dv_video = next((v for v in tracks.videos if v.range in (Video.Range.DV, Video.Range.HDR10P)), None)
if not hdr_video or not dv_video:
raise Exception("Missing HDR10 or DV video track for hybrid merge")
# Ensure both files exist before injection
def ensure_local_file(video):
if not getattr(video, "path", None) or not os.path.exists(video.path):
temp_path = config.directories.temp / f"{video.id}.hevc"
self.log.info(f"Downloading temporary stream for {video.range}{temp_path.name}")
with self.session.get(video.url, stream=True) as r:
r.raise_for_status()
with open(temp_path, "wb") as f:
for chunk in r.iter_content(chunk_size=1024 * 1024):
f.write(chunk)
video.path = temp_path
return video.path
ensure_local_file(hdr_video)
ensure_local_file(dv_video)
# Perform hybrid merge
Hybrid([hdr_video, dv_video], self.__class__.__name__.lower())
injected_path = config.directories.temp / "HDR10-DV.hevc"
self.log.info(f"Hybrid file created → {injected_path}")
# Replace HDR10 with merged track
hdr_video.range = Video.Range.DV
hdr_video.path = injected_path
except Exception as e:
self.log.warning(f"Hybrid injection failed: {e}")
# --------------------------------------------------------
# Disable proxy for all tracks
# --------------------------------------------------------
for track in tracks:
track.needs_proxy = False
# --------------------------------------------------------
# Add Attachments + Save poster
# --------------------------------------------------------
try:
if isinstance(title, Movie):
poster_url = title.data["boxart"][0]["url"]
else:
poster_url = title.data["stills"][0]["url"]
# Temp directory
temp_dir = Path(self.config.get("directories", {}).get("Downloads", "./Downloads"))
temp_dir.mkdir(parents=True, exist_ok=True)
poster_path = temp_dir / "poster.jpg"
# Save poster locally
try:
resp = requests.get(poster_url, timeout=15)
if resp.status_code == 200:
with open(poster_path, "wb") as f:
f.write(resp.content)
except Exception as e:
self.log.error(f"Failed to save poster.jpg: {e}")
# Create attachment
attachment = Attachment.from_url(url=poster_url)
attachment.filename = str(poster_path)
tracks.add(attachment)
except Exception as e:
self.log.error(f"Failed to add attachments: {e}")
return tracks
return tracks
def split_profiles(self, profiles: List[str]) -> List[List[str]]:
"""
Split profiles based on codec level ranges and also DV/HDR groups for HYBRID mode.
"""
# -----------------------------
# Patterns for profile splitting
# -----------------------------
if self.vcodec == Video.Codec.AVC:
level_patterns = ["l30", "l31", "l40"]
else:
level_patterns = ["L30", "L31", "L40", "L41", "L50", "L51"]
# -----------------------------
# HYBRID MODE — Add DV/HDR splits
# -----------------------------
dv_patterns = ["DV", "dv"]
hdr10_patterns = ["HDR10", "hdr10"]
hdr10p_patterns = ["HDR10P", "hdr10p"]
result: List[List[str]] = []
used = set()
# -----------------------------
# Group DV profiles first
# -----------------------------
if self.range[0] == Video.Range.HYBRID:
dv_group = [p for p in profiles if any(tag in p for tag in dv_patterns)]
if dv_group:
result.append(dv_group)
used.update(dv_group)
# Group HDR10 profiles
hdr10_group = [p for p in profiles if any(tag in p for tag in hdr10_patterns)]
if hdr10_group:
result.append(hdr10_group)
used.update(hdr10_group)
# Group HDR10+ profiles
hdr10p_group = [p for p in profiles if any(tag in p for tag in hdr10p_patterns)]
if hdr10p_group:
result.append(hdr10p_group)
used.update(hdr10p_group)
# -----------------------------
# Normal HEVC/H264 Level Splitting
# -----------------------------
for pattern in level_patterns:
group = [p for p in profiles if (pattern in p and p not in used)]
if group:
result.append(group)
used.update(group)
# -----------------------------
# Any remaining profiles
# -----------------------------
leftover = [p for p in profiles if p not in used]
if leftover:
result.append(leftover)
return result
def get_chapters(self, title: Title_T) -> Chapters:
chapters: Chapters = Chapters()
# self.log.info(f"Title data: {title.data}")
credits = title.data["skipMarkers"]["credit"]
if credits["start"] > 0 and credits["end"] > 0:
chapters.add(Chapter(
timestamp=credits["start"], # Milliseconds
name="Intro"
))
chapters.add(
Chapter(
timestamp=credits["end"], # Milliseconds
name="Part 01"
)
)
chapters.add(Chapter(
timestamp=float(title.data["creditsOffset"]), # this is seconds, needed to assign to float
name="Outro"
))
return chapters
def get_widevine_license(self, *, challenge: bytes, title: Movie | Episode | Song, track: AnyTrack) -> bytes | str | None:
if not self.msl:
self.log.error(f"MSL Client is not intialized!")
sys.exit(1)
application_data = {
"version": 2,
"url": track.data["license_url"],
"id": int(time.time() * 10000),
"esn": self.esn.data,
"languages": ["en-US"],
# "uiVersion": "shakti-v9dddfde5",
"clientVersion": "6.0026.291.011",
"params": [{
"sessionId": base64.b64encode(get_random_bytes(16)).decode("utf-8"),
"clientTime": int(time.time()),
"challengeBase64": base64.b64encode(challenge).decode("utf-8"),
"xid": str(int((int(time.time()) + 0.1612) * 1000)),
}],
"echo": "sessionId"
}
header, payload_data = self.msl.send_message(
endpoint=self.config["endpoints"]["license"],
params={
"reqAttempt": 1,
"reqName": "license",
},
application_data=application_data,
userauthdata=self.userauthdata
)
if not payload_data:
self.log.error(f" - Failed to get license: {header['message']} [{header['code']}]")
sys.exit(1)
if "error" in payload_data[0]:
error = payload_data[0]["error"]
error_display = error.get("display")
error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", ""))
if error_display:
self.log.critical(f" - {error_display}")
if error_detail:
self.log.critical(f" - {error_detail}")
if not (error_display or error_detail):
self.log.critical(f" - {error}")
sys.exit(1)
return payload_data[0]["licenseResponseBase64"]
def get_playready_license(self, *, challenge: bytes, title: Movie | Episode | Song, track: AnyTrack) -> bytes | str | None:
return None
# return super().get_widevine_license(challenge=challenge, title=title, track=track)
def configure(self):
# -----------------------------
# Profiles selection
# -----------------------------
if self.profile is None:
self.profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()]
if self.profile is not None:
self.requested_profiles = self.profile.split('+')
self.log.info(f"Requested profile: {self.requested_profiles}")
else:
self.requested_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()]
# -----------------------------
# Validate codec support
# -----------------------------
if self.vcodec.extension.upper() not in self.config["profiles"]["video"]:
raise ValueError(f"Video Codec {self.vcodec} is not supported by Netflix")
# -----------------------------
# HYBRID MODE FIX
# -----------------------------
if self.range[0] == Video.Range.HYBRID:
# Only allowed for HEVC
if self.vcodec != Video.Codec.HEVC:
self.log.error("HYBRID mode is only supported for HEVC codec.")
sys.exit(1)
self.log.info("HYBRID mode detected → Skipping standard range validation")
# Skip all range validation completely
else:
# Normal validation path (non-HYBRID)
if self.range[0].name not in list(self.config["profiles"]["video"][self.vcodec.extension.upper()].keys()) \
and self.vcodec not in (Video.Codec.AVC, Video.Codec.VP9):
self.log.error(f"Video range {self.range[0].name} is not supported by Video Codec: {self.vcodec}")
sys.exit(1)
if len(self.range) > 1:
self.log.error("Multiple video range is not supported right now.")
sys.exit(1)
if self.vcodec == Video.Codec.AVC and self.range[0] != Video.Range.SDR:
self.log.error("H.264 Video Codec only supports SDR")
sys.exit(1)
# -----------------------------
# Final profile resolution
# -----------------------------
self.profiles = self.get_profiles()
self.log.info("Initializing a MSL client")
self.get_esn()
scheme = KeyExchangeSchemes.AsymmetricWrapped
self.log.info(f"Scheme: {scheme}")
self.msl = MSL.handshake(
scheme=scheme,
session=self.session,
endpoint=self.config["endpoints"]["manifest"],
sender=self.esn.data,
cache=self.cache.get("MSL")
)
cookie = self.session.cookies.get_dict()
self.userauthdata = UserAuthentication.NetflixIDCookies(
netflixid=cookie["NetflixId"],
securenetflixid=cookie["SecureNetflixId"]
)
def get_profiles(self):
result_profiles = []
# -------------------------------
# AVC logic (unchanged)
# -------------------------------
if self.vcodec == Video.Codec.AVC:
if self.requested_profiles is not None:
for req in self.requested_profiles:
result_profiles.extend(
flatten(list(self.config["profiles"]["video"][self.vcodec.extension.upper()][req]))
)
return result_profiles
result_profiles.extend(
flatten(list(self.config["profiles"]["video"][self.vcodec.extension.upper()].values()))
)
return result_profiles
# -------------------------------
# VP9 logic (unchanged)
# -------------------------------
if self.vcodec == Video.Codec.VP9 and self.range[0] != Video.Range.HDR10:
result_profiles.extend(
self.config["profiles"]["video"][self.vcodec.extension.upper()].values()
)
return result_profiles
# -------------------------------
# HEVC Hybrid mode (FIXED)
# -------------------------------
if self.vcodec == Video.Codec.HEVC and self.range[0] == Video.Range.HYBRID:
self.log.info("HYBRID mode detected → Using HDR10 + DV profiles")
hevc_profiles = self.config["profiles"]["video"][self.vcodec.extension.upper()]
result_profiles = []
# 1. HDR10 FIRST
if "HDR10" in hevc_profiles:
result_profiles += hevc_profiles["HDR10"]
# 2. HDR10P (some titles use this instead of HDR10)
if "HDR10P" in hevc_profiles:
result_profiles += hevc_profiles["HDR10P"]
# 3. DV LAST (IMPORTANT!)
if "DV" in hevc_profiles:
result_profiles += hevc_profiles["DV"]
return result_profiles
# -------------------------------
# Normal HEVC (non HYBRID)
# -------------------------------
for profiles in self.config["profiles"]["video"][self.vcodec.extension.upper()]:
for r in self.range:
if r in profiles:
result_profiles.extend(
self.config["profiles"]["video"][self.vcodec.extension.upper()][r.name]
)
self.log.debug(f"Result_profiles: {result_profiles}")
return result_profiles
def get_esn(self):
ESN_GEN = "".join(random.choice("0123456789ABCDEF") for _ in range(30))
esn_value = f"NFCDIE-03-{ESN_GEN}"
# Check if ESN is expired or doesn't exist
if self.esn.data is None or self.esn.data == {} or (hasattr(self.esn, 'expired') and self.esn.expired):
# Set new ESN with 6-hour expiration
self.esn.set(esn_value, 1 * 60 * 60) # 6 hours in seconds
self.log.info(f"Generated new ESN with 1-hour expiration")
else:
self.log.info(f"Using cached ESN.")
self.log.info(f"ESN: {self.esn.data}")
def get_metadata(self, title_id: str):
"""
Obtain Metadata information about a title by it's ID.
:param title_id: Title's ID.
:returns: Title Metadata.
"""
try:
metadata = self.session.get(
self.config["endpoints"]["metadata"].format(build_id="release"),
params={
"movieid": title_id,
"drmSystem": self.config["configuration"]["drm_system"],
"isWatchlistEnabled": False,
"isShortformEnabled": False,
"languages": self.meta_lang
}
).json()
except requests.HTTPError as e:
if e.response.status_code == 500:
self.log.warning(
" - Recieved a HTTP 500 error while getting metadata, deleting cached reactContext data"
)
# self.cache.
# os.unlink(self.get_cache("web_data.json"))
# return self.get_metadata(self, title_id)
raise Exception(f"Error getting metadata: {e}")
except json.JSONDecodeError:
self.log.error(" - Failed to get metadata, title might not be available in your region.")
sys.exit(1)
else:
if "status" in metadata and metadata["status"] == "error":
self.log.error(
f" - Failed to get metadata, cookies might be expired. ({metadata['message']})"
)
sys.exit(1)
return metadata
def get_manifest(self, title: Title_T, video_profiles: List[str], required_text_track_id: Optional[str] = None, required_audio_track_id: Optional[str] = None):
audio_profiles = self.config["profiles"]["audio"].values()
video_profiles = sorted(set(flatten(as_list(
video_profiles,
audio_profiles,
self.config["profiles"]["video"]["H264"]["BPL"] if self.vcodec == Video.Codec.AVC else [],
self.config["profiles"]["subtitles"],
))))
self.log.debug("Profiles:\n\t" + "\n\t".join(video_profiles))
if not self.msl:
raise Exception("MSL Client is not intialized.")
params = {
"reqAttempt": 1,
"reqPriority": 10,
"reqName": "manifest",
}
_, payload_chunks = self.msl.send_message(
endpoint=self.config["endpoints"]["manifest"],
params=params,
application_data={
"version": 2,
"url": "manifest",
"id": int(time.time()),
"esn": self.esn.data,
"languages": ["en-US"],
"clientVersion": "6.0026.291.011",
"params": {
"clientVersion": "6.0051.090.911",
"challenge": self.config["payload_challenge_pr"] if self.drm_system == 'playready' else self.config["payload_challenge"],
"challanges": {
"default": self.config["payload_challenge_pr"] if self.drm_system == 'playready' else self.config["payload_challenge"]
},
"contentPlaygraph": ["v2"],
"deviceSecurityLevel": "3000",
"drmVersion": 25,
"desiredVmaf": "plus_lts",
"desiredSegmentVmaf": "plus_lts",
"flavor": "STANDARD", # ? PRE_FETCH, SUPPLEMENTAL
"drmType": self.drm_system,
"imageSubtitleHeight": 1080,
"isBranching": False,
"isNonMember": False,
"isUIAutoPlay": False,
"licenseType": "standard",
"liveAdsCapability": "replace",
"liveMetadataFormat": "INDEXED_SEGMENT_TEMPLATE",
"manifestVersion": "v2",
"osName": "windows",
"osVersion": "10.0",
"platform": "138.0.0.0",
"profilesGroups": [{
"name": "default",
"profiles": video_profiles
}],
"profiles": video_profiles,
"preferAssistiveAudio": False,
"requestSegmentVmaf": False,
"requiredAudioTrackId": required_audio_track_id, # This is for getting missing audio tracks (value get from `new_track_id``)
"requiredTextTrackId": required_text_track_id, # This is for getting missing subtitle. (value get from `new_track_id``)
"supportsAdBreakHydration": False,
"supportsNetflixMediaEvents": True,
"supportsPartialHydration": True, # This is important if you want get available all tracks. but you must fetch each missing url tracks with "requiredAudioTracksId" or "requiredTextTrackId"
"supportsPreReleasePin": True,
"supportsUnequalizedDownloadables": True,
"supportsWatermark": True,
"titleSpecificData": {
title.data.get("episodeId", title.data["id"]): {"unletterboxed": False}
},
"type": "standard", # ? PREPARE
"uiPlatform": "SHAKTI",
"uiVersion": "shakti-v49577320",
"useBetterTextUrls": True,
"useHttpsStreams": True,
"usePsshBox": True,
"videoOutputInfo": [{
# todo ; make this return valid, but "secure" values, maybe it helps
"type": "DigitalVideoOutputDescriptor",
"outputType": "unknown",
"supportedHdcpVersions": self.config["configuration"]["supported_hdcp_versions"],
"isHdcpEngaged": self.config["configuration"]["is_hdcp_engaged"]
}],
"viewableId": title.data.get("episodeId", title.data["id"]),
"xid": str(int((int(time.time()) + 0.1612) * 1000)),
"showAllSubDubTracks": True,
}
},
userauthdata=self.userauthdata
)
if "errorDetails" in payload_chunks:
raise Exception(f"Manifest call failed: {payload_chunks['errorDetails']}")
# with open(f"./manifest_{"+".join(video_profiles)}.json", mode='w') as r:
# r.write(jsonpickle.encode(payload_chunks, indent=4))
return payload_chunks
@staticmethod
def get_original_language(manifest) -> Language:
for language in manifest["audio_tracks"]:
if language["languageDescription"].endswith(" [Original]"):
return Language.get(language["language"])
# e.g. get `en` from "A:1:1;2;en;0;|V:2:1;[...]"
return Language.get(manifest["defaultTrackOrderList"][0]["mediaId"].split(";")[2])
def get_widevine_service_certificate(self, *, challenge: bytes, title: Movie | Episode | Song, track: AnyTrack) -> bytes | str:
return self.config["certificate"]
def manifest_as_tracks(self, manifest, title: Title_T, hydrate_tracks = False) -> Tracks:
tracks = Tracks()
original_language = self.get_original_language(manifest)
self.log.debug(f"Original language: {original_language}")
license_url = manifest["links"]["license"]["href"]
# self.log.info(f"Video: {jsonpickle.encode(manifest["video_tracks"], indent=2)}")
# self.log.info()
for video in reversed(manifest["video_tracks"][0]["streams"]):
# self.log.info(video)
id = video["downloadable_id"]
# self.log.info(f"Adding video {video["res_w"]}x{video["res_h"]}, bitrate: {(float(video["framerate_value"]) / video["framerate_scale"]) if "framerate_value" in video else None} with profile {video["content_profile"]}. kid: {video["drmHeaderId"]}")
tracks.add(
Video(
id_=video["downloadable_id"],
url=video["urls"][0]["url"],
codec=Video.Codec.from_netflix_profile(video["content_profile"]),
bitrate=video["bitrate"] * 1000,
width=video["res_w"],
height=video["res_h"],
fps=(float(video["framerate_value"]) / video["framerate_scale"]) if "framerate_value" in video else None,
language=Language.get(original_language),
edition=video["content_profile"],
range_=self.parse_video_range_from_profile(video["content_profile"]),
drm=[Widevine(
pssh=PSSH(
# Box.parse(
# Box.build(
# dict(
# type=b"pssh",
# version=0,
# flags=0,
# system_ID=Cdm.uuid,
# init_data=b"\x12\x10" + UUID(hex=video["drmHeaderId"]).bytes
# )
# )
# )
manifest["video_tracks"][0]["drmHeader"]["bytes"]
),
kid=video["drmHeaderId"]
)],
data={
'license_url': license_url
}
)
)
# Audio
# store unavailable tracks for hydrating later
unavailable_audio_tracks: List[Tuple[str, str]] = []
for index, audio in enumerate(manifest["audio_tracks"]):
if len(audio["streams"]) < 1:
# This
# self.log.debug(f"Audio lang {audio["languageDescription"]} is available but no stream available.")
unavailable_audio_tracks.append((audio["new_track_id"], audio["id"])) # Assign to `unavailable_subtitle` for request missing audio tracks later
continue
# self.log.debug(f"Adding audio lang: {audio["language"]} with profile: {audio["content_profile"]}")
is_original_lang = audio["language"] == original_language.language
# self.log.info(f"is audio {audio["languageDescription"]} original language: {is_original_lang}")
for stream in audio["streams"]:
tracks.add(
Audio(
id_=stream["downloadable_id"],
url=stream["urls"][0]["url"],
codec=Audio.Codec.from_netflix_profile(stream["content_profile"]),
language=Language.get(self.NF_LANG_MAP.get(audio["language"]) or audio["language"]),
is_original_lang=is_original_lang,
bitrate=stream["bitrate"] * 1000,
channels=stream["channels"],
descriptive=audio.get("rawTrackType", "").lower() == "assistive",
name="[Original]" if Language.get(audio["language"]).language == original_language.language else None,
joc=6 if "atmos" in stream["content_profile"] else None
)
)
# Subtitle
unavailable_subtitle: List[Tuple[str, str]] = []
for index, subtitle in enumerate(manifest["timedtexttracks"]):
if "isNoneTrack" in subtitle and subtitle["isNoneTrack"] == True:
continue
if subtitle["hydrated"] == False:
# This subtitles is there but has to request stream first
unavailable_subtitle.append((subtitle["new_track_id"], subtitle["id"])) # Assign to `unavailable_subtitle` for request missing subtitles later
# self.log.debug(f"Audio language: {subtitle["languageDescription"]} id: {subtitle["new_track_id"]} is not hydrated.")
continue
if subtitle["languageDescription"] == 'Off':
# I don't why this subtitles is requested, i consider for skip these subtitles for now
continue
# pass
id = list(subtitle["downloadableIds"].values())
language = Language.get(subtitle["language"])
profile = next(iter(subtitle["ttDownloadables"].keys()))
tt_downloadables = next(iter(subtitle["ttDownloadables"].values()))
is_original_lang = subtitle["language"] == original_language.language
# self.log.info(f"is subtitle {subtitle["languageDescription"]} original language {is_original_lang}")
# self.log.info(f"ddd")
tracks.add(
Subtitle(
id_=id[0],
url=tt_downloadables["urls"][0]["url"],
codec=Subtitle.Codec.from_netflix_profile(profile),
language=language,
forced=subtitle["isForcedNarrative"],
cc=subtitle["rawTrackType"] == "closedcaptions",
sdh=subtitle["trackVariant"] == 'STRIPPED_SDH' if "trackVariant" in subtitle else False,
is_original_lang=is_original_lang,
name=("[Original]" if language.language == original_language.language else None or "[Dubbing]" if "trackVariant" in subtitle and subtitle["trackVariant"] == "DUBTITLE" else None),
)
)
if hydrate_tracks == False:
return tracks
# Hydrate missing tracks
self.log.info(f"Getting all missing audio and subtitle tracks")
for audio_hydration, subtitle_hydration in zip_longest(unavailable_audio_tracks, unavailable_subtitle, fillvalue=("N/A", "N/A")):
# self.log.info(f"Audio hydration: {audio_hydration}")
manifest = self.get_manifest(title, self.profiles, subtitle_hydration[0], audio_hydration[0])
audios = next(item for item in manifest["audio_tracks"] if 'id' in item and item["id"] == audio_hydration[1])
subtitles = next(item for item in manifest["timedtexttracks"] if 'id' in item and item["id"] == subtitle_hydration[1])
for stream in audios["streams"]:
if audio_hydration[0] == 'N/A' and audio_hydration[1] == 'N/A':
# self.log.info(f"Skipping not available hydrated audio tracks")
continue
tracks.add(
Audio(
id_=stream["downloadable_id"],
url=stream["urls"][0]["url"],
codec=Audio.Codec.from_netflix_profile(stream["content_profile"]),
language=Language.get(self.NF_LANG_MAP.get(audios["language"]) or audios["language"]),
is_original_lang=stream["language"] == original_language.language,
bitrate=stream["bitrate"] * 1000,
channels=stream["channels"],
descriptive=audios.get("rawTrackType", "").lower() == "assistive",
name="[Original]" if Language.get(audios["language"]).language == original_language.language else None,
joc=6 if "atmos" in stream["content_profile"] else None
)
)
# self.log.info(jsonpickle.encode(subtitles, indent=2))
# sel
if subtitle_hydration[0] == 'N/A':
# self.log.info(f"Skipping not available hydrated subtitle tracks")
continue
id = list(subtitles["downloadableIds"].values())
language = Language.get(subtitles["language"])
profile = next(iter(subtitles["ttDownloadables"].keys()))
tt_downloadables = next(iter(subtitles["ttDownloadables"].values()))
tracks.add(
Subtitle(
id_=id[0],
url=tt_downloadables["urls"][0]["url"],
codec=Subtitle.Codec.from_netflix_profile(profile),
language=language,
forced=subtitles["isForcedNarrative"],
cc=subtitles["rawTrackType"] == "closedcaptions",
sdh=subtitles["trackVariant"] == 'STRIPPED_SDH' if "trackVariant" in subtitles else False,
is_original_lang=subtitles["language"] == original_language.language,
name=("[Original]" if language.language == original_language.language else None or "[Dubbing]" if "trackVariant" in subtitle and subtitle["trackVariant"] == "DUBTITLE" else None),
)
)
return tracks
def parse_video_range_from_profile(self, profile: str) -> Video.Range:
"""
Parse the video range from a Netflix profile string.
Args:
profile (str): The Netflix profile string (e.g., "hevc-main10-L30-dash-cenc")
Returns:
Video.Range: The corresponding Video.Range enum value
Examples:
>>> parse_video_range_from_profile("hevc-main10-L30-dash-cenc")
<Video.Range.SDR: 'SDR'>
>>> parse_video_range_from_profile("hevc-dv5-main10-L30-dash-cenc")
<Video.Range.DV: 'DV'>
"""
# Get video profiles from config
video_profiles = self.config.get("profiles", {}).get("video", {})
# Search through all codecs and ranges to find the profile
for codec, ranges in video_profiles.items():
# if codec == 'H264':
# return Video.Range.SDR # for H264 video always return SDR
for range_name, profiles in ranges.items():
# self.log.info(f"Checking range {range_name}")
if profile in profiles:
# Return the corresponding Video.Range enum value
try:
# self.log.info(f"Found {range_name}")
return Video.Range(range_name)
except ValueError:
# If range_name is not a valid Video.Range, return SDR as default
self.log.debug(f"Video range is not valid {range_name}")
return Video.Range.SDR
# If profile not found, return SDR as default
return Video.Range.SDR
File diff suppressed because one or more lines are too long
@@ -1,254 +0,0 @@
import json
import re
from http.cookiejar import CookieJar
from typing import Optional
from langcodes import Language
import click
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapter, Tracks, Subtitle
class NPO(Service):
"""
Service code for NPO Start (npo.nl)
Version: 1.0.0
Authorization: optional cookies (free/paid content supported)
Security: FHD @ L3 (Widevine)
Supports:
• Series ↦ https://npo.nl/start/serie/{slug}
• Movies ↦ https://npo.nl/start/video/{slug}
Only supports widevine at the moment
Note: Movie that is inside in a series (e.g.
https://npo.nl/start/serie/zappbios/.../zappbios-captain-nova/afspelen)
can be downloaded as movies by converting the URL to:
https://npo.nl/start/video/zappbios-captain-nova
"""
TITLE_RE = (
r"^(?:https?://(?:www\.)?npo\.nl/start/)?"
r"(?:(?P<type>video|serie)/(?P<slug>[^/]+)"
r"(?:/afleveringen)?"
r"(?:/seizoen-(?P<season>[^/]+)/(?P<episode>[^/]+)/afspelen)?)?$"
)
GEOFENCE = ("NL",)
NO_SUBTITLES = False
@staticmethod
@click.command(name="NPO", short_help="https://npo.nl")
@click.argument("title", type=str)
@click.pass_context
def cli(ctx, **kwargs):
return NPO(ctx, **kwargs)
def __init__(self, ctx, title: str):
super().__init__(ctx)
m = re.match(self.TITLE_RE, title)
if not m:
raise ValueError(
f"Unsupported NPO URL: {title}\n"
"Use /video/slug for movies or /serie/slug for series."
)
self.slug = m.group("slug")
self.kind = m.group("type") or "video"
self.season_slug = m.group("season")
self.episode_slug = m.group("episode")
if self.config is None:
raise EnvironmentError("Missing service config.")
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not cookies:
self.log.info("No cookies, proceeding anonymously.")
return
token = next((c.value for c in cookies if c.name == "__Secure-next-auth.session-token"), None)
if not token:
self.log.info("No session token, proceeding unauthenticated.")
return
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) Firefox/143.0",
"Origin": "https://npo.nl",
"Referer": "https://npo.nl/",
})
r = self.session.get("https://npo.nl/start/api/domain/user-profiles", cookies=cookies)
if r.ok and isinstance(r.json(), list) and r.json():
self.log.info(f"NPO login OK, profiles: {[p['name'] for p in r.json()]}")
else:
self.log.warning("NPO auth check failed.")
def _get_build_id(self, slug: str) -> str:
"""Fetch buildId from the actual video/series page."""
url = f"https://npo.nl/start/{'video' if self.kind == 'video' else 'serie'}/{slug}"
r = self.session.get(url)
r.raise_for_status()
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">({.*?})</script>', r.text, re.DOTALL)
if not match:
raise RuntimeError("Failed to extract __NEXT_DATA__")
data = json.loads(match.group(1))
return data["buildId"]
def get_titles(self) -> Titles_T:
build_id = self._get_build_id(self.slug)
if self.kind == "serie":
url = self.config["endpoints"]["metadata_series"].format(build_id=build_id, slug=self.slug)
else:
url = self.config["endpoints"]["metadata"].format(build_id=build_id, slug=self.slug)
resp = self.session.get(url)
resp.raise_for_status()
queries = resp.json()["pageProps"]["dehydratedState"]["queries"]
def get_data(fragment: str):
return next((q["state"]["data"] for q in queries if fragment in str(q.get("queryKey", ""))), None)
if self.kind == "serie":
series_data = get_data("series:detail-")
if not series_data:
raise ValueError("Series metadata not found")
episodes = []
seasons = get_data("series:seasons-") or []
for season in seasons:
eps = get_data(f"programs:season-{season['guid']}") or []
for e in eps:
episodes.append(
Episode(
id_=e["guid"],
service=self.__class__,
title=series_data["title"],
season=int(season["seasonKey"]),
number=int(e["programKey"]),
name=e["title"],
description=(e.get("synopsis", {}) or {}).get("long", ""),
language=Language.get("nl"),
data=e,
)
)
return Series(episodes)
# Movie
item = get_data("program:detail-") or queries[0]["state"]["data"]
synopsis = item.get("synopsis", {})
desc = synopsis.get("long") or synopsis.get("short", "") if isinstance(synopsis, dict) else str(synopsis)
year = (int(item["firstBroadcastDate"]) // 31536000 + 1970) if item.get("firstBroadcastDate") else None
return Movies([
Movie(
id_=item["guid"],
service=self.__class__,
name=item["title"],
description=desc,
year=year,
language=Language.get("nl"),
data=item,
)
])
def get_tracks(self, title: Title_T) -> Tracks:
product_id = title.data.get("productId")
if not product_id:
raise ValueError("no productId detected.")
# Get JWT
token_url = self.config["endpoints"]["player_token"].format(product_id=product_id)
r_tok = self.session.get(token_url, headers={"Referer": f"https://npo.nl/start/video/{self.slug}"})
r_tok.raise_for_status()
jwt = r_tok.json()["jwt"]
# Request stream
r_stream = self.session.post(
self.config["endpoints"]["streams"],
json={
"profileName": "dash",
"drmType": "widevine",
"referrerUrl": f"https://npo.nl/start/video/{self.slug}",
"ster": {"identifier": "npo-app-desktop", "deviceType": 4, "player": "web"},
},
headers={
"Authorization": jwt,
"Content-Type": "application/json",
"Origin": "https://npo.nl",
"Referer": f"https://npo.nl/start/video/{self.slug}",
},
)
r_stream.raise_for_status()
data = r_stream.json()
if "error" in data:
raise PermissionError(f"Stream error: {data['error']}")
stream = data["stream"]
manifest_url = stream.get("streamURL") or stream.get("url")
if not manifest_url:
raise ValueError("No stream URL in response")
is_unencrypted = "unencrypted" in manifest_url.lower() or not any(k in stream for k in ["drmToken", "token"])
# Parse DASH
tracks = DASH.from_url(manifest_url, session=self.session).to_tracks(language=title.language)
# Subtitles
subtitles = []
for sub in data.get("assets", {}).get("subtitles", []):
lang = sub.get("iso", "und")
subtitles.append(
Subtitle(
id_=sub.get("name", lang),
url=sub["location"].strip(),
language=Language.get(lang),
is_original_lang=lang == "nl",
codec=Subtitle.Codec.WebVTT,
name=sub.get("name", "Unknown"),
forced=False,
sdh=False,
)
)
tracks.subtitles = subtitles
# DRM
if is_unencrypted:
for tr in tracks.videos + tracks.audio:
if hasattr(tr, "drm") and tr.drm:
tr.drm.clear()
else:
self.drm_token = stream.get("drmToken") or stream.get("token") or stream.get("drm_token")
if not self.drm_token:
raise ValueError(f"No DRM token found. Available keys: {list(stream.keys())}")
for tr in tracks.videos + tracks.audio:
if getattr(tr, "drm", None):
tr.drm.license = lambda challenge, **kw: self.get_widevine_license(
challenge=challenge, title=title, track=tr
)
return tracks
def get_chapters(self, title: Title_T) -> list[Chapter]:
return []
def get_widevine_license(self, challenge: bytes, title: Title_T, track: AnyTrack) -> bytes:
if not self.drm_token:
raise ValueError("DRM token not set login or paid content may be required.")
r = self.session.post(
self.config["endpoints"]["widevine_license"],
params={"custom_data": self.drm_token},
data=challenge,
)
r.raise_for_status()
return r.content
@@ -1,8 +0,0 @@
endpoints:
metadata: "https://npo.nl/start/_next/data/{build_id}/video/{slug}.json"
metadata_series: "https://npo.nl/start/_next/data/{build_id}/serie/{slug}.json"
metadata_episode: "https://npo.nl/start/_next/data/{build_id}/serie/{series_slug}/seizoen-{season_slug}/{episode_slug}.json"
streams: "https://prod.npoplayer.nl/stream-link"
player_token: "https://npo.nl/start/api/domain/player-token?productId={product_id}"
widevine_license: "https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication"
homepage: "https://npo.nl/start"
@@ -1,142 +0,0 @@
from __future__ import annotations
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
from functools import partial
from pathlib import Path
import json
import re
import click
import isodate
from click import Context
from envied.core.credential import Credential
from envied.core.service import Service
from envied.core.titles import Movie, Movies, Episode, Series
from envied.core.tracks import Track, Chapter, Tracks, Video, Audio, Subtitle
from envied.core.manifests.hls import HLS
from envied.core.manifests.dash import DASH
from rich.console import Console
class NRK(Service):
"""
Service code for NRK TV (https://tv.nrk.no)
\b
Version: 1.0.0
Author: lambda
Authorization: None
Robustness:
Unencrypted: 1080p, DD5.1
"""
GEOFENCE = ("no",)
TITLE_RE = r"^https://tv.nrk.no/serie/fengselseksperimentet/sesong/1/episode/(?P<content_id>.+)$"
@staticmethod
@click.command(name="NRK", short_help="https://tv.nrk.no", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> NRK:
return NRK(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
pass
def get_titles(self) -> Union[Movies, Series]:
match = re.match(self.TITLE_RE, self.title)
if match:
content_id = match.group("content_id")
EPISODE = True
MOVIE = False
else:
content_id = self.title.split('/')[-1]
MOVIE = True
EPISODE = False
r = self.session.get(self.config["endpoints"]["content"].format(content_id=content_id))
item = r.json()
# development only
#console = Console()
#console.print_json(data=item)
if EPISODE:
episode, name = item["programInformation"]["titles"]["title"].split(". ", maxsplit=1)
return Series([Episode(
id_=content_id,
service=self.__class__,
language="nb",
year=item["moreInformation"]["productionYear"],
title=item["_links"]["seriesPage"]["title"],
name=name,
season=item["_links"]["season"]["name"],
number=episode,
)])
if MOVIE:
name = item["programInformation"]["titles"]["title"]
return Movies([Movie(
id_ = content_id,
service=self.__class__,
name = name,
year = item["moreInformation"]["productionYear"],
language="nb",
data = None,
description = None,)])
def get_tracks(self, title: Union[Episode, Movie]) -> Tracks:
r = self.session.get(self.config["endpoints"]["manifest"].format(content_id=title.id))
manifest = r.json()
tracks = Tracks()
for asset in manifest["playable"]["assets"]:
if asset["format"] == "HLS":
tracks += Tracks(HLS.from_url(asset["url"], session=self.session).to_tracks("nb"))
for sub in manifest["playable"]["subtitles"]:
tracks.add(Subtitle(
codec=Subtitle.Codec.WebVTT,
language=sub["language"],
url=sub["webVtt"],
sdh=sub["type"] == "ttv",
))
for track in tracks:
track.needs_proxy = True
# if isinstance(track, Audio) and track.channels == 6.0:
# track.channels = 5.1
return tracks
def get_chapters(self, title: Union[Episode, Movie]) -> list[Chapter]:
r = self.session.get(self.config["endpoints"]["metadata"].format(content_id=title.id))
sdi = r.json()["skipDialogInfo"]
chapters = []
if sdi["endIntroInSeconds"]:
if sdi["startIntroInSeconds"]:
chapters.append(Chapter(timestamp=0))
chapters |= [
Chapter(timestamp=sdi["startIntroInSeconds"], name="Intro"),
Chapter(timestamp=sdi["endIntroInSeconds"])
]
if sdi["startCreditsInSeconds"]:
if not chapters:
chapters.append(Chapter(timestamp=0))
credits = isodate.parse_duration(sdi["startCredits"])
chapters.append(Chapter(credits.total_seconds(), name="Credits"))
return chapters
@@ -1,9 +0,0 @@
headers:
Accept-Language: nb-NO,de;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
endpoints:
content: https://psapi.nrk.no/tv/catalog/programs/{content_id}?contentGroup=adults&ageRestriction=None
metadata: https://psapi.nrk.no/playback/metadata/program/{content_id}
# manifest: https://psapi.nrk.no/playback/manifest/program/{content_id}
manifest: https://psapi.nrk.no/playback/manifest/program/{content_id}?eea-portability=true&ageRestriction=None
@@ -1,472 +0,0 @@
import base64
import hashlib
import hmac
import json
import time
from datetime import datetime
from http.cookiejar import CookieJar
from typing import Optional, Union
import click
from langcodes import Language
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapters, Tracks, Video
class PCOK(Service):
"""
Service code for NBC's Peacock streaming service (https://peacocktv.com).
Version: 1.0.0
Authorization: Cookies
Security: UHD@-- FHD@SL|L3
Tips: - The library of contents can be viewed without logging in at https://www.peacocktv.com/stream/tv
See the footer for links to movies, news, etc. A US IP is required to view.
"""
ALIASES = ("PCOK", "peacock")
GEOFENCE = ("US",)
TITLE_RE = [
r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>movies/[a-z0-9/./-]+/[a-f0-9-]+)",
r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>tv/[a-z0-9/./-]+/[a-f0-9-]+)",
r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>tv/[a-z0-9-/.]+/\d+)",
r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>news/[a-z0-9/./-]+/[a-f0-9-]+)",
r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>news/[a-z0-9-/.]+/\d+)",
r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>-/[a-z0-9-/.]+/\d+)",
r"(?:https?://(?:www\.)?peacocktv\.com/stream-tv/)?(?P<id>[a-z0-9-/.]+)",
]
@staticmethod
@click.command(name="PCOK", short_help="https://peacocktv.com")
@click.argument("title", type=str)
@click.option("-m", "--movie", is_flag=True, default=False, help="Title is a movie.")
@click.pass_context
def cli(ctx, **kwargs):
return PCOK(ctx, **kwargs)
def __init__(self, ctx, title, movie):
super().__init__(ctx)
self.title = title
self.movie = movie
self.cdm = ctx.obj.cdm
range_param = ctx.parent.params.get("range_")
self.range = range_param[0].name if range_param else "SDR"
vcodec_param = ctx.parent.params.get("vcodec")
self.vcodec = vcodec_param if vcodec_param else "H264"
if self.config is None:
raise Exception("Config is missing!")
profile_name = ctx.parent.params.get("profile")
if profile_name is None:
profile_name = "default"
self.profile = profile_name
self.hmac_key = None
self.tokens = None
self.license_api = None
self.license_bt = None
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not cookies:
raise EnvironmentError("Service requires Cookies for Authentication.")
self.session.headers.update({"Origin": "https://www.peacocktv.com"})
self.log.info("Getting Peacock Client configuration")
if self.config["client"]["platform"] != "PC":
self.service_config = self.session.get(
url=self.config["endpoints"]["config"].format(
territory=self.config["client"]["territory"],
provider=self.config["client"]["provider"],
proposition=self.config["client"]["proposition"],
device=self.config["client"]["platform"],
version=self.config["client"]["config_version"],
)
).json()
self.hmac_key = bytes(self.config["security"]["signature_hmac_key_v4"], "utf-8")
self.log.info("Getting Authorization Tokens")
self.tokens = self.get_tokens()
self.log.info("Verifying Authorization Tokens")
if not self.verify_tokens():
raise EnvironmentError("Failed! Cookies might be outdated.")
def get_titles(self) -> Titles_T:
# Parse title from various URL formats
import re
title_id = self.title
for pattern in self.TITLE_RE:
match = re.search(pattern, self.title)
if match:
title_id = match.group("id")
break
# Handle stream-tv redirects
if "/" not in title_id:
r = self.session.get(self.config["endpoints"]["stream_tv"].format(title_id=title_id))
match = re.search(r"/watch/asset(/[^']+)", r.text)
if match:
title_id = match.group(1)
else:
raise ValueError("Title ID not found or invalid")
if not title_id.startswith("/"):
title_id = f"/{title_id}"
if title_id.startswith("/movies/"):
self.movie = True
res = self.session.get(
url=self.config["endpoints"]["node"],
params={
"slug": title_id,
"represent": "(items(items))"
},
headers={
"Accept": "*",
"Referer": f"https://www.peacocktv.com/watch/asset{title_id}",
"X-SkyOTT-Device": self.config["client"]["device"],
"X-SkyOTT-Platform": self.config["client"]["platform"],
"X-SkyOTT-Proposition": self.config["client"]["proposition"],
"X-SkyOTT-Provider": self.config["client"]["provider"],
"X-SkyOTT-Territory": self.config["client"]["territory"],
"X-SkyOTT-Language": "en"
}
).json()
if self.movie:
return Movies([
Movie(
id_=title_id,
service=self.__class__,
name=res["attributes"]["title"],
year=res["attributes"]["year"],
data=res,
)
])
else:
episodes = []
for season in res["relationships"]["items"]["data"]:
for episode in season["relationships"]["items"]["data"]:
episodes.append(episode)
episode_titles = []
for x in episodes:
episode_titles.append(
Episode(
id_=title_id,
service=self.__class__,
title=res["attributes"]["title"],
season=x["attributes"].get("seasonNumber"),
number=x["attributes"].get("episodeNumber"),
name=x["attributes"].get("title"),
year=x["attributes"].get("year"),
data=x
)
)
return Series(episode_titles)
def get_tracks(self, title: Title_T) -> Tracks:
supported_colour_spaces = ["SDR"]
if self.range == "HDR10":
self.log.info("Switched dynamic range to HDR10")
supported_colour_spaces = ["HDR10"]
elif self.range == "DV":
self.log.info("Switched dynamic range to DV")
supported_colour_spaces = ["DolbyVision"]
content_id = title.data["attributes"]["formats"]["HD"]["contentId"]
variant_id = title.data["attributes"]["providerVariantId"]
sky_headers = {
"X-SkyOTT-Agent": ".".join([
self.config["client"]["proposition"].lower(),
self.config["client"]["device"].lower(),
self.config["client"]["platform"].lower()
]),
"X-SkyOTT-PinOverride": "false",
"X-SkyOTT-Provider": self.config["client"]["provider"],
"X-SkyOTT-Territory": self.config["client"]["territory"],
"X-SkyOTT-UserToken": self.tokens["userToken"]
}
body = json.dumps({
"device": {
"capabilities": [
{
"protection": "PLAYREADY",
"container": "ISOBMFF",
"transport": "DASH",
"acodec": "AAC",
"vcodec": "H265" if self.vcodec == "H265" else "H264",
},
{
"protection": "WIDEVINE",
"container": "ISOBMFF",
"transport": "DASH",
"acodec": "AAC",
"vcodec": "H265" if self.vcodec == "H265" else "H264",
}
],
"maxVideoFormat": "UHD" if self.vcodec == "H265" else "HD",
"supportedColourSpaces": supported_colour_spaces,
"model": self.config["client"]["platform"],
"hdcpEnabled": "true"
},
"client": {
"thirdParties": ["FREEWHEEL", "YOSPACE"]
},
"contentId": content_id,
"providerVariantId": variant_id,
"parentalControlPin": "null"
}, separators=(",", ":"))
manifest = self.session.post(
url=self.config["endpoints"]["vod"],
data=body,
headers=dict(**sky_headers, **{
"Accept": "application/vnd.playvod.v1+json",
"Content-Type": "application/vnd.playvod.v1+json",
"X-Sky-Signature": self.create_signature_header(
method="POST",
path="/video/playouts/vod",
sky_headers=sky_headers,
body=body,
timestamp=int(time.time())
)
})
).json()
if "errorCode" in manifest:
raise ValueError(f"An error occurred: {manifest['description']} [{manifest['errorCode']}]")
self.license_api = manifest["protection"]["licenceAcquisitionUrl"]
self.license_bt = manifest["protection"]["licenceToken"]
tracks = DASH.from_url(
url=manifest["asset"]["endpoints"][0]["url"],
session=self.session
).to_tracks(language=Language.get("en"))
# Set HDR attributes
for video in tracks.videos:
if supported_colour_spaces == ["HDR10"]:
video.range = Video.Range.HDR10
elif supported_colour_spaces == ["DolbyVision"]:
video.range = Video.Range.DV
else:
video.range = Video.Range.SDR
# Fix audio description language
for track in tracks.audio:
if track.language.territory == "AD":
track.language.territory = None
return tracks
def get_chapters(self, title: Title_T) -> Chapters:
"""Get chapters for the title. Peacock doesn't typically provide chapter data."""
return Chapters([])
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]:
"""Retrieve a PlayReady license for a given track."""
if not self.license_api:
return None
response = self.session.post(
url=self.license_api,
headers={
"Accept": "*",
"X-Sky-Signature": self.create_signature_header(
method="POST",
path="/" + self.license_api.split("://", 2)[1].split("/", 1)[1],
sky_headers={},
body="",
timestamp=int(time.time())
)
},
data=challenge
)
response.raise_for_status()
return response.content
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
"""Retrieve a Widevine license for a given track."""
if not self.license_api:
return None
response = self.session.post(
url=self.license_api,
headers={
"Accept": "*",
"X-Sky-Signature": self.create_signature_header(
method="POST",
path="/" + self.license_api.split("://", 1)[1].split("/", 1)[1],
sky_headers={},
body="",
timestamp=int(time.time())
)
},
data=challenge
)
response.raise_for_status()
return response.content
@staticmethod
def calculate_sky_header_md5(headers):
if len(headers.items()) > 0:
headers_str = "\n".join(f"{x[0].lower()}: {x[1]}" for x in headers.items()) + "\n"
else:
headers_str = "{}"
return str(hashlib.md5(headers_str.encode()).hexdigest())
@staticmethod
def calculate_body_md5(body):
return str(hashlib.md5(body.encode()).hexdigest())
def calculate_signature(self, msg):
digest = hmac.new(self.hmac_key, bytes(msg, "utf-8"), hashlib.sha1).digest()
return str(base64.b64encode(digest), "utf-8")
def create_signature_header(self, method, path, sky_headers, body, timestamp):
data = "\n".join([
method.upper(),
path,
"",
self.config["client"]["client_sdk"],
"1.0",
self.calculate_sky_header_md5(sky_headers),
str(timestamp),
self.calculate_body_md5(body)
]) + "\n"
signature_hmac = self.calculate_signature(data)
return self.config["security"]["signature_format"].format(
client=self.config["client"]["client_sdk"],
signature=signature_hmac,
timestamp=timestamp
)
def get_tokens(self):
# Try to get cached tokens
cache = self.cache.get(f"tokens_{self.profile}_{self.config['client']['id']}")
if cache and cache.data.get("tokenExpiryTime"):
tokens_expiration = cache.data.get("tokenExpiryTime")
if datetime.strptime(tokens_expiration, "%Y-%m-%dT%H:%M:%S.%fZ") > datetime.now():
return cache.data
# Get all SkyOTT headers
sky_headers = {
"X-SkyOTT-Agent": ".".join([
self.config["client"]["proposition"],
self.config["client"]["device"],
self.config["client"]["platform"]
]).lower(),
"X-SkyOTT-Device": self.config["client"]["device"],
"X-SkyOTT-Platform": self.config["client"]["platform"],
"X-SkyOTT-Proposition": self.config["client"]["proposition"],
"X-SkyOTT-Provider": self.config["client"]["provider"],
"X-SkyOTT-Territory": self.config["client"]["territory"]
}
try:
# Call personas endpoint to get the accounts personaId
personas = self.session.get(
url=self.config["endpoints"]["personas"],
headers=dict(**sky_headers, **{
"Accept": "application/vnd.persona.v1+json",
"Content-Type": "application/vnd.persona.v1+json",
"X-SkyOTT-TokenType": self.config["client"]["auth_scheme"]
})
).json()
except Exception as e:
raise EnvironmentError(f"Unable to get persona ID: {e}")
persona = personas["personas"][0]["personaId"]
# Craft the body data
body = json.dumps({
"auth": {
"authScheme": self.config["client"]["auth_scheme"],
"authIssuer": self.config["client"]["auth_issuer"],
"provider": self.config["client"]["provider"],
"providerTerritory": self.config["client"]["territory"],
"proposition": self.config["client"]["proposition"],
"personaId": persona
},
"device": {
"type": self.config["client"]["device"],
"platform": self.config["client"]["platform"],
"id": self.config["client"]["id"],
"drmDeviceId": self.config["client"]["drm_device_id"]
}
}, separators=(",", ":"))
# Get the tokens
tokens = self.session.post(
url=self.config["endpoints"]["tokens"],
headers=dict(**sky_headers, **{
"Accept": "application/vnd.tokens.v1+json",
"Content-Type": "application/vnd.tokens.v1+json",
"X-Sky-Signature": self.create_signature_header(
method="POST",
path="/auth/tokens",
sky_headers=sky_headers,
body=body,
timestamp=int(time.time())
)
}),
data=body
).json()
# Cache the tokens
if not cache:
cache = self.cache.get(f"tokens_{self.profile}_{self.config['client']['id']}")
cache.set(data=tokens)
return tokens
def verify_tokens(self):
"""Verify the tokens by calling the /auth/users/me endpoint"""
sky_headers = {
"X-SkyOTT-Device": self.config["client"]["device"],
"X-SkyOTT-Platform": self.config["client"]["platform"],
"X-SkyOTT-Proposition": self.config["client"]["proposition"],
"X-SkyOTT-Provider": self.config["client"]["provider"],
"X-SkyOTT-Territory": self.config["client"]["territory"],
"X-SkyOTT-UserToken": self.tokens["userToken"]
}
try:
self.session.get(
url=self.config["endpoints"]["me"],
headers=dict(**sky_headers, **{
"Accept": "application/vnd.userinfo.v2+json",
"Content-Type": "application/vnd.userinfo.v2+json",
"X-Sky-Signature": self.create_signature_header(
method="GET",
path="/auth/users/me",
sky_headers=sky_headers,
body="",
timestamp=int(time.time())
)
})
)
return True
except Exception:
return False
@@ -1,27 +0,0 @@
endpoints:
stream_tv: 'https://www.peacocktv.com/stream-tv/{title_id}'
config: 'https://config.clients.peacocktv.com/{territory}/{provider}/{proposition}/{device}/PROD/{version}/config.json'
login: 'https://rango.id.peacocktv.com/signin/service/international'
personas: 'https://persona.id.peacocktv.com/persona-store/personas'
tokens: 'https://ovp.peacocktv.com/auth/tokens'
me: 'https://ovp.peacocktv.com/auth/users/me'
node: 'https://atom.peacocktv.com/adapter-calypso/v3/query/node'
vod: 'https://ovp.peacocktv.com/video/playouts/vod'
client:
config_version: '1.0.8'
territory: 'US'
provider: 'NBCU'
proposition: 'NBCUOTT'
platform: 'ANDROID' # PC, ANDROID
device: 'TABLET' # COMPUTER, TABLET
id: 'Jcvf1y0whKOI29vRXcJy'
drm_device_id: 'UNKNOWN'
client_sdk: 'NBCU-WEB-v4' # NBCU-ANDROID-v3 NBCU-ANDRTV-v4
auth_scheme: 'MESSO'
auth_issuer: 'NOWTV'
security:
signature_hmac_key_v4: 'FvT9VtwvhtSZvqnExMsvDDTEvBqR3HdsMcBFtWYV'
signature_hmac_key_v6: 'izU6EJqqu6DOhOWSk5X4p9dod3fNqH7vzKtYDK8d'
signature_format: 'SkyOTT client="{client}",signature="{signature}",timestamp="{timestamp}",version="1.0"'
@@ -1,323 +0,0 @@
from __future__ import annotations
import json
import re
import uuid
from collections.abc import Generator
from concurrent.futures import ThreadPoolExecutor
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional
from urllib.parse import quote, urljoin, urlparse
import click
from click import Context
from requests import Request
from envied.core.credential import Credential
from envied.core.manifests import DASH, HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks
class PLEX(Service):
"""
\b
Service code for Plex's free streaming service (https://watch.plex.tv/).
\b
Version: 1.0.4
Author: stabbedbybrick
Authorization: None
Geofence: API and downloads are locked into whatever region the user is in
Robustness:
L3: 720p, AAC2.0
\b
Tips:
- Input should be complete URL:
SHOW: https://watch.plex.tv/show/taboo-2017
EPISODE: https://watch.plex.tv/show/taboo-2017/season/1/episode/1
MOVIE: https://watch.plex.tv/movie/the-longest-yard
"""
ALIASES = ("plextv",)
@staticmethod
@click.command(name="PLEX", short_help="https://watch.plex.tv/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> PLEX:
return PLEX(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
self.session.headers.update(
{
"accept": "application/json",
"x-plex-client-identifier": str(uuid.uuid4()),
"x-plex-language": "en",
"x-plex-product": "Plex Mediaverse",
"x-plex-provider-version": "6.5.0",
}
)
user = self._request("POST", self.config["endpoints"]["user"])
if not (auth_token := user.get("authToken")):
raise ValueError(f"PLEX authentication failed: {user}")
self.auth_token = auth_token
self.session.headers.update({"x-plex-token": self.auth_token})
def search(self) -> Generator[SearchResult, None, None]:
results = self._request(
"GET", "https://discover.provider.plex.tv/library/search",
params={
"searchTypes": "movies,tv",
"searchProviders": "discover,plexAVOD,plexFAST",
"includeMetadata": 1,
"filterPeople": 1,
"limit": 10,
"query": quote(self.title),
},
)
for result in results["MediaContainer"]["SearchResults"]:
if "free on demand" not in result.get("title", "").lower():
continue
for result in result["SearchResult"]:
kind = result.get("Metadata", {}).get("type")
slug = result.get("Metadata", {}).get("slug")
yield SearchResult(
id_=f"https://watch.plex.tv/{kind}/{slug}",
title=result.get("Metadata", {}).get("title"),
description=result.get("Metadata", {}).get("description"),
label=kind,
url=f"https://watch.plex.tv/{kind}/{slug}",
)
def get_titles(self) -> Movies | Series:
url_pattern = re.compile(
r"^https://watch.plex.tv/"
r"(?:[a-z]{2}(?:-[A-Z]{2})?/)??"
r"(?P<type>movie|show)/"
r"(?P<id>[\w-]+)"
r"(?P<url_path>(/season/\d+/episode/\d+))?"
)
match = url_pattern.match(self.title)
if not match:
raise ValueError(f"Could not parse ID from title: {self.title}")
kind, guid, url_path = (match.group(i) for i in ("type", "id", "url_path"))
if kind == "show":
if url_path is not None:
path = urlparse(self.title).path
url = re.sub(r"/[a-z]{2}(?:-[A-Z]{2})?/", "/", path)
episode = self._episode(url)
return Series(episode)
episodes = self._series(guid)
return Series(episodes)
elif kind == "movie":
movie = self._movie(guid)
return Movies(movie)
else:
raise ValueError(f"Could not parse content type from title: {self.title}")
def get_tracks(self, title: Movie | Episode) -> Tracks:
dash_media = next((x for x in title.data.get("Media", []) if x.get("protocol", "").lower() == "dash"), None)
if not dash_media:
hls_media = next((x for x in title.data.get("Media", []) if x.get("protocol", "").lower() == "hls"), None)
media = dash_media or hls_media
if not media:
raise ValueError("Failed to find either DASH or HLS media")
manifest = DASH if dash_media else HLS
media_key = media.get("id")
has_drm = media.get("drm")
if has_drm:
manifest_url = (
self.config["endpoints"]["base_url"]
+ self.config["endpoints"]["manifest_drm"].format(media_key, self.auth_token)
)
title.data["license_url"] = (
self.config["endpoints"]["base_url"]
+ self.config["endpoints"]["license"].format(media_key, self.auth_token)
)
else:
manifest_url = (
self.config["endpoints"]["base_url"]
+ self.config["endpoints"]["manifest_clear"].format(media_key, self.auth_token)
)
title.data["license_url"] = None
tracks = manifest.from_url(manifest_url, self.session).to_tracks(language="en")
return tracks
def get_chapters(self, title: Movie | Episode) -> Chapters:
if not (markers := title.data.get("Marker")):
try:
metadata = self._request(
"POST", "/playQueues",
params={
"uri": self.config["endpoints"]["provider"] + title.data.get("key"),
"type": "video",
"continuous": "1",
},
)
markers = next((
x.get("Marker") for x in metadata.get("MediaContainer", {}).get("Metadata", [])
if x.get("key") == title.data.get("key")), [])
except Exception as e:
self.log.debug("Failed to fetch markers: %s", e)
return Chapters()
if not markers:
return Chapters()
chapters = []
for cue in markers:
if cue.get("startTimeOffset", 0) > 0:
chapters.append(Chapter(name=cue.get("type", "").title(), timestamp=cue.get("startTimeOffset")))
return Chapters(chapters)
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, *, challenge: bytes, title: Movie | Episode, track: Any) -> bytes | str | None:
if license_url := title.data.get("license_url"):
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
return None
# Service specific
def _fetch_season(self, url: str) -> list:
return self._request("GET", url).get("MediaContainer", {}).get("Metadata", [])
def _series(self, guid: str) -> list[Episode]:
data = self._request("GET", f"/library/metadata/show:{guid}")
meta_key = data.get("MediaContainer", {}).get("Metadata", [])[0].get("key")
if not meta_key:
raise ValueError("Failed to find metadata for title")
series = self._request("GET", f"{self.config['endpoints']['base_url']}/{meta_key}")
seasons = [
self.config["endpoints"]["base_url"] + item.get("key")
for item in series.get("MediaContainer", {}).get("Metadata", [])
if item.get("type") == "season"
]
if not seasons:
raise ValueError("Failed to find seasons for title")
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(self._fetch_season, seasons))
episodes = [
Episode(
id_=episode.get("ratingKey"),
service=self.__class__,
name=episode.get("title"),
season=int(episode.get("parentIndex", 0)),
number=int(episode.get("index", 0)),
title=re.sub(r"\s*\(\d{4}\)", "", episode.get("grandparentTitle", "")),
# year=episode.get("year"),
data=episode,
)
for season in results
for episode in season
if episode.get("type") == "episode"
]
return episodes
def _movie(self, guid: str) -> Movie:
data = self._request("GET", f"/library/metadata/movie:{guid}")
movie = data.get("MediaContainer", {}).get("Metadata", [])[0]
if not movie:
raise ValueError(f"Could not find any data for ID {guid}")
movies = [
Movie(
id_=movie.get("ratingKey"),
service=self.__class__,
name=movie.get("title"),
year=movie.get("year"),
data=movie,
)
]
return movies
def _episode(self, path: str) -> Episode:
data = self._request("GET", self.config["endpoints"]["screen"] + path)
meta_key = data.get("actions", [])[0].get("data", {}).get("key")
if not meta_key:
raise ValueError("Failed to find metadata for title")
metadata = self._request(
"POST", "/playQueues",
params={
"uri": self.config["endpoints"]["provider"] + meta_key,
"type": "video",
"continuous": "1",
},
)
episode = next((x for x in metadata.get("MediaContainer", {}).get("Metadata", []) if x.get("key") == meta_key), None)
if not episode:
raise ValueError("Failed to find metadata for title")
episodes = [
Episode(
id_=episode.get("ratingKey"),
service=self.__class__,
name=episode.get("title"),
season=int(episode.get("parentIndex", 0)),
number=int(episode.get("index", 0)),
title=re.sub(r"\s*\(\d{4}\)", "", episode.get("grandparentTitle", "")),
# year=episode.get("year"),
data=episode,
)
]
return episodes
def _request(self, method: str, endpoint: str, **kwargs: Any) -> Any[dict | str]:
url = urljoin(self.config["endpoints"]["base_url"], endpoint)
prep = self.session.prepare_request(Request(method, url, **kwargs))
response = self.session.send(prep)
if response.status_code not in (200, 201, 426):
raise ConnectionError(f"{response.text}")
try:
return json.loads(response.content)
except json.JSONDecodeError:
return response.text
@@ -1,12 +0,0 @@
headers:
User-Agent: Mozilla/5.0 (Linux; Android 11; Smart TV Build/AR2101; wv)
endpoints:
base_url: https://vod.provider.plex.tv
user: https://plex.tv/api/v2/users/anonymous
screen: https://luma.plex.tv/api/screen
provider: provider://tv.plex.provider.vod
manifest_clear: /library/parts/{}?includeAllStreams=1&X-Plex-Product=Plex+Mediaverse&X-Plex-Token={}
manifest_drm: /library/parts/{}?includeAllStreams=1&X-Plex-Product=Plex+Mediaverse&X-Plex-Token={}&X-Plex-DRM=widevine
license: /library/parts/{}/license?X-Plex-Token={}&X-Plex-DRM=widevine
@@ -1,294 +0,0 @@
from __future__ import annotations
import re
import uuid
from collections.abc import Generator
from http.cookiejar import CookieJar
from typing import Any, Optional
import click
from envied.core.credential import Credential
from envied.core.manifests import DASH, HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapters, Tracks
class PLUTO(Service):
"""
\b
Service code for Pluto TV on demand streaming service (https://pluto.tv/)
Credit to @wks_uwu for providing an alternative API, making the codebase much cleaner
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: None
Robustness:
Widevine:
L3: 1080p, AAC2.0
\b
Tips:
- Input can be complete title URL or just the path:
SERIES: /series/65ce4e5003fa740013793127/details
EPISODE: /series/65ce4e5003fa740013793127/season/1/episode/662c2af0a9f2d200131ba731
MOVIE: /movies/635c1e430888bc001ad01a9b/details
- Use --lang LANG_RANGE option to request non-English tracks
- Use --hls to request HLS instead of DASH:
devine dl pluto URL --hls
\b
Notes:
- Both DASH(widevine) and HLS(AES) are looked for in the API.
- DASH is prioritized over HLS since the latter doesn't have 1080p. If DASH has audio/subtitle issues,
you can try using HLS with the --hls flag.
- Pluto use transport streams for HLS, meaning the video and audio are a part of the same stream
As a result, only videos are listed as tracks. But the audio will be included as well.
- With the variations in manifests, and the inconsistency in the API, the language is set as "en" by default
for all tracks, no matter what region you're in.
You can manually set the language in the get_titles() function if you want to change it.
"""
ALIASES = ("plu", "plutotv")
TITLE_RE = (
r"^"
r"(?:https?://(?:www\.)?pluto\.tv(?:/[a-z]{2})?)?"
r"(?:/on-demand)?"
r"/(?P<type>movies|series)"
r"/(?P<id>[a-z0-9-]+)"
r"(?:(?:/season/(\d+)/episode/(?P<episode>[a-z0-9-]+)))?"
)
@staticmethod
@click.command(name="PLUTO", short_help="https://pluto.tv/", help=__doc__)
@click.option("--hls", is_flag=True, help="Request HLS instead of DASH")
@click.argument("title", type=str)
@click.pass_context
def cli(ctx, **kwargs):
return PLUTO(ctx, **kwargs)
def __init__(self, ctx, title, hls=False):
super().__init__(ctx)
self.title = title
self.force_hls = hls
def authenticate(
self,
cookies: Optional[CookieJar] = None,
credential: Optional[Credential] = None,
) -> None:
super().authenticate(cookies, credential)
self.session.params = {
"appName": "web",
"appVersion": "na",
"clientID": str(uuid.uuid1()),
"deviceDNT": 0,
"deviceId": "unknown",
"clientModelNumber": "na",
"serverSideAds": "false",
"deviceMake": "unknown",
"deviceModel": "web",
"deviceType": "web",
"deviceVersion": "unknown",
"sid": str(uuid.uuid1()),
"drmCapabilities": "widevine:L3",
}
info = self.session.get(self.config["endpoints"]["auth"]).json()
self.token = info["sessionToken"]
self.region = info["session"].get("activeRegion", "").lower()
def search(self) -> Generator[SearchResult, None, None]:
params = {
"q": self.title,
"limit": "100",
}
r = self.session.get(
self.config["endpoints"]["search"].format(query=self.title),
headers={"Authorization": f"Bearer {self.token}"},
params=params,
)
r.raise_for_status()
results = r.json()
for result in results["data"]:
if result.get("type") not in ["timeline", "channel"]:
content = result.get("id")
kind = result.get("type")
kind = "movies" if kind == "movie" else "series"
yield SearchResult(
id_=f"/{kind}/{content}/details",
title=result.get("name"),
description=result.get("synopsis"),
label=result.get("type"),
url=f"https://pluto.tv/{self.region}/on-demand/{kind}/{content}/details",
)
def get_titles(self) -> Titles_T:
try:
kind, content_id, episode_id = (
re.match(self.TITLE_RE, self.title).group(i) for i in ("type", "id", "episode")
)
except Exception:
raise ValueError("Could not parse ID from title - is the URL correct?")
if kind == "series" and episode_id:
r = self.session.get(self.config["endpoints"]["series"].format(season_id=content_id))
if not r.ok:
raise ConnectionError(f"{r.json().get('message')}")
data = r.json()
return Series(
[
Episode(
id_=episode.get("_id"),
service=self.__class__,
title=data.get("name"),
season=int(episode.get("season")),
number=int(episode.get("number")),
name=episode.get("name"),
year=None,
language="en", # self.region,
data=episode,
)
for series in data["seasons"]
for episode in series["episodes"]
if episode.get("_id") == episode_id
]
)
elif kind == "series":
r = self.session.get(self.config["endpoints"]["series"].format(season_id=content_id))
if not r.ok:
raise ConnectionError(f"{r.json().get('message')}")
data = r.json()
return Series(
[
Episode(
id_=episode.get("_id"),
service=self.__class__,
title=data.get("name"),
season=int(episode.get("season")),
number=int(episode.get("number")),
name=episode.get("name"),
year=self.year(episode),
language="en", # self.region,
data=episode,
)
for series in data["seasons"]
for episode in series["episodes"]
]
)
elif kind == "movies":
url = self.config["endpoints"]["movie"].format(video_id=content_id)
r = self.session.get(url, headers={"Authorization": f"Bearer {self.token}"})
if not r.ok:
raise ConnectionError(f"{r.json().get('message')}")
data = r.json()
return Movies(
[
Movie(
id_=movie.get("_id"),
service=self.__class__,
name=movie.get("name"),
language="en", # self.region,
data=movie,
year=self.year(movie),
)
for movie in data
]
)
def get_tracks(self, title: Title_T) -> Tracks:
url = self.config["endpoints"]["episodes"].format(episode_id=title.id)
episode = self.session.get(url).json()
sources = next((item.get("sources") for item in episode if not self.bumpers(item.get("name", ""))), None)
if not sources:
raise ValueError("Unable to find manifest for this title")
hls = next((x.get("file") for x in sources if x.get("type").lower() == "hls"), None)
dash = next((x.get("file") for x in sources if x.get("type").lower() == "dash"), None)
if dash and not self.force_hls:
self.license = self.config["endpoints"]["license"]
manifest = dash.replace("https://siloh.pluto.tv", "http://silo-hybrik.pluto.tv.s3.amazonaws.com")
tracks = DASH.from_url(manifest, self.session).to_tracks(language=title.language)
for track in tracks.audio:
role = track.data["dash"]["adaptation_set"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
else:
self.license = None
m3u8_url = hls.replace("https://siloh.pluto.tv", "http://silo-hybrik.pluto.tv.s3.amazonaws.com")
manifest = self.clean_manifest(self.session.get(m3u8_url).text)
tracks = HLS.from_text(manifest, m3u8_url).to_tracks(language=title.language)
# Remove separate AD audio tracks
for track in tracks.audio:
tracks.audio.remove(track)
return tracks
def get_chapters(self, title: Title_T) -> Chapters:
return Chapters()
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
if not self.license:
return None
r = self.session.post(url=self.license, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
# service specific functions
@staticmethod
def clean_manifest(text: str) -> str:
# Remove fairplay entries
index = text.find('#PLUTO-DRM:ID="fairplay')
if index == -1:
return text
else:
end_of_previous_line = text.rfind("\n", 0, index)
if end_of_previous_line == -1:
return ""
else:
return text[:end_of_previous_line]
@staticmethod
def bumpers(text: str) -> bool:
ads = (
"Pluto_TV_OandO",
"_ad",
"creative",
"Bumper",
"Promo",
"WarningCard",
)
return any(ad in text for ad in ads)
@staticmethod
def year(data: dict) -> Optional[int]:
title_year = (int(match.group(1)) if (match := re.search(r"\((\d{4})\)", data.get("name", ""))) else None)
slug_year = (int(match.group(1)) if (match := re.search(r"\b(\d{4})\b", data.get("slug", ""))) else None)
return None if title_year else slug_year
@@ -1,7 +0,0 @@
endpoints:
auth: https://boot.pluto.tv/v4/start
search: https://service-media-search.clusters.pluto.tv/v1/search
series: https://service-vod.clusters.pluto.tv/v3/vod/series/{season_id}/seasons
episodes: http://api.pluto.tv/v2/episodes/{episode_id}/clips.json
movie: https://service-vod.clusters.pluto.tv/v4/vod/items?ids={video_id}
license: https://service-concierge.clusters.pluto.tv/v1/wv/alt
@@ -1,149 +0,0 @@
import json
import re
from typing import Optional
from http.cookiejar import CookieJar
from langcodes import Language
import click
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.service import Service
from envied.core.titles import Movie, Movies, Title_T, Titles_T
from envied.core.tracks import Tracks
class PTHS(Service):
"""
Service code for Pathé Thuis (pathe-thuis.nl)
Version: 1.0.0
Security: SD @ L3 (Widevine)
FHD @ L1
Authorization: Cookies or authentication token
Supported:
• Movies → https://www.pathe-thuis.nl/film/{id}
Note:
Pathé Thuis does not have episodic content, only movies.
"""
TITLE_RE = (
r"^(?:https?://(?:www\.)?pathe-thuis\.nl/film/)?(?P<id>\d+)(?:/[^/]+)?$"
)
GEOFENCE = ("NL",)
NO_SUBTITLES = True
@staticmethod
@click.command(name="PTHS", short_help="https://www.pathe-thuis.nl")
@click.argument("title", type=str)
@click.pass_context
def cli(ctx, **kwargs):
return PTHS(ctx, **kwargs)
def __init__(self, ctx, title: str):
super().__init__(ctx)
m = re.match(self.TITLE_RE, title)
if not m:
raise ValueError(
f"Unsupported Pathé Thuis URL or ID: {title}\n"
"Use e.g. https://www.pathe-thuis.nl/film/30591"
)
self.movie_id = m.group("id")
self.drm_token = None
if self.config is None:
raise EnvironmentError("Missing service config for Pathé Thuis.")
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not cookies:
self.log.warning("No cookies provided, proceeding unauthenticated.")
return
token = next((c.value for c in cookies if c.name == "authenticationToken"), None)
if not token:
self.log.info("No authenticationToken cookie found, unauthenticated mode.")
return
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0",
"X-Pathe-Device-Identifier": "web-widevine-1",
"X-Pathe-Auth-Session-Token": token,
})
self.log.info("Authentication token successfully attached to session.")
def get_titles(self) -> Titles_T:
url = self.config["endpoints"]["metadata"].format(movie_id=self.movie_id)
r = self.session.get(url)
r.raise_for_status()
data = r.json()
movie = Movie(
id_=str(data["id"]),
service=self.__class__,
name=data["name"],
description=data.get("intro", ""),
year=data.get("year"),
language=Language.get(data.get("language", "en")),
data=data,
)
return Movies([movie])
def get_tracks(self, title: Title_T) -> Tracks:
ticket_id = self._get_ticket_id(title)
url = self.config["endpoints"]["ticket"].format(ticket_id=ticket_id)
r = self.session.get(url)
r.raise_for_status()
data = r.json()
stream = data["stream"]
manifest_url = stream.get("url") or stream.get("drmurl")
if not manifest_url:
raise ValueError("No stream manifest URL found.")
self.drm_token = stream["token"]
self.license_url = stream["rawData"]["licenseserver"]
tracks = DASH.from_url(manifest_url, session=self.session).to_tracks(language=title.language)
return tracks
def _get_ticket_id(self, title: Title_T) -> str:
"""Fetch the user's owned ticket ID if present."""
data = title.data
for t in (data.get("tickets") or []):
if t.get("playable") and str(t.get("movieId")) == str(self.movie_id):
return str(t["id"])
raise ValueError("No valid ticket found for this movie. Ensure purchase or login.")
def get_chapters(self, title: Title_T):
return []
def get_widevine_license(self, challenge: bytes, title: Title_T, track: AnyTrack) -> bytes:
if not self.license_url or not self.drm_token:
raise ValueError("Missing license URL or token.")
headers = {
"Content-Type": "application/octet-stream",
"Authorization": f"Bearer {self.drm_token}",
}
params = {"custom_data": self.drm_token}
r = self.session.post(self.license_url, params=params, data=challenge, headers=headers)
r.raise_for_status()
if not r.content:
raise ValueError("Empty license response, likely invalid or expired token.")
return r.content
@@ -1,3 +0,0 @@
endpoints:
metadata: "https://www.pathe-thuis.nl/api/movies/{movie_id}?include=editions"
ticket: "https://www.pathe-thuis.nl/api/tickets/{ticket_id}"
@@ -1,24 +0,0 @@
A collection of non-premium services for envied.
## Usage:
Clone repository:
`git clone https://github.com/stabbedbybrick/services.git`
Add folder to `envied.yaml`:
```
directories:
services: "path/to/services"
```
See help text for each service:
`unshackle dl SERVICE --help`
## Notes:
Some versions of the dependencies work better than others. These are the recommended versions as of 25/11/11:
- Shaka Packager: [v2.6.1](https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1)
- CCExtractor: [v0.93](https://github.com/CCExtractor/ccextractor/releases/tag/v0.93)
- MKVToolNix: [latest](https://mkvtoolnix.download/downloads.html)
- FFmpeg: [latest](https://ffmpeg.org/download.html)
@@ -1,791 +0,0 @@
import base64
from copy import copy
import datetime
import hashlib
import hmac
import json
import re
import os
import sys
from typing import Optional
from aiohttp import CookieJar
from pymediainfo import MediaInfo
from langcodes import Language
import click
import urllib.parse
from requests import HTTPError
from envied.core.config import config
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.manifests.dash import DASH
from envied.core.service import Service
from envied.core.titles import Title_T
from envied.core.titles.episode import Episode, Series
from envied.core.titles.movie import Movie, Movies
from envied.core.tracks.audio import Audio
from envied.core.tracks.chapters import Chapters
from envied.core.tracks.subtitle import Subtitle
from envied.core.tracks.tracks import Tracks
from envied.core.tracks.video import Video
from pyplayready.cdm import Cdm as PlayReadyCdm
class RKTN(Service):
"""
Service code for Rakuten's Rakuten TV streaming service (https://rakuten.tv).
\b
Authorization: Credentials
Security: FHD-UHD@L1, SD-FHD@L3; with trick
\b
Maximum of 3 audio tracks, otherwise will fail because Rakuten blocks more than 3 requests.
Subtitles requests expires fast, so together with video and audio it will fail.
If you want subs, use -S or -na -nv -nc, and download the rest separately.
\b
Command for Titles with no SDR (if not set range to HDR10 it will fail):
uv run unshackle dl -r HDR10 [OPTIONS] RKTN -m https://www.rakuten.tv/...
\b
TODO: - TV Shows are not yet supported as there's 0 TV Shows to purchase, rent, or watch in my region
\b
NOTES: - Only movies are supported as my region's Rakuten has no TV shows available to purchase at all
"""
ALIASES = ["RakutenTV", "rakuten", "rakutentv"]
TITLE_RE = r"^(?:https?://(?:www\.)?rakuten\.tv/([a-z]+/|)movies(?:/[a-z]{2})?/)(?P<id>[a-z0-9-]+)"
LANG_MAP = {
"es": "es-ES",
"pt": "pt-PT",
}
@staticmethod
@click.command(name="RakutenTV", short_help="https://rakuten.tv")
@click.argument("title", type=str, required=False)
@click.option(
"-dev",
"--device",
default=None,
type=click.Choice(
[
"web", # Device: Web Browser - Maximum Quality: 720p - DRM: Widevine
"android", # Device: Android Phone - Maximum Quality: 720p - DRM: Widevine
"atvui40", # Device: AndroidTV - Maximum Quality: 2160p - DRM: Widevine
"lgui40", # Device: LG SMART TV - Maximum Quality: 2160p - DRM: Playready
"smui40", # Device: Samsung SMART TV - Maximum Quality: 2160p - DRM: Playready
],
case_sensitive=True,
),
help="The device you want to make requests with.",
)
@click.option(
"-m", "--movie", is_flag=True, default=False, help="Title is a movie."
)
@click.option(
"-dal", "--desired-audio-language", type=str, default="SPA,ENG", help="Select desired audio language tracks for this title. Default SPA,ENG. Separate multiple languages with a comma."
)
@click.pass_context
def cli(ctx, **kwargs):
return RKTN(ctx, **kwargs)
def __init__(self, ctx, title, device, movie, desired_audio_language):
super().__init__(ctx)
#self.parse_title(ctx, title)
self.title = title
self.cdm = ctx.obj.cdm
self.playready = isinstance(self.cdm, PlayReadyCdm)
self.desired_audio_language = desired_audio_language
self.range = ctx.parent.params.get("range_")[0].name or "SDR"
self.vcodec = ctx.parent.params.get("vcodec") or Video.Codec.AVC # Defaults to H264
self.resolution = "UHD" if (self.vcodec.extension.lower() == "h265" or self.range in ['HYBRID', 'HDR10', 'HDR10P', 'DV']) else "FHD"
self.device = "lgui40" if self.playready else "android"
self.movie = movie or "movies" in title
self.audio_languages = []
# set a custom device if provided
if device is not None:
self.device = device
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
self.session.headers.update(
{
"Origin": "https://rakuten.tv/",
"User-Agent": "Mozilla/5.0 (Linux; Android 11; SHIELD Android TV Build/RQ1A.210105.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36",
}
)
def get_titles(self):
self.pair_device()
if self.movie:
endpoint = self.config["endpoints"]["title"]
else:
endpoint = self.config["endpoints"]["show"]
params = urllib.parse.urlencode(
{
"classification_id": self.classification_id,
"device_identifier": self.config["clients"][self.device][
"device_identifier"
],
"device_serial": self.config["clients"][self.device]["device_serial"],
"locale": self.locale,
"market_code": self.market_code,
"session_uuid": self.session_uuid,
"timestamp": f"{int(datetime.datetime.now().timestamp())}005",
"support_closed_captions": "true",
}
)
title_url = endpoint.format(
title_id=self.title
) + params
title = self.session.get(url=title_url).json()
if "errors" in title:
error = title["errors"][0]
if error["code"] == "error.not_found":
self.log.error(f"Title [{self.title}] was not found on this account.")
else:
self.log.error(
f"Unable to get title info: {error['message']} [{error['code']}]"
)
sys.exit(1)
title = self.get_info(title["data"])
if self.movie:
return Movies(
[
Movie(
id_=self.title,
service=self.__class__,
name=title["title"],
year=title["year"],
language="en",
data=title,
description=title["plot"],
)
]
)
else:
episodes_list = []
#title_ep = self.get_info(title["data"]['episodes'])
for season in title["tv_show"]["seasons"]:
data_season = endpoint.format(
title_id=season["id"]
) + params
data = self.session.get(url=data_season).json()
if "errors" in data:
error = data["errors"][0]
if error["code"] == "error.not_found":
self.log.error(f"Season [{season['id']}] was not found on this account.")
else:
self.log.error(
f"Unable to get title info: {error['message']} [{error['code']}]"
)
continue
for episode in data["data"]["episodes"]:
episodes_list.append(
Episode(
id_=episode["id"],
service=self.__class__,
title=episode["tv_show_title"],
season=episode["season_number"],
number=episode["number"],
name=episode["title"] or episode['display_name'],
description=episode["short_plot"],
year=episode["year"],
language="en",
data=episode,
)
)
return Series(episodes_list)
def get_tracks(self, title: Title_T) -> Tracks:
# Obtener tracks para todos los idiomas de audio disponibles
all_tracks = None
for audio_lang in self.audio_languages:
self.log.info(f"Getting tracks for audio language: {audio_lang}")
# Obtener stream info para este idioma específico
stream_info = self.get_avod(audio_lang, title) if self.kind == "avod" else self.get_me(audio_lang, title)
if "errors" in stream_info:
error = stream_info["errors"][0]
if "error.streaming.no_active_right" in stream_info["errors"][0]["code"]:
self.log.error(
" x You don't have the rights for this content\n You need to rent or buy it first"
)
else:
self.log.error(
f" - Failed to get track info: {error['message']} [{error['code']}]"
)
sys.exit(1)
stream_info = stream_info["data"]["stream_infos"][0]
if all_tracks is None:
# Primera iteración: crear el objeto tracks principal
self.license_url = stream_info["license_url"]
all_tracks = DASH.from_url(url=stream_info["url"], session=self.session).to_tracks(language=title.language)
# Procesar subtítulos (solo una vez)
subtitle_tracks = []
for subtitle in stream_info.get("all_subtitles", []):
subtitle_tracks += [
Subtitle(
id_=hashlib.md5(subtitle["url"].encode()).hexdigest()[0:6],
url=subtitle["url"],
codec=Subtitle.Codec.from_mime(subtitle["format"]),
forced=subtitle["forced"],
language=subtitle["locale"],
)
]
all_tracks.add(subtitle_tracks)
else:
# Iteraciones adicionales: obtener tracks de audio adicionales
temp_tracks = DASH.from_url(url=stream_info["url"], session=self.session).to_tracks(language=title.language)
# Agregar solo los tracks de audio nuevos
for audio_track in temp_tracks.audio:
# Verificar que no sea duplicado basado en el idioma y codec
is_duplicate = False
for existing_audio in all_tracks.audio:
if (existing_audio.language == audio_track.language and
existing_audio.codec == audio_track.codec):
is_duplicate = True
break
if not is_duplicate:
all_tracks.audio.append(audio_track)
# Procesar HDR para videos
for video in all_tracks.videos:
if "HDR10" in video.url:
video.range = Video.Range.HDR10
# Aplicar el método append_tracks mejorado
self.append_tracks(all_tracks)
return all_tracks
def get_chapters(self, title: Title_T) -> Chapters:
return Chapters([])
def get_me(self, audio_language=None, title: Title_T = None):
# Si no se especifica idioma, usar el primero disponible
if audio_language is None:
audio_language = self.audio_languages[0]
stream_info_url = self.config["endpoints"]["manifest"].format(
kind="me"
) + urllib.parse.urlencode(
{
"audio_language": audio_language, # Usar el idioma especificado
"audio_quality": "5.1", # Will get better audio in different request to make sure it wont error
"classification_id": self.classification_id,
"content_id": title.id,
"content_type": "movies" if self.movie else "episodes",
"device_identifier": self.config["clients"][self.device][
"device_identifier"
],
"device_serial": "not_implemented",
"device_stream_audio_quality": "5.1",
"device_stream_hdr_type": self.hdr_type,
"device_stream_video_quality": self.resolution,
"device_uid": "affa434b-8b7c-4ff3-a15e-df1fe500e71e",
"device_year": self.config["clients"][self.device]["device_year"],
"disable_dash_legacy_packages": "false",
"gdpr_consent": self.config["gdpr_consent"],
"gdpr_consent_opt_out": 0,
"hdr_type": self.hdr_type,
"ifa_subscriber_id": self.ifa_subscriber_id,
"locale": self.locale,
"market_code": self.market_code,
"player": self.config["clients"][self.device]["player"],
"player_height": 1080,
"player_width": 1920,
"publisher_provided_id": "046f58b1-d89b-4fa4-979b-a9bcd6d78a76",
"session_uuid": self.session_uuid,
"strict_video_quality": "false",
"subtitle_formats": ["vtt"],
"subtitle_language": "MIS",
"timestamp": f"{int(datetime.datetime.now().timestamp())}122",
"video_type": "stream",
}
)
stream_info_url += "&signature=" + self.generate_signature(stream_info_url)
return self.session.post(
url=stream_info_url,
).json()
def get_avod(self, audio_language=None, title: Title_T = None):
# Si no se especifica idioma, usar el primero disponible
if audio_language is None:
audio_language = self.audio_languages[0]
stream_info_url = self.config["endpoints"]["manifest"].format(
kind="avod"
) + urllib.parse.urlencode(
{
"device_stream_video_quality": self.resolution,
"device_identifier": self.config["clients"][self.device][
"device_identifier"
],
"market_code": self.market_code,
"session_uuid": self.session_uuid,
"timestamp": f"{int(datetime.datetime.now().timestamp())}122",
}
)
stream_info_url += "&signature=" + self.generate_signature(stream_info_url)
return self.session.post(
url=stream_info_url,
data={
"hdr_type": self.hdr_type,
"audio_quality": "5.1", # Will get better audio in different request to make sure it wont error
"app_version": self.config["clients"][self.device]["app_version"],
"content_id": title.id,
"video_quality": self.resolution,
"audio_language": audio_language, # Usar el idioma especificado
"video_type": "stream",
"device_serial": self.config["clients"][self.device]["device_serial"],
"content_type": "movies" if self.movie else "episodes",
"classification_id": self.classification_id,
"subtitle_language": "MIS",
"player": self.config["clients"][self.device]["player"],
},
).json()
def generate_signature(self, url):
up = urllib.parse.urlparse(url)
digester = hmac.new(
self.access_token.encode(),
f"POST{up.path}{up.query}".encode(),
hashlib.sha1,
)
return (
base64.b64encode(digester.digest())
.decode("utf-8")
.replace("+", "-")
.replace("/", "_")
)
def append_tracks(self, tracks):
"""
Busca y agrega tracks adicionales de video y audio que no están en el manifest.
"""
if not tracks.videos:
self.log.warning("No video tracks found, skipping append_tracks")
return
# Buscar tracks de video adicionales
self._append_video_tracks(tracks)
# Buscar tracks de audio adicionales
self._append_audio_tracks(tracks)
def _append_video_tracks(self, tracks):
"""Busca y agrega tracks de video adicionales para H.264."""
if not tracks.videos:
return
codec = tracks.videos[0].codec
# Solo buscar tracks adicionales para H.264
if codec != Video.Codec.AVC:
self.log.debug(f"Skipping video track search (codec: {codec.name}, only works for AVC/H.264)")
return
# Extraer el patrón del codec de la URL
url_pattern = tracks.videos[-1].url
codec_match = re.search(r'(avc1|h264)-(\d+)', url_pattern, re.IGNORECASE)
if not codec_match:
self.log.debug("Could not find codec pattern in URL for video track search")
return
codec_prefix = codec_match.group(1) # "avc1" o "h264"
self.log.info(f"Searching for additional H.264 video tracks (pattern: {codec_prefix})...")
# Usar el directorio temp de Unshackle
temp_file = os.path.join(str(config.directories.temp), "video_test.mp4")
tracks_found = 0
for n in range(100):
# Generar URL del siguiente track
current_number = len(tracks.videos) + 1
ismv = re.sub(
rf"{codec_prefix}-\d+",
rf"{codec_prefix}-{current_number}",
tracks.videos[-1].url,
)
# Verificar si existe
try:
response = self.session.head(ismv, timeout=5)
if response.status_code != 200:
self.log.debug(f"Video track search ended at index {current_number}")
break
except Exception as e:
self.log.debug(f"Video track search failed: {e}")
break
# Crear copia del último video track
video = copy(tracks.videos[-1])
video.url = ismv
video.id_ = hashlib.md5(ismv.encode()).hexdigest()[:16]
# Descargar chunk para obtener info con MediaInfo
try:
with open(temp_file, "wb") as chunkfile:
data = self.session.get(
url=ismv,
headers={"Range": "bytes=0-50000"},
timeout=10
)
chunkfile.write(data.content)
# Parsear con MediaInfo
info = MediaInfo.parse(temp_file)
if not info.video_tracks:
self.log.debug(f"No video info found for track {current_number}")
continue
video_info = info.video_tracks[0]
video.height = video_info.height
video.width = video_info.width
video.bitrate = video_info.maximum_bit_rate or video_info.bit_rate
# Agregar el track
tracks.videos.append(video)
tracks_found += 1
self.log.info(
f" + Added video track #{current_number}: "
f"{video.width}x{video.height} @ {video.bitrate} bps"
)
except Exception as e:
self.log.warning(f"Failed to process video track {current_number}: {e}")
break
finally:
if os.path.exists(temp_file):
os.remove(temp_file)
if tracks_found > 0:
self.log.info(f"Total additional video tracks found: {tracks_found}")
def _append_audio_tracks(self, tracks):
"""Busca y agrega tracks de audio adicionales para todos los idiomas seleccionados."""
if not tracks.audio:
self.log.warning("No audio tracks found to use as base")
return
if not hasattr(self, 'audio_languages') or not self.audio_languages:
self.log.debug("No audio languages configured")
return
self.log.info(f"Searching for additional audio tracks in languages: {self.audio_languages}")
# Codecs a probar (en orden de preferencia)
codecs_to_try = ["ec-3", "ac-3", "dts", "mp4a"]
# Usar el directorio temp de Unshackle
temp_file = os.path.join(str(config.directories.temp), "audio_test.mp4")
base_audio = tracks.audio[0]
base_url = base_audio.url
tracks_found = 0
for language in self.audio_languages:
for codec_name in codecs_to_try:
# Generar URL del track
# Patrón: audio-{LANG}-{CODEC}-{NUMBER}
isma = re.sub(
r"audio-[a-zA-Z]{2,3}-[a-z0-9\-]+-\d+",
f"audio-{language.lower()}-{codec_name}-1",
base_url,
)
# Verificar si existe
try:
response = self.session.head(isma, timeout=5)
if response.status_code != 200:
continue
except Exception:
continue
# Verificar si ya existe (evitar duplicados)
if any(audio.url == isma for audio in tracks.audio):
self.log.debug(f"Audio track already exists: {language}-{codec_name}")
continue
# Crear nuevo track de audio
audio = copy(base_audio)
audio.url = isma
audio.id_ = hashlib.md5(isma.encode()).hexdigest()[:16]
# Mapear idioma
mapped_lang = self.LANG_MAP.get(language, language)
audio.language = Language.get(mapped_lang)
# Determinar si es idioma original
if tracks.videos:
audio.is_original_lang = (
audio.language.language == tracks.videos[0].language.language
)
# Obtener información del track con MediaInfo
try:
with open(temp_file, "wb") as bytetest:
data = self.session.get(
url=isma,
headers={"Range": "bytes=0-50000"},
timeout=10
)
bytetest.write(data.content)
info = MediaInfo.parse(temp_file)
if not info.audio_tracks:
self.log.debug(f"No audio info found for {language}-{codec_name}")
continue
audio_info = info.audio_tracks[0]
audio.bitrate = audio_info.bit_rate
# Detectar canales basado en codec
if codec_name in ["ec-3", "ac-3", "dts"]:
audio.channels = audio_info.channel_s or "5.1"
else: # mp4a (AAC)
audio.channels = audio_info.channel_s or "2.0"
# Actualizar codec
# Para Unshackle, necesitas mantener el formato correcto
audio.codec = Audio.Codec.from_codecs(codec_name)
# Agregar el track
tracks.audio.append(audio)
tracks_found += 1
self.log.info(
f" + Added audio track: {audio.language.display_name()} "
f"[{codec_name.upper()}] - {audio.channels}ch @ {audio.bitrate} bps"
)
except Exception as e:
self.log.debug(f"Failed to process audio {language}-{codec_name}: {e}")
finally:
if os.path.exists(temp_file):
os.remove(temp_file)
if tracks_found > 0:
self.log.info(f"Total additional audio tracks found: {tracks_found}")
def get_widevine_service_certificate(self, **kwargs):
return self.config["certificate"]
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]:
res = self.session.post(
url=self.license_url,
data=challenge,
)
if "errors" in res.text:
res = res.json()
if res["errors"][0]["message"] == "HttpException: Forbidden":
self.log.error(
" x This CDM is not eligible to decrypt this\n"
" content or has been blacklisted by RakutenTV"
)
elif res["errors"][0]["message"] == "HttpException: An error happened":
self.log.error(
" x This CDM seems to be revoked and\n"
" therefore it can't decrypt this content",
)
sys.exit(1)
return res.content
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[bytes]:
res = self.session.post(
url=self.license_url,
data=challenge,
)
if "errors" in res.text:
res = res.json()
if res["errors"][0]["message"] == "HttpException: Forbidden":
self.log.error(
" x This CDM is not eligible to decrypt this\n"
" content or has been blacklisted by RakutenTV"
)
elif res["errors"][0]["message"] == "HttpException: An error happened":
self.log.error(
" x This CDM seems to be revoked and\n"
" therefore it can't decrypt this content",
)
sys.exit(1)
return res.content
def pair_device(self):
# TODO: Make this return the tokens, move print out of the func
# log.info_("Logging into RakutenTV as an Android device")
if not self.credential:
self.log.error(" - No credentials provided, unable to log in.")
sys.exit(1)
try:
res = self.session.post(
url=self.config["endpoints"]["auth"],
params={
"device_identifier": self.config["clients"][self.device][
"device_identifier"
]
},
data={
"app_version": self.config["clients"][self.device]["app_version"],
"device_metadata[uid]": self.config["clients"][self.device][
"device_serial"
],
"device_metadata[os]": self.config["clients"][self.device][
"device_os"
],
"device_metadata[model]": self.config["clients"][self.device][
"device_model"
],
"device_metadata[year]": self.config["clients"][self.device][
"device_year"
],
"device_serial": self.config["clients"][self.device][
"device_serial"
],
"device_metadata[trusted_uid]": False,
"device_metadata[brand]": self.config["clients"][self.device][
"device_brand"
],
"classification_id": 69,
"user[password]": self.credential.password,
"device_metadata[app_version]": self.config["clients"][self.device][
"app_version"
],
"user[username]": self.credential.username,
"device_metadata[serial_number]": self.config["clients"][
self.device
]["device_serial"],
},
).json()
except HTTPError as e:
if e.response.status_code == 403:
self.log.error(
" - Rakuten returned a 403 (FORBIDDEN) error. "
"This could be caused by your IP being detected as a proxy, or regional issues. Cannot continue."
)
if "errors" in res:
error = res["errors"][0]
if "exception.forbidden_vpn" in error["code"]:
self.log.error(" x RakutenTV is detecting this VPN or Proxy")
else:
self.log.error(f" - Login failed: {error['message']} [{error['code']}]")
self.access_token = res["data"]["user"]["access_token"]
self.ifa_subscriber_id = res["data"]["user"]["avod_profile"][
"ifa_subscriber_id"
]
self.session_uuid = res["data"]["user"]["session_uuid"]
self.classification_id = res["data"]["user"]["profile"]["classification"]["id"]
self.locale = res["data"]["market"]["locale"]
self.market_code = res["data"]["market"]["code"]
def get_info(self, title):
self.kind = title["labels"]["purchase_types"][0]["kind"]
# self.available_resolutions = [x for x in title["labels"]["video_qualities"]]
# if any(x["abbr"] == "UHD" for x in title["labels"]["video_qualities"]):
# self.resolution = "UHD"
# elif any(x["abbr"] == "FHD" for x in title["labels"]["video_qualities"]):
# self.resolution = "FHD"
# elif any(x["abbr"] == "HD" for x in title["labels"]["video_qualities"]):
# self.resolution = "HD"
# else:
# self.resolution = "SD"
self.available_hdr_types = [x for x in title["labels"]["hdr_types"]]
if any(x["abbr"] == "HDR10_PLUS" for x in self.available_hdr_types) and any(
x["abbr"] == "HDR10_PLUS"
for x in title["view_options"]["support"]["hdr_types"]
):
self.hdr_type = "HDR10_PLUS"
elif any(x["abbr"] == "DOLBY_VISION" for x in self.available_hdr_types) and any(
x["abbr"] == "DOLBY_VISION"
for x in title["view_options"]["support"]["hdr_types"]
):
self.hdr_type = "DOLBY_VISION"
elif any(x["abbr"] == "HDR10" for x in self.available_hdr_types) and any(
x["abbr"] == "HDR10" for x in title["view_options"]["support"]["hdr_types"]
):
self.hdr_type = "HDR10"
else:
self.hdr_type = "NONE"
# Obtener view_options desde title o episodes
view_options = title.get("episodes", [{}])[0].get("view_options") or title.get("view_options")
# FIJO: Obtener TODOS los idiomas de audio disponibles
if len(view_options["private"]["offline_streams"]) == 1:
# Caso 1: Un solo stream con múltiples idiomas
self.audio_languages = [
x["abbr"]
for x in view_options["private"]["streams"][0]["audio_languages"]
]
else:
# Caso 2: Múltiples streams, obtener todos los idiomas únicos
all_audio_languages = []
for stream in view_options["private"]["streams"]:
for audio_lang in stream["audio_languages"]:
if audio_lang["abbr"] not in all_audio_languages:
all_audio_languages.append(audio_lang["abbr"])
self.audio_languages = all_audio_languages
# # TODO: Look up only for languages chosen by the user
# print(f"\nAvailable audio languages: {', '.join(self.audio_languages)}")
# selected = input("Type your desired languages, maximum of 3, UPPER CASE (ex: ENG,SPA,FRA): ")
selected_langs = [lang.strip() for lang in self.desired_audio_language.split(",") if lang.strip() in self.audio_languages]
if not selected_langs:
self.log.error("No selected language. Exiting.")
self.audio_languages = selected_langs
# Log para debug
self.log.info(f"Selected audio languages: {self.audio_languages}")
return title
@@ -1,71 +0,0 @@
certificate: |
CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8yzdQPgZFuBTYdrjfQFEEQa2M462xG
7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHleB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3rM3BdmDoh+07svUoQyk
dJDKR+ql1DghjduvHK3jOS8T1v+2RC/THhv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ7c4kcHCCaA1vZ8bYLErF8xNEkKd
O7DevSy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmluZS5jb20SgAOuNHMUtag1KX8nE4j7e7jLUnfS
SYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M4PxL/CCpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80
NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9qm9Nta/gr52u/DLpP3lnSq8x2/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWC
q5z3CqCLl5+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeFHd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkPj89qPwXCYcOxF+6gjo
mPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98X/8z8QSQ+spbJTYLdgFenFoGq47gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=
gdpr_consent: |
CPGeIEAPV65UAADABBNLCGCsAP_AAH_AAAAAHrsXZCpcBSlgYCpoAIoAKIAUEAAAgyAAABAAAoABCAAAIAQAgAAgIAAAAAAAAAAAIAJAAQAAAAEAAAAAAA
AAAAAIIACAAAAAIABAAAAAAAAACAAAAAAAAAAAAAAEAAAAgABAABAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAgZ8xdkKlwFKWBgKGgAigAogBQQAACDIAAA
EAACAAAIAAAgBACAACAAAAAAAAAAAAAgAgABAAAAAQAAAAAAAAAAAAggAAAAAAAgAEAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAEAAEAAAAAAQAA.YAAAAAAAA8DA
endpoints:
title: https://gizmo.rakuten.tv/v3/movies/{title_id}?
show: https://gizmo.rakuten.tv/v3/seasons/{title_id}?
manifest: https://gizmo.rakuten.tv/v3/{kind}/streamings?
auth: https://gizmo.rakuten.tv/v3/me/login_or_wuaki_link
clients:
web:
app_version: v3.0.11
device_identifier: web
device_serial: 6cc3584a-c182-4cc1-9f8d-b90e4ed76de9
player: web:DASH-CENC:WVM
device_os: Windows 10
device_model: GENERIC
device_year: 2019
device_brand: chrome
device_sdk: 100.0.4896
android:
app_version: 3.22.0
device_identifier: android
device_serial: 3187ad6c-4d1c-4cbb-9c59-8396d054eb2a
player: android:DASH-CENC
device_os: Android
device_model: SM-A105FN
device_year: 2021
device_brand: Samsung
device_sdk: ""
atvui40:
app_version: v2.77.0
device_identifier: atvui40
device_serial: 0424814603535001d1b1
player: atvui40:DASH-CENC:WVM
device_os: Android TV UI 40
device_model: SHIELD Android TV
device_year: 1970
device_brand: NVIDIA
device_sdk: ""
lgui40:
app_version: v2.77.0
device_identifier: lgui40
device_serial: 203WRMD8U920
player: lgui40:DASH-CENC:PR
device_os: LG UI 40
device_model: OLED65C11LB
device_year: 2021
device_brand: LG
device_sdk: ""
smui40:
app_version: v2.77.0
device_identifier: smui40
device_serial: 6cc3584a-c182-4cc1-9f8d-b90e4ed76de9
player: smtvui40:DASH-CENC:WVM
device_os: Samsung UI 40
device_model: QE43Q60RATXXH
device_year: 2019
device_brand: Samsung
device_sdk: ""
@@ -1,261 +0,0 @@
import json
import re
import sys
from collections.abc import Generator
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from http.cookiejar import CookieJar
from typing import Any, Optional
from urllib.parse import unquote, urlparse
import click
import requests
from envied.core.credential import Credential
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapter, Tracks
class ROKU(Service):
"""
Service code for The Roku Channel (https://therokuchannel.roku.com)
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: Cookies
Robustness:
Widevine:
L3: 1080p, DD5.1
\b
Tips:
- Use complete title/episode URL or id as input:
https://therokuchannel.roku.com/details/e05fc677ab9c5d5e8332f123770697b9/paddington
OR
e05fc677ab9c5d5e8332f123770697b9
- Supports movies, series, and single episodes
- Search is geofenced
"""
GEOFENCE = ("us",)
TITLE_RE = r"^(?:https?://(?:www.)?therokuchannel.roku.com/(?:details|watch)/)?(?P<id>[a-z0-9-]+)"
@staticmethod
@click.command(name="ROKU", short_help="https://therokuchannel.roku.com", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx, **kwargs):
return ROKU(ctx, **kwargs)
def __init__(self, ctx, title):
self.title = re.match(self.TITLE_RE, title).group("id")
super().__init__(ctx)
self.license: str
def authenticate(
self,
cookies: Optional[CookieJar] = None,
credential: Optional[Credential] = None,
) -> None:
super().authenticate(cookies, credential)
if cookies is not None:
self.session.cookies.update(cookies)
def search(self) -> Generator[SearchResult, None, None]:
token = self.session.get(self.config["endpoints"]["token"]).json()["csrf"]
headers = {"csrf-token": token}
payload = {"query": self.title}
r = self.session.post(self.config["endpoints"]["search"], headers=headers, json=payload)
r.raise_for_status()
results = r.json()
for result in results["view"]:
if result["content"]["type"] not in ["zone", "provider"]:
_id = result["content"].get("meta", {}).get("id")
_desc = result["content"].get("descriptions", {})
label = f'{result["content"].get("type")} ({result["content"].get("releaseYear")})'
if result["content"].get("viewOptions"):
label += f' ({result["content"]["viewOptions"][0].get("priceDisplay")})'
title = re.sub(r"^-|-$", "", re.sub(r"\W+", "-", result["content"].get("title").lower()))
yield SearchResult(
id_=_id,
title=title,
description=_desc["250"]["text"] if _desc.get("250") else None,
label=label,
url=f"https://therokuchannel.roku.com/details/{_id}/{title}",
)
def get_titles(self) -> Titles_T:
data = self.session.get(self.config["endpoints"]["content"] + self.title).json()
if not data["isAvailable"]:
self.log.error("This title is temporarily unavailable or expired")
sys.exit(1)
if data["type"] in ["movie", "tvspecial"]:
return Movies(
[
Movie(
id_=data["meta"]["id"],
service=self.__class__,
name=data["title"],
year=data["releaseYear"],
language=data["viewOptions"][0]["media"].get("originalAudioLanguage", "en"),
data=data,
)
]
)
elif data["type"] == "series":
episodes = self.fetch_episodes(data)
return Series(
[
Episode(
id_=episode["meta"]["id"],
service=self.__class__,
title=data["title"],
season=int(episode["seasonNumber"]),
number=int(episode["episodeNumber"]),
name=episode["title"],
year=data["releaseYear"],
language=episode["viewOptions"][0]["media"].get("originalAudioLanguage", "en"),
data=data,
)
for episode in episodes
]
)
elif data["type"] == "episode":
return Series(
[
Episode(
id_=data["meta"]["id"],
service=self.__class__,
title=data["title"],
season=int(data["seasonNumber"]),
number=int(data["episodeNumber"]),
name=data["title"],
year=data["releaseYear"],
language=data["viewOptions"][0]["media"].get("originalAudioLanguage", "en"),
data=data,
)
]
)
def get_tracks(self, title: Title_T) -> Tracks:
token = self.session.get(self.config["endpoints"]["token"]).json()["csrf"]
options = title.data["viewOptions"]
subscription = options[0].get("license", "").lower()
authenticated = next((x for x in options if x.get("isAuthenticated")), None)
if subscription == "subscription" and not authenticated:
self.log.error("This title is only available to subscribers")
sys.exit(1)
play_id = authenticated.get("playId") if authenticated else options[0].get("playId")
provider_id = authenticated.get("providerId") if authenticated else options[0].get("providerId")
headers = {
"csrf-token": token,
}
payload = {
"rokuId": title.id,
"playId": play_id,
"mediaFormat": "mpeg-dash",
"drmType": "widevine",
"quality": "fhd",
"providerId": provider_id,
}
r = self.session.post(
self.config["endpoints"]["vod"],
headers=headers,
json=payload,
)
r.raise_for_status()
videos = r.json()["playbackMedia"]["videos"]
self.license = next(
(
x["drmParams"]["licenseServerURL"]
for x in videos
if x.get("drmParams") and x["drmParams"]["keySystem"] == "Widevine"
),
None,
)
url = next((x["url"] for x in videos if x["streamFormat"] == "dash"), None)
if url and "origin" in urlparse(url).query:
url = unquote(urlparse(url).query.split("=")[1]).split("?")[0]
tracks = DASH.from_url(url=url).to_tracks(language=title.language)
tracks.videos[0].data["playbackMedia"] = r.json()["playbackMedia"]
for track in tracks.audio:
label = track.data["dash"]["adaptation_set"].find("Label")
if label is not None and "description" in label.text:
track.descriptive = True
for track in tracks.subtitles:
label = track.data["dash"]["adaptation_set"].find("Label")
if label is not None and "caption" in label.text:
track.cc = True
return tracks
def get_chapters(self, title: Title_T) -> list[Chapter]:
track = title.tracks.videos[0]
chapters = []
if track.data.get("playbackMedia", {}).get("adBreaks"):
timestamps = sorted(track.data["playbackMedia"]["adBreaks"])
chapters = [Chapter(name=f"Chapter {i + 1:02}", timestamp=ad.split(".")[0]) for i, ad in enumerate(timestamps)]
if track.data.get("playbackMedia", {}).get("creditCuePoints"):
start = next((
x.get("start") for x in track.data["playbackMedia"]["creditCuePoints"] if x.get("start") != 0), None)
if start:
chapters.append(
Chapter(
name="Credits",
timestamp=datetime.fromtimestamp((start / 1000), tz=timezone.utc).strftime("%H:%M:%S.%f")[:-3],
)
)
return chapters
def get_widevine_service_certificate(self, **_: Any) -> str:
return # WidevineCdm.common_privacy_cert
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
r = self.session.post(url=self.license, data=challenge)
if r.status_code != 200:
self.log.error(r.text)
sys.exit(1)
return r.content
# service specific functions
def fetch_episode(self, episode: dict) -> json:
try:
r = self.session.get(self.config["endpoints"]["content"] + episode["meta"]["id"])
r.raise_for_status()
return r.json()
except requests.exceptions.RequestException as e:
self.log.error(f"An error occurred while fetching episode {episode['meta']['id']}: {e}")
return None
def fetch_episodes(self, data: dict) -> list:
"""TODO: Switch to async once https proxies are fully supported"""
with ThreadPoolExecutor(max_workers=10) as executor:
tasks = list(executor.map(self.fetch_episode, data["episodes"]))
return [task for task in tasks if task is not None]
@@ -1,5 +0,0 @@
endpoints:
content: https://therokuchannel.roku.com/api/v2/homescreen/content/https%3A%2F%2Fcontent.sr.roku.com%2Fcontent%2Fv1%2Froku-trc%2F
vod: https://therokuchannel.roku.com/api/v3/playback
token: https://therokuchannel.roku.com/api/v1/csrf
search: https://therokuchannel.roku.com/api/v1/search
@@ -1,285 +0,0 @@
from __future__ import annotations
import base64
import json
import re
from collections.abc import Generator
from typing import Any, Optional, Union
from urllib.parse import urljoin
import click
from requests import Request
from envied.core.constants import AnyTrack
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Chapter, Chapters, Tracks
from envied.core.utils.xml import load_xml
class RTE(Service):
"""
\b
Service code for RTE Player streaming service (https://www.rte.ie/player/).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: None
Robustness:
Widevine:
L3: 1080p, AAC2.0
\b
Tips:
- Input (pay attention to the URL format):
SERIES: https://www.rte.ie/player/series/crossfire/10003928-00-0000
EPISODE: https://www.rte.ie/player/series/crossfire/10003928-00-0000?epguid=AQ10003929-01-0001
MOVIE: https://www.rte.ie/player/movie/glass/360230440380
\b
Notes:
- Since some content is accessible worldwide, geofence is deactivated.
- Using an IE IP-address is recommended to access everything.
"""
# GEOFENCE = ("ie",)
@staticmethod
@click.command(name="RTE", short_help="https://www.rte.ie/player/", help=__doc__)
@click.argument("title", type=str, required=False)
@click.pass_context
def cli(ctx, **kwargs) -> RTE:
return RTE(ctx, **kwargs)
def __init__(self, ctx, title):
self.title = title
super().__init__(ctx)
self.base_url = self.config["endpoints"]["base_url"]
self.feed = self.config["endpoints"]["feed"]
self.license = self.config["endpoints"]["license"]
def search(self) -> Generator[SearchResult, None, None]:
params = {
"byProgramType": "Series|Movie",
"q": f"title:({self.title})",
"range": "0-40",
"schema": "2.15",
"sort": "rte$rank|desc",
"gzip": "true",
"omitInvalidFields": "true",
}
results = self._request(f"{self.feed}/f/1uC-gC/rte-prd-prd-search", params=params)["entries"]
for result in results:
link = "https://www.rte.ie/player/{}/{}/{}"
series = result.get("plprogram$programType").lower() == "series"
_id = result.get("guid") if series else result.get("id").split("/")[-1]
_title = result.get("title") if series else result.get("plprogram$longTitle")
_type = result.get("plprogram$programType")
title = _title.format(_type, _title, _id).lower()
title = re.sub(r"\W+", "-", title)
title = re.sub(r"^-|-$", "", title)
yield SearchResult(
id_=link.format(_type, title, _id),
title=_title,
description=result.get("plprogram$shortDescription"),
label=_type,
url=link.format(_type, title, _id),
)
def get_titles(self) -> Titles_T:
title_re = (
r"https://www\.rte\.ie/player"
r"/(?P<type>series|movie)"
r"/(?P<slug>[a-zA-Z0-9_-]+)"
r"/(?P<id>[a-zA-Z0-9_\-=?]+)/?$"
)
try:
kind, _, title_id = (re.match(title_re, self.title).group(i) for i in ("type", "slug", "id"))
except Exception:
raise ValueError("- Could not parse ID from input")
episode = title_id.split("=")[1] if "epguid" in title_id else None
if episode:
episode = self._episode(title_id, episode)
return Series(episode)
elif kind == "movie":
movie = self._movie(title_id)
return Movies(movie)
elif kind == "series":
episodes = self._show(title_id)
return Series(episodes)
def get_tracks(self, title: Title_T) -> Tracks:
self.token, self.account = self.get_config()
media = title.data["plprogramavailability$media"][0].get("plmedia$publicUrl")
if not media:
raise ValueError("Could not find any streams - is the title still available?")
manifest, self.pid = self.get_manifest(media)
tracks = DASH.from_url(manifest, self.session).to_tracks(language=title.language)
for track in tracks.audio:
role = track.data["dash"]["adaptation_set"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Episode) -> Chapters:
if not title.data.get("rte$chapters"):
return Chapters()
timecodes = [x for x in title.data["rte$chapters"]]
chapters = [Chapter(timestamp=float(x)) for x in timecodes]
if title.data.get("rte$creditStart"):
chapters.append(Chapter(name="Credits", timestamp=float(title.data["rte$creditStart"])))
return chapters
def certificate(self, **_):
return None # will use common privacy cert
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
params = {
"token": self.token,
"account": self.account,
"form": "json",
"schema": "1.0",
}
payload = {
"getWidevineLicense": {
"releasePid": self.pid,
"widevineChallenge": base64.b64encode(challenge).decode("utf-8"),
}
}
r = self.session.post(url=self.license, params=params, json=payload)
if not r.ok:
raise ConnectionError(f"License request failed: {r.text}")
return r.json()["getWidevineLicenseResponse"]["license"]
# Service specific functions
def _movie(self, title: str) -> Movie:
params = {"count": "true", "entries": "true", "byId": title}
data = self._request("/mpx/1uC-gC/rte-prd-prd-all-programs", params=params)["entries"]
return [
Movie(
id_=movie["guid"],
service=self.__class__,
name=movie.get("plprogram$longTitle"),
year=movie.get("plprogram$year"),
language=movie["plprogram$languages"][0] if movie.get("plprogram$languages") else "eng",
data=movie,
)
for movie in data
]
def _show(self, title: str) -> Episode:
entry = self._request("/mpx/1uC-gC/rte-prd-prd-all-movies-series?byGuid={}".format(title))["entries"][0]["id"]
data = self._request("/mpx/1uC-gC/rte-prd-prd-all-programs?bySeriesId={}".format(entry.split("/")[-1]))["entries"]
return [
Episode(
id_=episode.get("guid"),
title=episode.get("plprogram$longTitle"),
season=episode.get("plprogram$tvSeasonNumber") or 0,
number=episode.get("plprogram$tvSeasonEpisodeNumber") or 0,
name=episode.get("description"),
language=episode["plprogram$languages"][0] if episode.get("plprogram$languages") else "eng",
service=self.__class__,
data=episode,
)
for episode in data
if episode["plprogram$programType"] == "episode"
]
def _episode(self, title: str, guid: str) -> Episode:
title = title.split("?")[0]
entry = self._request("/mpx/1uC-gC/rte-prd-prd-all-movies-series?byGuid={}".format(title))["entries"][0]["id"]
data = self._request("/mpx/1uC-gC/rte-prd-prd-all-programs?bySeriesId={}".format(entry.split("/")[-1]))["entries"]
return [
Episode(
id_=episode.get("guid"),
title=episode.get("plprogram$longTitle"),
season=episode.get("plprogram$tvSeasonNumber") or 0,
number=episode.get("plprogram$tvSeasonEpisodeNumber") or 0,
name=episode.get("description"),
language=episode["plprogram$languages"][0] if episode.get("plprogram$languages") else "eng",
service=self.__class__,
data=episode,
)
for episode in data
if episode["plprogram$programType"] == "episode" and episode.get("guid") == guid
]
def get_config(self):
token = self._request("/servicelayer/api/anonymouslogin")["mpx_token"]
account = self._request("/wordpress/wp-content/uploads/standard/web/config.json")["mpx_config"]["account_id"]
return token, account
def get_manifest(self, media_url: str) -> str:
try:
res = self._request(
media_url,
params={
"formats": "MPEG-DASH",
"auth": self.token,
"assetTypes": "default:isl",
"tracking": "true",
"format": "SMIL",
"iu": "/3014/RTE_Player_VOD/Android_Phone/NotRegistered",
"policy": "168602703",
},
)
root = load_xml(res)
video = root.xpath("//switch/video")
manifest = video[0].get("src")
elem = root.xpath("//switch/ref")
value = elem[0].find(".//param[@name='trackingData']").get("value")
pid = re.search(r"pid=([^|]+)", value).group(1)
return manifest, pid
except Exception as e:
raise ValueError(
f"Request for manifest failed: {e}.\n"
"Content may be geo-restricted to IE"
)
def _request(self, api: str, params: dict = None, headers: dict = None) -> Any[dict | str]:
url = urljoin(self.base_url, api)
self.session.headers.update(self.config["headers"])
if params:
self.session.params.update(params)
if headers:
self.session.headers.update(headers)
prep = self.session.prepare_request(Request("GET", url))
response = self.session.send(prep)
if response.status_code != 200:
raise ConnectionError(
f"Status: {response.status_code} - {response.url}\n"
"Content may be geo-restricted to IE"
)
try:
return json.loads(response.content)
except json.JSONDecodeError:
return response.text
@@ -1,7 +0,0 @@
headers:
user-agent: Dalvik/2.1.0 (Linux; U; Android 13; SM-A536E Build/RSR1.210722.013.A2)
endpoints:
base_url: https://www.rte.ie
feed: https://feed.entertainment.tv.theplatform.eu
license: https://widevine.entitlement.eu.theplatform.com/wv/web/ModularDrm
@@ -1,439 +0,0 @@
from __future__ import annotations
import re
from collections.abc import Generator
from concurrent.futures import ThreadPoolExecutor
from http.cookiejar import MozillaCookieJar
from typing import Any, List, Optional, Union
from uuid import uuid4
import click
from click import Context
from pyplayready.cdm import Cdm as PlayReadyCdm
from envied.core.credential import Credential
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks
class SEVEN(Service):
"""
Service code for 7Plus streaming service (https://7plus.com.au/).
\b
Version: 1.0.0
Author: stabbedbybrick
Authorization: Cookies
Geofence: AU (API and downloads)
Robustness:
Widevine:
L3: 720p
PlayReady:
SL2000: 720p
\b
Tips:
- Use complete title URL as input:
SERIES: https://7plus.com.au/ncis-los-angeles
EPISODE: https://7plus.com.au/ncis-los-angeles?episode-id=NCIL01-001
- There's no way to distinguish between series and movies, so use `--movie` to download as movie
\b
Examples:
- SERIES: unshackle dl -w s01e01 7plus https://7plus.com.au/ncis-los-angeles
- EPISODE: unshackle dl 7plus https://7plus.com.au/ncis-los-angeles?episode-id=NCIL01-001
- MOVIE: unshackle dl 7plus --movie https://7plus.com.au/puss-in-boots-the-last-wish
"""
GEOFENCE = ("au",)
ALIASES = ("7plus", "sevenplus",)
@staticmethod
@click.command(name="SEVEN", short_help="https://7plus.com.au/", help=__doc__)
@click.option("-m", "--movie", is_flag=True, default=False, help="Download as Movie")
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> SEVEN:
return SEVEN(ctx, **kwargs)
def __init__(self, ctx: Context, movie: bool, title: str):
self.title = title
self.movie = movie
super().__init__(ctx)
self.cdm = ctx.obj.cdm
self.drm_system = "playready" if isinstance(self.cdm, PlayReadyCdm) else "widevine"
self.key_system = "com.microsoft.playready" if isinstance(self.cdm, PlayReadyCdm) else "com.widevine.alpha"
self.profile = ctx.parent.params.get("profile")
if not self.profile:
self.profile = "default"
self.session.headers.update(self.config["headers"])
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if cookies is None:
raise EnvironmentError("Service requires Cookies for Authentication.")
self.session.cookies.update(cookies)
api_key = next((cookie.name.replace("gig_bootstrap_", "") for cookie in cookies if "login_ver" in cookie.value), None)
login_token = next((cookie.value for cookie in cookies if "glt_" in cookie.name), None)
if not api_key or not login_token:
raise ValueError("Invalid cookies. Try refreshing.")
market = self.session.get(
"https://market-cdn.swm.digital/v1/market/ip/",
params={"apikey": "web"}
).json()
self.market_id = market.get("_id", 4)
cache = self.cache.get(f"tokens_{self.profile}")
if cache and not cache.expired:
# cached
self.log.info(" + Using cached tokens...")
tokens = cache.data
elif cache and cache.expired:
# expired, refresh
self.log.info("+ Refreshing tokens...")
payload = {
"platformId": self.config["PLATFORM_ID"],
"regSource": "7plus",
"refreshToken": cache.data.get("refresh_token"),
}
r = self.session.post("https://auth2.swm.digital/connect/token", data=payload)
if r.status_code != 200:
raise ConnectionError(f"Failed to refresh tokens: {r.text}")
tokens = r.json()
cache.set(tokens, expiration=int(tokens["expires_in"]) - 60)
else:
# new
self.log.info(" + Authenticating...")
device_id = str(uuid4())
payload = {
"platformId": self.config["PLATFORM_ID"],
"regSource": "7plus",
"deviceId": device_id,
"locationVerificationRequired": "false",
}
r = self.session.post("https://auth2.swm.digital/account/device/authorize", data=payload)
if r.status_code != 200:
raise ConnectionError(f"Failed to authenticate: {r.text}")
auth = r.json()
uri = auth.get("verification_uri_complete")
user_code = auth.get("user_code")
device_code = auth.get("device_code")
if not uri or not user_code or not device_code:
raise ValueError(f"Failed to authenticate device: {auth}")
data = {
"APIKey": api_key,
"sdk": "js_next",
"login_token": login_token,
"authMode": "cookie",
"pageURL": "https://7plus.com.au/connect",
"sdkBuild": "18051",
"format": "json",
}
response = self.session.post("https://login.7plus.com.au/accounts.getJWT", cookies=cookies, data=data)
if response.status_code != 200:
raise ConnectionError(f"Failed to fetch JWT: {response.text}")
id_token = response.json().get("id_token")
if not id_token:
raise ValueError(f"Failed to fetch JWT: {response.text}")
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "en-US,en;q=0.9",
"authorization": f"Bearer {id_token}",
"content-type": "application/json;charset=UTF-8",
"origin": "https://7plus.com.au",
"referer": "https://7plus.com.au/connect",
}
payload = {
"platformId": "web",
"regSource": "7plus",
"code": user_code,
"attemptLocationPairing": False,
}
r = self.session.post("https://7plus.com.au/auth/otp", headers=headers, json=payload)
if r.status_code != 200:
raise ConnectionError(f"Failed to verify OTP: {r.status_code}")
payload = {
"platformId": self.config["PLATFORM_ID"],
"regSource": "7plus",
"deviceCode": device_code,
}
r = self.session.post("https://auth2.swm.digital/connect/token", data=payload)
if r.status_code != 200:
raise ConnectionError(f"Failed to fetch device token: {r.text}")
tokens = r.json()
tokens["device_id"] = device_id
cache.set(tokens, expiration=int(tokens["expires_in"]) - 60)
self.device_id = tokens.get("device_id") or str(uuid4())
self.session.headers.update({"authorization": f"Bearer {tokens['access_token']}"})
def search(self) -> Generator[SearchResult, None, None]:
params = {
"searchTerm": self.title,
"market-id": self.market_id,
"api-version": "4.4",
"platform-id": self.config["PLATFORM_ID"],
"platform-version": self.config["PLATFORM_VERSION"],
}
r = self.session.get("https://searchapi.swm.digital/3.0/api/Search", params=params)
r.raise_for_status()
results = r.json()
if isinstance(results, list):
for result in results:
title = result.get("image", {}).get("altTag")
slug = result.get("contentLink", {}).get("url")
yield SearchResult(
id_=f"https://7plus.com.au{slug}",
title=title,
url=f"https://7plus.com.au{slug}",
)
def get_titles(self) -> Movies | Series:
if match := re.match(r"https:\/\/7plus\.com\.au\/([^?\/]+)(?:\?.*episode-id=([^&]+))?", self.title):
slug, episode_id = match.groups()
else:
raise ValueError(f"Invalid title: {self.title}")
params = {
"platform-id": self.config["PLATFORM_ID"],
"market-id": self.market_id,
"platform-version": self.config["PLATFORM_VERSION"],
"api-version": self.config["API_VERSION"],
}
r = self.session.get(f"https://component-cdn.swm.digital/content/{slug}", params=params)
if r.status_code != 200:
raise ConnectionError(f"Failed to fetch content: {r.text}")
content = r.json()
if episode_id:
episodes = self._series(content, slug)
episode = next((e for e in episodes if e.id == episode_id), None)
return Series([episode])
elif self.movie:
movie = self._movie(content)
return Movies([movie])
else:
episodes = self._series(content, slug)
return Series(episodes)
def get_tracks(self, title: Movie | Episode) -> Tracks:
params = {
"appId": "7plus",
"deviceType": self.config["PLATFORM_ID"],
"platformType": "tv",
"deviceId": self.device_id,
"pc": 3181,
"advertid": "null",
"accountId": "5303576322001",
"referenceId": f"ref:{title.id}",
"deliveryId": "csai",
"marketId": self.market_id,
"ozid": "dc6095c7-e895-41d3-6609-79f673fc7f63",
"sdkverification": "true",
"cp.encryptionType": "cenc",
"cp.drmSystems": self.drm_system,
"cp.containerFormat": "cmaf",
"cp.supportedCodecs": "avc",
"cp.drmAuth": "true",
}
resp = self.session.get("https://videoservice.swm.digital/playback", params=params)
if resp.status_code != 200:
raise ConnectionError(f"Failed to fetch playback data: {resp.text}")
data = resp.json()
drm = data.get("media", {}).get("stream_type_drm", False)
if drm:
source_manifest = next((
x["src"] for x in data["media"]["sources"]
if x.get("key_systems").get("com.widevine.alpha")),
None,
)
title.data["license_url"] = next((
x["key_systems"][self.key_system]["license_url"]
for x in data["media"]["sources"]
if x.get("key_systems").get(self.key_system)),
None,
)
else:
source_manifest = next((
x["src"] for x in data["media"]["sources"]
if x.get("type") == "application/dash+xml"),
None,
)
if not source_manifest:
raise ValueError("Failed to get manifest")
title.data["cue_points"] = data.get("media", {}).get("cue_points")
tracks = DASH.from_url(source_manifest, self.session).to_tracks(title.language)
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Movie | Episode) -> Chapters:
if not (cue_points := title.data.get("cue_points")):
return Chapters()
cue_points = sorted(cue_points, key=lambda x: x["time"])
chapters = []
for cue_point in cue_points:
if cue_point.get("time", 0) > 0:
name = "End Credits" if cue_point.get("name", "").lower() == "credits" else None
chapters.append(Chapter(name=name, timestamp=cue_point["time"] * 1000))
return Chapters(chapters)
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, *, challenge: bytes, title: Episode | Movie, track: Any) -> Optional[Union[bytes, str]]:
if license_url := title.data.get("license_url"):
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
return None
def get_playready_license(self, *, challenge: bytes, title: Episode | Movie, track: Any) -> Optional[Union[bytes, str]]:
if license_url := title.data.get("license_url"):
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
return None
# Service specific functions
def _movie(self, content: dict) -> Movie:
title = content.get("title")
metadata = content.get("items", [{}])[0].get("videoMetadata", {})
if not metadata:
raise ValueError("Failed to find metadata for this movie")
return Movie(
id_=metadata.get("videoBref"),
service=self.__class__,
name=title,
year=metadata.get("productionYear"),
language="en",
data=content,
)
def _get_season_data(self, season_id: str, slug: str) -> List[Episode]:
params = {
"component-id": season_id,
"platform-id": self.config.get("PLATFORM_ID"),
"market-id": self.market_id,
"platform-version": self.config.get("PLATFORM_VERSION"),
"api-version": self.config.get("API_VERSION"),
"signedUp": "True",
}
try:
r = self.session.get(f"https://component.swm.digital/component/{slug}", params=params)
r.raise_for_status()
comp = r.json()
except ConnectionError as e:
self.log.error(f"Error fetching season {season_id}: {e}")
return []
except Exception as e:
self.log.error(f"An unexpected error occurred for season {season_id}: {e}")
return []
episodes = []
for episode in comp.get("items", []):
info_panel = episode.get("infoPanelData", {})
player_data = episode.get("playerData", {})
card_data = episode.get("cardData", {})
catalogue_number = episode.get("catalogueNumber", "")
title = info_panel.get("title")
episode_name = card_data.get("image", {}).get("altTag")
card_name = card_data.get("title", "").lstrip("0123456789. ").split(" - ")[-1].strip()
season, number, name = 0, 0, card_name
if match := re.search(r"(?:Season|Year)\s*(\d+)\s*E(?:pisode)?\s*(\d+)", episode_name, re.IGNORECASE):
season = int(match.group(1))
number = int(match.group(2))
if not season and not number:
if match := re.compile(r"\w+(\d+)-(\d+)").search(catalogue_number):
season = int(match.group(1))
number = int(match.group(2))
episodes.append(
Episode(
id_=player_data.get("episodePlayerId"),
service=self.__class__,
title=title,
year=card_data.get("productionYear"),
season=season,
number=number,
name=name,
language="en",
data=episode,
)
)
return episodes
def _series(self, content: dict, slug: str) -> List[Episode]:
items = next((x for x in content.get("items", []) if x.get("type") == "shelfContainer"), {})
episodes_shelf = next((x for x in items.get("items", []) if x.get("title") == "Episodes"), {})
seasons_container = next((x for x in episodes_shelf.get("items", []) if x.get("title") in ("Season", "Year")), {})
season_ids = [
item.get("items", [{}])[0].get("id")
for item in seasons_container.get("items", [])
if item.get("items") and item.get("items")[0].get("id")
]
if not season_ids:
return []
all_episodes = []
with ThreadPoolExecutor(max_workers=len(season_ids)) as executor:
future_to_season = {
executor.submit(self._get_season_data, season_id, slug): season_id for season_id in season_ids
}
for future in future_to_season:
try:
episodes_of_season = future.result()
all_episodes.extend(episodes_of_season)
except Exception as exc:
season_id = future_to_season[future]
self.log.error(f"{season_id} generated an exception: {exc}")
return all_episodes
@@ -1,7 +0,0 @@
headers:
user-agent: "7plus/5.25.1 (Linux;Android 8.1.0) ExoPlayerLib/2.11.7"
x-swm-apikey: "kGcrNnuPClrkynfnKwG8IA/NhVG6ut5nPEdWF2jscvE="
PLATFORM_ID: "androidtv"
PLATFORM_VERSION: "5.25.0.0"
API_VERSION: "5.9.0.0"
@@ -1,257 +0,0 @@
import base64
import re
from http.cookiejar import CookieJar
from typing import Optional, Union
import click
import requests
from envied.core.service import Service
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.titles import Song, Album, Title_T
from envied.core.tracks import Audio, Tracks
from envied.core.drm import DRM_T, Widevine
from envied.utils import base62
from pywidevine.pssh import PSSH
class SPOT(Service):
"""
Service code for Spotify
Written by ToonsHub
Reference: https://github.com/glomatico/spotify-aac-downloader
Authorization: Cookies (Free - 128kbps and Premium - 256kbps)
Security: AAC@L3
"""
# Static method, this method belongs to the class
@staticmethod
# The command name, must much the service tag (and by extension the service folder)
@click.command(name="SPOT", short_help="https://open.spotify.com", help=__doc__)
# Using track/playlist/album/artist page URL
@click.argument("title", type=str)
# Pass the context back to the CLI with arguments
@click.pass_context
def cli(ctx, **kwargs):
return SPOT(ctx, **kwargs)
# Accept the CLI arguments by overriding the constructor (The __init__() method)
def __init__(self, ctx, title):
# Pass the title argument to self so it's accessable across all methods
self.title = title
self.is_premium = False
super().__init__(ctx)
# Defining an authinticate function
def authenticate(self, cookies: Optional[CookieJar], credential: Optional[Credential] = None):
super().authenticate(cookies, credential)
# Check for cookies
if not cookies:
raise Exception("Cookies are required for performing this action.")
# Authenticate using Cookies
self.session.headers.update(
{
'accept': 'application/json',
'accept-language': 'en',
"app-platform": "WebPlayer",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
}
)
self.session.cookies.update(cookies)
home_page = self.session.get("https://open.spotify.com/").content
token = re.search(r'accessToken":"(.*?)"', home_page).group(1)
self.is_premium = re.search(r'isPremium":(.*?),', home_page).group(1) == 'true'
self.session.headers.update(
{
"authorization": f"Bearer {token}",
}
)
# Function to determine the type of collection
def getCollectionTypeAndId(self):
_type = self.title.split("open.spotify.com/")[1].split("/")[0]
_id = self.title.split(_type + "/")[1].split("?")[0]
return _type, _id
# Defining a function to return titles
def get_titles(self):
songs = []
_type, _id = self.getCollectionTypeAndId()
if _type == 'album':
album = self.session.get(self.config['endpoints']['albums'].format(id=_id)).json()
album_next_url = album["tracks"]["next"]
while album_next_url is not None:
album_next = self.session.get(album_next_url).json()
album["tracks"]["items"].extend(album_next["items"])
album_next_url = album_next["next"]
# Get the episode metadata by iterating through each season id
for song in album["tracks"]["items"]:
# Set a class for each song
song_class = Song(
id_=song["id"],
name=song["name"],
artist=", ".join([ artist["name"] for artist in song["artists"] ]),
album=album["name"],
track=song["track_number"],
disc=song["disc_number"],
year=int(album["release_date"][:4].strip()),
service=self.__class__
)
# Append it to the list
songs.append(song_class)
elif _type == "playlist":
playlist = self.session.get(
self.config['endpoints']['playlists'].format(id=_id)
).json()
playlist_next_url = playlist["tracks"]["next"]
while playlist_next_url is not None:
playlist_next = self.session.get(playlist_next_url).json()
playlist["tracks"]["items"].extend(playlist_next["items"])
playlist_next_url = playlist_next["next"]
# Get the episode metadata by iterating through each season id
for song in playlist["tracks"]["items"]:
song = song["track"]
# Set a class for each song
song_class = Song(
id_=song["id"],
name=song["name"],
artist=", ".join([ artist["name"] for artist in song["artists"] ]),
album=song["album"]["name"],
track=song["track_number"],
disc=song["disc_number"],
year=int(song["album"]["release_date"][:4].strip()),
service=self.__class__
)
# Append it to the list
songs.append(song_class)
elif _type == "artist":
playlist = self.session.get(
self.config['endpoints']['artists'].format(id=_id)
).json()
# Get the episode metadata by iterating through each season id
for song in playlist["tracks"]:
# Set a class for each song
song_class = Song(
id_=song["id"],
name=song["name"],
artist=", ".join([ artist["name"] for artist in song["artists"] ]),
album=song["album"]["name"],
track=song["track_number"],
disc=song["disc_number"],
year=int(song["album"]["release_date"][:4].strip()),
service=self.__class__
)
# Append it to the list
songs.append(song_class)
elif _type == "track":
song = self.session.get(
self.config['endpoints']['tracks'].format(id=_id)
).json()
# Set a class for each song
song_class = Song(
id_=song["id"],
name=song["name"],
artist=", ".join([ artist["name"] for artist in song["artists"] ]),
album=song["album"]["name"],
track=song["track_number"],
disc=song["disc_number"],
year=int(song["album"]["release_date"][:4].strip()),
service=self.__class__
)
# Append it to the list
songs.append(song_class)
return Album(songs)
# Get DRM
def get_spotify_drm(self) -> DRM_T:
pssh = requests.get(
self.config['endpoints']['pssh'].format(file_id=self.file_id)
).json()["pssh"]
return Widevine(
pssh=PSSH(pssh)
)
# Defining a function to get tracks
def get_tracks(self, title: Title_T) -> Tracks:
self.audio_quality = "MP4_256_DUAL" if self.is_premium else "MP4_128_DUAL"
# Get FileID
gid = hex(base62.decode(title.id, base62.CHARSET_INVERTED))[2:].zfill(32)
metadata = self.session.get(
self.config['endpoints']['metadata'].format(gid=gid)
).json()
audio_files = metadata.get("file")
if audio_files is None:
if metadata.get("alternative") is not None:
audio_files = metadata["alternative"][0]["file"]
else:
return None
self.file_id = next(
i["file_id"] for i in audio_files if i["format"] == self.audio_quality
)
# Get stream URL
stream_url = self.session.get(
self.config['endpoints']['stream'].format(file_id=self.file_id)
).json()["cdnurl"][0] # Can change index to get different server
# Get & Set DRM
drm = [self.get_spotify_drm()]
# Set the tracks
tracks = Tracks()
tracks.add(Audio(
url=stream_url,
drm=drm,
codec=Audio.Codec.AAC,
language=metadata.get("language_of_performance", ["en"])[0],
bitrate=256000 if self.is_premium else 128000,
channels=2
))
# Return the tracks
return tracks
# Defining a function to get chapters
def get_chapters(self, title):
return []
# Defining a function to get widevine license keys
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
# Send the post request to the license server
license_raw = self.session.post(
self.config['endpoints']['license'],
data=challenge
)
# Return the license
return base64.b64encode(license_raw.content).decode()
@@ -1,9 +0,0 @@
endpoints:
albums: https://api.spotify.com/v1/albums/{id}
playlists: https://api.spotify.com/v1/playlists/{id}
artists: https://api.spotify.com/v1/artists/{id}/top-tracks
tracks: https://api.spotify.com/v1/tracks/{id}
pssh: https://seektables.scdn.co/seektable/{file_id}.json
metadata: https://spclient.wg.spotify.com/metadata/4/track/{gid}?market=from_token
stream: https://gue1-spclient.spotify.com/storage-resolve/v2/files/audio/interactive/11/{file_id}?version=10000000&product=9&platform=39&alt=json
license: https://gae2-spclient.spotify.com/widevine-license/v1/audio/license
@@ -1,232 +0,0 @@
from __future__ import annotations
import re
from collections.abc import Generator
from datetime import timedelta
from typing import Any, Union
from urllib.parse import urlparse
import click
from click import Context
from lxml import etree
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks
class STV(Service):
"""
Service code for STV Player streaming service (https://player.stv.tv/).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: None
Robustness:
L3: 1080p
\b
Tips:
- Use complete title URL as input:
SERIES: https://player.stv.tv/summary/rebus
EPISODE: https://player.stv.tv/episode/2ro8/rebus
- Use the episode URL for movies:
MOVIE: https://player.stv.tv/episode/4lw7/wonder-woman-1984
"""
GEOFENCE = ("gb",)
ALIASES = ("stvplayer",)
@staticmethod
@click.command(name="STV", short_help="https://player.stv.tv/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> STV:
return STV(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.session.headers.update({"user-agent": "okhttp/4.11.0"})
self.base = self.config["endpoints"]["base"]
def search(self) -> Generator[SearchResult, None, None]:
data = {
"engine_key": "S1jgssBHdk8ZtMWngK_y",
"q": self.title,
}
r = self.session.post(self.config["endpoints"]["search"], data=data)
r.raise_for_status()
results = r.json()["records"]["page"]
for result in results:
label = result.get("category")
if label and isinstance(label, list):
label = result["category"][0]
yield SearchResult(
id_=result.get("url"),
title=result.get("title"),
description=result.get("body"),
label=label,
url=result.get("url"),
)
def get_titles(self) -> Union[Movies, Series]:
kind, slug = self.parse_title(self.title)
self.session.headers.update({"stv-drm": "true"})
if kind == "episode":
r = self.session.get(self.base + f"episodes/{slug}")
r.raise_for_status()
episode = r.json()["results"]
if episode.get("genre").lower() == "movie":
return Movies(
[
Movie(
id_=episode["video"].get("id"),
service=self.__class__,
year=None,
name=episode.get("title"),
language="en",
data=episode,
)
]
)
episodes = [
Episode(
id_=episode["video"].get("id"),
service=self.__class__,
title=episode["programme"].get("name"),
season=int(episode["playerSeries"]["name"].split(" ")[1])
if episode.get("playerSeries") and re.match(r"Series \d+", episode["playerSeries"]["name"])
else 0,
number=int(episode.get("number", 0)),
name=episode.get("title", "").lstrip("0123456789. ").lstrip(),
language="en",
data=episode,
)
]
elif kind == "summary":
r = self.session.get(self.base + f"programmes/{slug}")
r.raise_for_status()
data = r.json()
series = [series.get("guid") for series in data["results"]["series"]]
seasons = [self.session.get(self.base + f"episodes?series.guid={i}").json() for i in series]
episodes = [
Episode(
id_=episode["video"].get("id"),
service=self.__class__,
title=data["results"].get("name"),
season=int(episode["playerSeries"]["name"].split(" ")[1])
if episode.get("playerSeries")
and re.match(r"Series \d+", episode["playerSeries"]["name"])
else 0,
number=int(episode.get("number", 0)),
name=episode.get("title", "").lstrip("0123456789. ").lstrip(),
language="en",
data=episode,
)
for season in seasons
for episode in season["results"]
]
self.session.headers.pop("stv-drm")
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
self.drm = title.data["programme"].get("drmEnabled")
headers = self.config["headers"]["drm"] if self.drm else self.config["headers"]["clear"]
accounts = self.config["accounts"]["drm"] if self.drm else self.config["accounts"]["clear"]
r = self.session.get(
self.config["endpoints"]["playback"].format(accounts=accounts, id=title.id),
headers=headers,
)
if not r.ok:
raise ConnectionError(r.text)
data = r.json()
source_manifest = next(
(source["src"] for source in data["sources"] if source.get("type") == "application/dash+xml"),
None,
)
self.license = None
if self.drm:
key_systems = next((
source
for source in data["sources"]
if source.get("type") == "application/dash+xml"
and source.get("key_systems").get("com.widevine.alpha")),
None,
)
self.license = key_systems["key_systems"]["com.widevine.alpha"]["license_url"] if key_systems else None
manifest = self.trim_duration(source_manifest)
tracks = DASH.from_text(manifest, source_manifest).to_tracks(title.language)
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
cue_points = title.data.get("_cuePoints")
if not cue_points:
return Chapters()
return Chapters([Chapter(timestamp=int(cue)) for cue in cue_points])
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
if not self.license:
return None
r = self.session.post(url=self.license, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
# Service specific functions
@staticmethod
def parse_title(title: str) -> tuple[str, str]:
parsed_url = urlparse(title).path.split("/")
kind, slug = parsed_url[1], parsed_url[2]
if kind not in ["episode", "summary"]:
raise ValueError("Failed to parse title - is the URL correct?")
return kind, slug
@staticmethod
def trim_duration(source_manifest: str) -> str:
"""
The last segment on all tracks return a 404 for some reason, causing a failed download.
So we trim the duration by exactly one segment to account for that.
TODO: Calculate the segment duration instead of assuming length.
"""
manifest = DASH.from_url(source_manifest).manifest
period_duration = manifest.get("mediaPresentationDuration")
period_duration = DASH.pt_to_sec(period_duration)
hours, minutes, seconds = str(timedelta(seconds=period_duration - 6)).split(":")
new_duration = f"PT{hours}H{minutes}M{seconds}S"
manifest.set("mediaPresentationDuration", new_duration)
return etree.tostring(manifest, encoding="unicode")
@@ -1,20 +0,0 @@
accounts:
drm: "6204867266001"
clear: "1486976045"
headers:
drm:
BCOV-POLICY: BCpkADawqM32Q7lZg8ME0ydIOV8bD_9Ke2YD5wvY_T2Rq2TBtz6QQfpHtSAJTiDL-MiYAxyJVvScaKt82d1Q6b_wP6MG-O8SGQjRnwczfdsTesTZy-uj23uKv1vjHijtTeQC0DONN53zS38v
User-Agent: Dalvik/2.1.0 (Linux; U; Android 12; SM-A226B Build/SP1A.210812.016)
Host: edge.api.brightcove.com
Connection: keep-alive
clear:
BCOV-POLICY: BCpkADawqM2Dpx-ht5hP1rQqWFTcOTqTT5x5bSUlY8FaOO1_P8LcKxmL2wrFzTvRb3HzO2YTIzVDuoeLfqvFvp1dWRPnxKT8zt9ErkENYteaU9T6lz7OogjL8W8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 12; SM-A226B Build/SP1A.210812.016)
Host: edge.api.brightcove.com
Connection: keep-alive
endpoints:
base: https://player.api.stv.tv/v1/
playback: https://edge.api.brightcove.com/playback/v1/accounts/{accounts}/videos/{id}
search: https://api.swiftype.com/api/v1/public/engines/search.json
@@ -1,565 +0,0 @@
from __future__ import annotations
import base64
import concurrent.futures
import hashlib
import hmac
import json
import re
import sys
import time
import uuid
from collections.abc import Generator
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
import click
import m3u8
from click import Context
from langcodes import Language
from requests import Request
from envied.core.config import config
from envied.core.credential import Credential
from envied.core.downloaders import requests
from envied.core.manifests import HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Subtitle, Tracks, Video
class TEN(Service):
"""
\b
Service code for 10Play streaming service (https://10.com.au/).
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: credentials
Geofence: AU (API and downloads)
Robustness:
AES: 1080p, AAC2.0
\b
Tips:
- Input should be complete URL:
SHOW: https://10.com.au/australian-survivor
EPISODE: https://10.com.au/australian-survivor/episodes/season-11-australia-v-the-world/episode-9/tpv250831fxatm
MOVIE: https://10.com.au/a-quiet-place
- Non-standard programmes (e.g. game shows/sports) have very inconsistent episode number labels. It's recommended to use episode URLs for those.
\b
Notes:
- 10Play uses transport streams for HLS, meaning the video and audio are a part of the same stream.
As a result, only videos are listed as tracks. But the audio will be included as well.
- Since 1080p streams require some manipulation of the manifest, n_m3u8dl_re downloader is required.
"""
GEOFENCE = ("au",)
ALIASES = (
"10play",
"tenplay",
)
@staticmethod
@click.command(name="TEN", short_help="https://10.com.au/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> TEN:
return TEN(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
if config.downloader != "n_m3u8dl_re":
self.log.error(" - Error: n_m3u8dl_re downloader is required for this service.")
sys.exit(1)
def search(self) -> Generator[SearchResult, None, None]:
query = self.endpoints["searchApiEndpoint"] + self.title
results = self._request("GET", query)
for result in results:
clean_title = self._sanitize(result.get("title"))
yield SearchResult(
id_=f"https://10.com.au/{clean_title}",
title=result.get("title"),
description=result.get("abstractShowDescription"),
label=result.get("subtitle", "").split("|")[-1].strip(),
url=f"https://10.com.au/{clean_title}",
)
def authenticate(
self,
cookies: Optional[MozillaCookieJar] = None,
credential: Optional[Credential] = None,
) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
self.session.headers.update(self.config["headers"])
self.endpoints = self._request(
"GET", self.config["endpoints"]["config"], params={"SystemName": "tvos"}
)
cache = self.cache.get(f"tokens_{credential.sha1}")
if cache and not cache.expired:
self.log.info(" + Using cached Tokens...")
tokens = cache.data
elif cache and cache.expired:
self.log.info(" + Refreshing expired Tokens...")
payload = {
"alternativeToken": cache.data["alternativeToken"],
"refreshToken": cache.data["refreshToken"],
}
tokens = self._request(
"POST", self.endpoints["authConfig"]["refreshToken"], json=payload
)
cache.set(tokens, expiration=tokens["expiresIn"])
else:
self.log.info(" + Logging in...")
headers = {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"origin": "https://10.com.au",
"referer": "https://10.com.au/",
}
login = self._request(
"POST",
self.config["endpoints"]["auth"],
headers=headers,
json={"email": credential.username, "password": credential.password},
)
access_token = login.get("jwt", {}).get("accessToken")
if not access_token:
raise ValueError(
"Failed to authenticate with credentials: " + login.text
)
identifier = str(uuid.uuid4())
payload = {
"deviceIdentifier": identifier,
"machine": "Hisense",
"system": "vidaa",
"systemVersion": "U6",
"platform": "vidaa",
"appVersion": "v1",
"ipAddress": "string",
}
device = self._request(
"POST", self.endpoints["authConfig"]["generateCode"], json=payload
)
code = device.get("code")
expiry = device.get("expiry")
if not code or not expiry:
raise ValueError("Failed to generate device code: " + device.text)
headers = {
"accept": "application/json, text/plain, */*",
"authorization": f"Bearer {access_token}",
"content-type": "application/json",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"origin": "https://10.com.au",
"referer": "https://10.com.au/activate",
}
activate = self._request(
"POST",
self.endpoints["activateApiEndpoint"],
headers=headers,
json={"code": code},
)
if not activate:
raise ValueError("Failed to activate device")
payload = {
"code": code,
"deviceIdentifier": identifier,
"expiry": expiry,
}
auth = self._request(
"POST",
self.endpoints["authConfig"]["validateCode"],
json={
"code": code,
"deviceIdentifier": identifier,
"expiry": expiry,
},
)
tokens = auth.get("jwt")
tokens["identifier"] = identifier
self.log.info(" + User successfully logged in, TV device activated")
cache.set(tokens, expiration=tokens.get("expiresIn"))
self.access_token = tokens.get("alternativeToken")
self.session.headers.update({"authorization": f"Bearer {self.access_token}"})
def get_titles(self) -> Union[Movies, Series]:
url_pattern = re.compile(
r"^https://10\.com\.au/(?:[a-z0-9-]+)"
r"(?:/episodes/(?:season-)?(?P<season>[a-z0-9-]+)/(?:episode-)?(?P<episode>[a-z0-9-]+)/(?P<id>[a-z0-9]+))?$"
)
match = url_pattern.match(self.title)
if not match:
raise ValueError(f"Could not parse ID from title: {self.title}")
matches = match.groupdict()
if not matches.get("id"):
show_id = self._get_html(self.title)
content = self._shows(show_id)
if "movie" in content.get("subtitle", "").lower():
movies = self._movie(content)
return Movies(movies)
else:
episodes = self._series(content)
return Series(episodes)
else:
episodes = self._episode(matches.get("id"))
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
playback_url = title.data.get("playbackApiEndpoint")
if not playback_url:
raise ValueError("Could not find playback URL for this title")
params = {
"device": "Tv",
"platform": "vidaa",
"appVersion": "v1",
}
r = self.session.get(playback_url, params=params)
if not r.ok:
raise ValueError("Failed to get playback data: " + r.text)
dai_auth = r.headers.get("X-DAI-AUTH")
video_id = r.headers.get("x-dai-video-id")
if dai_auth is not None:
payload = {"auth-token": dai_auth}
playback_data = r.json()
video_id = playback_data.get("dai", {}).get("videoId")
source_id = playback_data.get("dai", {}).get("contentSourceId", "2690006")
if not video_id or not source_id:
raise ValueError("Failed to get video ID: " + r.text)
dai_stream = f"https://dai.google.com/ondemand/v1/hls/content/{source_id}/vid/{video_id}/stream"
stream_data = self._request("POST", dai_stream, data=payload)
title.data["chapters"] = stream_data.get("time_events_url")
# program_language = Language.find(stream_data["customFields"].get("program_language", "en"))
manifest_url = stream_data.get("stream_manifest")
tracks = HLS.from_url(manifest_url, self.session).to_tracks(language="en")
tracks = self._add_tracks(tracks)
for track in tracks:
track.OnSegmentFilter = lambda x: re.search(r"redirector.googlevideo.com", x.uri)
track.downloader_args = {"--ad-keyword": "redirector.googlevideo.com"}
if isinstance(track, Subtitle):
track.downloader = requests
# if caption := stream_data.get("subtitles", [])[0].get("webvtt"):
# tracks.add(
# Subtitle(
# id_=hashlib.md5(caption.encode()).hexdigest()[0:6],
# url=caption,
# codec=Subtitle.Codec.from_mime(caption[-3:]),
# language=stream_data.get("subtitles", [])[0].get("language", "en"),
# )
# )
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
if not title.data.get("chapters"):
return Chapters()
events = self._request("GET", title.data["chapters"])
cue_points = events.get("cuepoints")
if not cue_points:
return Chapters()
chapters = []
for cue in cue_points:
chapters.append(Chapter(timestamp=float(cue["start_float"])))
chapters.append(Chapter(timestamp=float(cue["end_float"])))
return Chapters(chapters)
# Service specific
def _head_request(self, url: str) -> int:
try:
return self.session.head(url, timeout=10).status_code
except Exception:
return 0
def _check_and_add_track(
self, best_track: Video, quality_info: dict, source_bitrate: int
) -> Video | None:
playlist_uri = best_track.data["hls"]["playlist"].uri
playlist_text = self.session.get(playlist_uri).text
string_to_replace = f"-{source_bitrate}"
replacement_string = f"-{quality_info['bitrate']}"
lines = []
for line in playlist_text.splitlines():
if "redirector.googlevideo.com" in line:
continue
if string_to_replace in line:
line = line.replace(string_to_replace, replacement_string)
lines.append(line)
modified_playlist_text = "\n".join(lines)
playlist_obj = m3u8.loads(modified_playlist_text)
if not playlist_obj.segments:
return None
first_segment = playlist_obj.segments[0].uri
if self._head_request(first_segment) == 200:
playlist_file = config.directories.cache / "TEN" / f"playlist_{quality_info['quality']}.m3u8"
playlist_obj.dump(playlist_file)
video = Video(
id_=f"{best_track.id}-{quality_info['quality']}",
url=best_track.url,
height=quality_info["height"],
width=quality_info["width"],
bitrate=quality_info["bitrate"],
language=best_track.language,
codec=best_track.codec,
range_=best_track.range,
fps=best_track.fps,
descriptor=best_track.descriptor,
data=best_track.data.copy(),
from_file=playlist_file,
)
return video
return None
def _add_tracks(self, tracks: Tracks) -> Tracks:
if not tracks.videos:
return tracks
best_track = max(tracks.videos, key=lambda t: t.height or 0)
source_bitrate = {
1080: "5000000",
720: "3000000",
540: "1500000",
360: "750000",
}.get(best_track.height)
all_qualities = [
{"quality": "540p", "bitrate": 1500000, "height": 540, "width": 960},
{"quality": "720p", "bitrate": 3000000, "height": 720, "width": 1280},
{"quality": "1080p", "bitrate": 5000000, "height": 1080, "width": 1920},
]
qualities_to_check = [
q for q in all_qualities if q["height"] > best_track.height
]
if not qualities_to_check:
return tracks
with ThreadPoolExecutor(max_workers=len(qualities_to_check)) as executor:
future_to_track = {
executor.submit(self._check_and_add_track, best_track, quality, source_bitrate): quality
for quality in qualities_to_check
}
for future in concurrent.futures.as_completed(future_to_track):
new_track = future.result()
if new_track:
tracks.add(new_track)
return tracks
def _shows(self, show_id: str) -> dict:
show = self._request("GET", f'{self.endpoints["showsApiEndpoint"]}/{show_id}')
return show[0] if isinstance(show, list) else show
def _fetch_episode(self, url: str) -> list:
return self._request("GET", url)
def _series(self, content: dict) -> Episode:
season_list = content.get("seasons")
if not season_list:
raise ValueError("Could not find a season list for this title")
seasons = [
season.get("menuItems", [])[0].get("apiEndpoint")
for season in season_list
if season.get("menuItems", [])
and season.get("menuItems", [])[0].get("menuTitle", "").lower()
== "episodes"
]
if not seasons:
raise ValueError("Could not find a season list for this title")
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(self._fetch_episode, seasons))
titles = []
for result in results:
for episode in result:
ep_number = episode.get("episode")
sea_number = episode.get("season")
titles.append(
Episode(
id_=episode.get("id"),
service=self.__class__,
name=episode.get("vodTitle", "").split(" - ")[-1],
season=int(sea_number) if sea_number and sea_number.isdigit() else 0,
number=int(ep_number) if ep_number and ep_number.isdigit() else 0,
title=episode.get("tvShow"),
data=episode,
)
)
return titles
def _movie(self, data: dict) -> Movie:
endpoint = next(
(
season.get("menuItems", [])[0].get("apiEndpoint")
for season in data.get("seasons", [])
if season.get("menuItems", [])
),
None,
)
if not endpoint:
raise ValueError("Could not find an endpoint for this title")
movie = self._request("GET", endpoint)[0]
return [
Movie(
id_=movie.get("id"),
service=self.__class__,
name=movie.get("title"),
year=movie.get("season"),
data=movie,
)
]
def _episode(self, video_id: str) -> Episode:
data = self._request("GET", f"{self.endpoints['videosApiEndpoint']}/{video_id}")
ep_number = data.get("episode")
sea_number = data.get("season")
return [
Episode(
id_=data.get("id"),
service=self.__class__,
name=data.get("vodTitle", "").split(" - ")[-1],
season=int(sea_number) if sea_number and sea_number.isdigit() else 0,
number=int(ep_number) if ep_number and ep_number.isdigit() else 0,
title=data.get("tvShow"),
data=data,
)
]
def _get_html(self, url: str) -> Optional[str]:
page = self.session.get(url).text
pattern = re.compile(r"const showPageData = ({.*?});", re.DOTALL)
match = pattern.search(page)
if not match:
raise ValueError(
" - Failed to parse HTML. Page Data not found in the source code."
)
page_data = match.group(1)
try:
data = json.loads(page_data)
except json.JSONDecodeError as e:
raise json.JSONDecodeError(f"Failed to parse JSON: {e}")
show_id = data.get("video", {}).get("showUrlCode")
if not show_id:
raise ValueError(" - showUrlCode not found in the source code.")
return show_id
def _signature_header(self, url: str) -> str:
timestamp = int(time.time())
message = f"{timestamp}:{url}".encode("utf-8")
api_key = bytes.fromhex(self.config["api_key"])
signature = hmac.new(api_key, message, hashlib.sha256).hexdigest()
return f"{timestamp}_{signature}"
def _auth_header(self) -> str:
now_utc = datetime.now(timezone.utc)
timestamp_str = now_utc.strftime("%Y%m%d%H%M%S")
encoded_bytes = base64.b64encode(timestamp_str.encode("utf-8"))
return encoded_bytes.decode("ascii")
def _request(self, method: str, url: str, **kwargs: Any) -> Any[dict | str]:
if method == "GET":
self.session.headers.update(
{
"X-N10-SIG": self._signature_header(url),
"tp-acceptfeature": "v1/fw;v1/drm;v2/live",
"tp-platform": "UAP",
}
)
elif method == "POST":
self.session.headers.update({"X-Network-Ten-Auth": self._auth_header()})
prep = self.session.prepare_request(Request(method, url, **kwargs))
response = self.session.send(prep)
if response.status_code not in (200, 201):
raise ConnectionError(f"{response.text}")
try:
return json.loads(response.content)
except json.JSONDecodeError:
return True if "true" in response.text else False
@staticmethod
def _sanitize(title: str) -> str:
title = title.lower()
title = title.replace("&", "and")
title = re.sub(r"[:;/()]", "", title)
title = re.sub(r"[ ]", "-", title)
title = re.sub(r"[\\*!?¿,'\"<>|$#`]", "", title)
title = re.sub(rf"[{'.'}]{{2,}}", ".", title)
title = re.sub(rf"[{'_'}]{{2,}}", "_", title)
title = re.sub(rf"[{'-'}]{{2,}}", "-", title)
title = re.sub(rf"[{' '}]{{2,}}", " ", title)
return title
@@ -1,9 +0,0 @@
headers:
User-Agent: 10play/7.4.0.500325 Android UAP
endpoints:
config: https://10.com.au/api/v1/config
auth: https://10.com.au/api/user/auth
query: https://vod.ten.com.au/api/videos/bcquery # androidapps-v2
api_key: "b918ff793563080c5821c89ee6c415c363cb36d369db1020369ac4b405a0211d"
@@ -1,332 +0,0 @@
from __future__ import annotations
import json
import re
from collections.abc import Generator
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
import click
from click import Context
from envied.core.credential import Credential
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapters, Subtitle, Tracks
import requests
def get_widevine_license_url(manifest_str):
# Try JSON first
try:
data = json.loads(manifest_str)
for source in data.get("sources", []):
widevine = source.get("key_systems", {}).get("com.wienvied.alpha")
if widevine and "license_url" in widevine:
return widevine["license_url"]
except json.JSONDecodeError:
pass
# Fallback for XML style
match = re.search(r'bc:licenseAcquisitionUrl="([^"]+)"', manifest_str)
if match:
return match.group(1)
return None
class TPTV(Service):
"""
Service code for TPTVencore streaming service (https://www.TPTVencore.co.uk/).
\b
version 1.0.4
Date: June 2025
Author: A_n_g_e_l_a
Authorization: email/password for service in envied.yaml
Robustness:
DRM free... with rare exceptions
\b
Note:
TPTV will not allow the usual -w S01-S04 syntax as TPTV is eclictic in what it serves.
Series and episodes carry little meaning on this platform.
It is not possible to remove S00E00 from the end of a video title - envied insists.
\b
Tips:
Use complete url in all cases.
SERIES: https://tptvencore.co.uk/collection/1717422888871355373
Note: TPTV do not specify Series and Episodes numbers in any meaningful and organized way.
They MAY sometimes be in the program title, but often incomplete.
FILM: https://tptvencore.co.uk/product/the-importance-of-being-earnest-6290333578001
EPISODE: https://tptvencore.co.uk/product/sherlock-holmes---the-case-of-greystone-inscription-s1-ep16-6282604132001
\b
Examples:
SERIES: https://tptvencore.co.uk/collection/1717422888871355373
EPISODE: https://tptvencore.co.uk/product/sherlock-holmes---the-case-of-greystone-inscription-s1-ep16-6282604132001
FILM: https://tptvencore.co.uk/product/the-importance-of-being-earnest-6290333578001
"""
GEOFENCE = ("gb",)
ALIASES = ("TPTVencore",)
@staticmethod
@click.command(name="TPTV", short_help="https://www.tptvencore.co.uk/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> TPTV:
return TPTV(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.profile = ctx.parent.params.get("profile")
if not self.profile:
self.profile = "default"
self.session = requests.session()
self.session.headers.update(self.config["headers"])
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
cache = self.cache.get(f"tokens_{credential.sha1}")
# first contact
fc_headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
'Accept': '*/*',
'Accept-Language': 'en-GB,en;q=0.5',
'api-key': 'zq5pyPd0RTbNg3Fyj52PrkKL9c2Af38HHh4itgZTKDaCzjAyhd',
'Referer': 'https://tptvencore.co.uk/',
'tenant': 'encore',
'Content-Type': 'application/json',
'Origin': 'https://tptvencore.co.uk',
'DNT': '1',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Priority': 'u=0',
}
payload = {}
r = self.session.post(self.config["endpoints"]["session"], headers=fc_headers, json=payload)
if r.status_code != 200:
raise ConnectionError
else:
session_id = r.json()['id']
self.session.headers.update({'session': session_id})
# login
if cache and not cache.expired:
# cached
self.log.info(" + Using cached Tokens...")
tokens = cache.data
else:
self.log.info(" + Logging in...")
payload = {"email": credential.username, "password": credential.password}
r = self.session.post(
self.config["endpoints"]["login"],
headers=self.session.headers,
json={
"email": credential.username,
"password": credential.password,
},
)
try:
res = r.json()
except json.JSONDecodeError:
raise ValueError(f"Failed to refresh tokens: {r.text}")
tokens = res
self.log.info(" + Acquired tokens...")
# cache.set(tokens)
self.authorization = tokens
def search(self) -> Generator[SearchResult, None, None]:
query = self.title.replace(" ", "+")
search_url = self.config["endpoints"]["search"].replace("{query}", query)
session_id = self.session.headers['session']
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
'Accept': '*/*',
'Accept-Language': 'en-GB,en;q=0.5',
'Referer': 'https://tptvencore.co.uk/',
'session': session_id,
'tenant': 'encore',
'Origin': 'https://tptvencore.co.uk',
'DNT': '1',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Priority': 'u=4',
}
r = self.session.get(search_url, headers=headers)
if r.status_code != 200:
raise ConnectionError(f"Search failed with {r.status_code}: {r.text}")
results = r.json()["data"]
myitems = []
if isinstance(results, list):
for result in results:
# do lookup on list item product or collection
if result.startswith('collection_'):
id = result.replace('collection_','')
collection_url = f"https://prod.suggestedtv.com/api/client/v1/collection/by-reference/{id}?extend=label"
response = self.session.get(collection_url, headers=headers)
if response.status_code == 200:
data = response.json()
for item in data['children']:
myitems.append(item['id'].replace('product_',''))
else:
print(f"Error: {response.status_code} - {response.text}")
else:
myitems.append(result.replace('product_',''))
mystring = ",".join(myitems)
continued_search_url = "https://prod.suggestedtv.com/api/client/v1/product?ids=" + mystring + "&extend=label"
response = self.session.get(continued_search_url, headers=headers)
response.raise_for_status
results = response.json()
if isinstance(results, list):
items = results
elif isinstance(results, dict) and "data" in results:
items = results["data"]
else:
raise ValueError("Unexpected search result format")
for item in items:
try:
yield SearchResult(
id_=item.get("reference"),
title=item.get("name"),
description=item.get("description", "").replace('\n', ' '),
label=item.get("type", ""),
url=f"https://tptvencore.co.uk/product/{item.get('reference')}",
)
except Exception as e:
print(f"Failed to yield item: {e}")
def get_titles(self) -> Union[Movies, Series]:
data = self.get_data(self.title)
ids = ",".join(data)
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
'Accept': '*/*',
'Accept-Language': 'en-GB,en;q=0.5',
'Referer': 'https://tptvencore.co.uk/',
'tenant': 'encore',
'Origin': 'https://tptvencore.co.uk',
'DNT': '1',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Priority': 'u=4',
}
session_id = self.session.headers['session']
headers['session'] = session_id
params = {
'ids': ids,
'extend': 'label',
}
response = self.session.get('https://prod.suggestedtv.com/api/client/v1/product', params=params, headers=headers)
if response.status_code == 200:
mydata=json.loads(response.text)
titles = mydata['data']
episodes =[
Episode(
id_=episode["id"],
service=self.__class__,
title=episode["name"],
season=0,
number=0,
name = '',
language="en", # TODO: language detection
data=episode,
)
for episode in titles
]
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
playlist = f"https://edge.api.brightcove.com/playback/v1/accounts/6272132012001/videos/{title.data.get('id')}"
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
'Accept': '*/*',
'Accept-Language': 'en-GB,en;q=0.5',
'Referer': 'https://tptvencore.co.uk/',
'BCOV-Policy': 'BCpkADawqM1yq3Go9abHJ4lBZ0wrYStC-pS1W01hdlACHxsiIz9AvQXy1wa3iqyd6yVJLXLZnZjFkKI2BCJjbtxiJqyPMZjIezEWKrI1TTSbugkD6dAXs7Ucxq09P9zQ8ZRU4ZjTa83VFhiL',
'Origin': 'https://tptvencore.co.uk',
'DNT': '1',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Priority': 'u=4',
}
r = requests.get(playlist, headers=headers)
if r.status_code != 200:
raise ConnectionError(r.text)
data = r.json()
self.manifest = data["sources"][2].get("src")
tracks = DASH.from_url(self.manifest, self.session).to_tracks(title.language)
tracks.videos[0].data = data
# odd couple of DRM vids found
self.license = get_widevine_license_url(r.text)
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
return Chapters()
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
r = self.session.post(url=self.license, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
def get_data(self, url: str) -> dict:
self.session.headers.update({'tenant': 'encore'})
if 'collection' in url:
prod_id = url.split('/')[-1]
url = f"https://prod.suggestedtv.com/api/client/v1/collection/by-reference/{prod_id}?extend=label"
r = self.session.get(url)
if r.status_code != 200:
raise ConnectionError(r.text)
myjson = r.json()
children = myjson.get('children')
product_links = []
for child in children:
product_links.append(child['id']) if 'product' in child.get('classification') else None
return product_links
elif 'product' in url: # single item
prod_id = url.split('-')[-1]
return [prod_id]
@@ -1,23 +0,0 @@
headers:
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: '*/*'
Accept-Language: en-GBen;q=0.5
Access-Control-Request-Method: POST
Access-Control-Request-Headers: baggagecontent-typesentry-tracesessiontenant
Referer: https://tptvencore.co.uk/
Origin: https://tptvencore.co.uk
DNT: '1'
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Priority: u=4
session:
api-key: zq5pyPd0RTbNg3Fyj52PrkKL9c2Af38HHh4itgZTKDaCzjAyhd
endpoints:
login: https://prod.suggestedtv.com/api/client/v1/session/login
session: https://prod.suggestedtv.com/api/client/v1/session
search: https://prod.suggestedtv.com/api/client/v2/search/{query}
@@ -1,302 +0,0 @@
from __future__ import annotations
import hashlib
import os
import re
import sys
import uuid
from collections.abc import Generator
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional
import click
from langcodes import Language
from pyplayready.cdm import Cdm as PlayReadyCdm
from envied.core.credential import Credential
from envied.core.downloaders import aria2c, requests
from envied.core.manifests import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
from envied.core.tracks import Audio, Chapter, Chapters, Subtitle, Track, Tracks
class TUBI(Service):
"""
Service code for TubiTV streaming service (https://tubitv.com/)
\b
Version: 1.0.5
Author: stabbedbybrick
Authorization: Cookies (Optional)
Geofence: Locked to whatever region the user is in (API only)
Robustness:
Widevine:
L3: 1080p, AAC2.0
PlayReady:
SL2000: 1080p, AAC2.0
Clear:
1080p, AAC2.0
\b
Tips:
- Input can be complete title URL or just the path:
/series/300001423/gotham
/tv-shows/200024793/s01-e01-pilot
/movies/589279/the-outsiders
- Use '-v H.265' to request HEVC tracks.
\b
Notes:
- Authentication is currently not required, but cookies are used if provided.
- If 1080p exists, it's currently only available as H.265.
- Unshackle fails to mux properly when n_m3u8dl_re is used, so aria2c is forced as downloader.
- Search is currently disabled.
"""
TITLE_RE = r"^(?:https?://(?:www\.)?tubitv\.com?)?/(?P<type>movies|series|tv-shows)/(?P<id>[a-z0-9-]+)"
@staticmethod
@click.command(name="TUBI", short_help="https://tubitv.com/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx, **kwargs):
return TUBI(ctx, **kwargs)
def __init__(self, ctx, title):
self.title = title
super().__init__(ctx)
cdm = ctx.obj.cdm
self.drm_system = "playready" if isinstance(cdm, PlayReadyCdm) else "widevine"
vcodec = ctx.parent.params.get("vcodec")
self.vcodec = "H264" if vcodec is None else "H265"
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
self.auth_token = None
if cookies is not None:
self.auth_token = next((cookie.value for cookie in cookies if cookie.name == "at"), None)
self.session.headers.update({"Authorization": f"Bearer {self.auth_token}"})
# Disable search for now
# def search(self) -> Generator[SearchResult, None, None]:
# params = {
# "search": self.title,
# "include_linear": "true",
# "include_channels": "false",
# "is_kids_mode": "false",
# }
# r = self.session.get(self.config["endpoints"]["search"], params=params)
# r.raise_for_status()
# results = r.json()
# from devine.core.console import console
# console.print(results)
# exit()
# for result in results:
# label = "series" if result["type"] == "s" else "movies" if result["type"] == "v" else result["type"]
# title = (
# result.get("title", "")
# .lower()
# .replace(" ", "-")
# .replace(":", "")
# .replace("(", "")
# .replace(")", "")
# .replace(".", "")
# )
# yield SearchResult(
# id_=f"https://tubitv.com/{label}/{result.get('id')}/{title}",
# title=result.get("title"),
# description=result.get("description"),
# label=label,
# url=f"https://tubitv.com/{label}/{result.get('id')}/{title}",
# )
def get_titles(self) -> Titles_T:
try:
kind, content_id = (re.match(self.TITLE_RE, self.title).group(i) for i in ("type", "id"))
except Exception:
raise ValueError("Could not parse ID from title - is the URL correct?")
params = {
"app_id": "tubitv",
"platform": "web", # web, android, androidtv
"device_id": str(uuid.uuid4()),
"content_id": content_id,
"limit_resolutions[]": [
"h264_1080p",
"h265_1080p",
],
"video_resources[]": [
"dash_widevine_nonclearlead",
"dash_playready_psshv0",
"dash",
],
}
if kind == "tv-shows":
content = self.session.get(self.config["endpoints"]["content"], params=params)
content.raise_for_status()
series_id = "0" + content.json().get("series_id")
params.update({"content_id": int(series_id)})
data = self.session.get(self.config["endpoints"]["content"], params=params).json()
return Series(
[
Episode(
id_=episode["id"],
service=self.__class__,
title=data["title"],
season=int(season.get("id", 0)),
number=int(episode.get("episode_number", 0)),
name=episode["title"].split("-")[1],
year=data.get("year"),
language=Language.find(episode.get("lang", "en")).to_alpha3(),
data=episode,
)
for season in data["children"]
for episode in season["children"]
if episode["id"] == content_id
]
)
if kind == "series":
r = self.session.get(self.config["endpoints"]["content"], params=params)
r.raise_for_status()
data = r.json()
return Series(
[
Episode(
id_=episode["id"],
service=self.__class__,
title=data["title"],
season=int(season.get("id", 0)),
number=int(episode.get("episode_number", 0)),
name=episode["title"].split("-")[1],
year=data.get("year"),
language=Language.find(episode.get("lang") or "en").to_alpha3(),
data=episode,
)
for season in data["children"]
for episode in season["children"]
]
)
if kind == "movies":
r = self.session.get(self.config["endpoints"]["content"], params=params)
r.raise_for_status()
data = r.json()
return Movies(
[
Movie(
id_=data["id"],
service=self.__class__,
year=data.get("year"),
name=data["title"],
language=Language.find(data.get("lang", "en")).to_alpha3(),
data=data,
)
]
)
def get_tracks(self, title: Title_T) -> Tracks:
if not (resources := title.data.get("video_resources")):
self.log.error(" - Failed to obtain video resources. Check geography settings.")
self.log.info(f"Title is available in: {title.data.get('country')}")
sys.exit(1)
codecs = [x.get("codec") for x in resources]
if not any(self.vcodec in x for x in codecs):
raise ValueError(f"Could not find a {self.vcodec} video resource for this title")
resource = next((
x for x in resources
if self.drm_system in x.get("type", "") and self.vcodec in x.get("codec", "")
), None) or next((
x for x in resources
if self.drm_system not in x.get("type", "") and
"dash" in x.get("type", "") and
self.vcodec in x.get("codec", "")
), None)
if not resource:
raise ValueError("Could not find a video resource for this title")
manifest = resource.get("manifest", {}).get("url")
if not manifest:
raise ValueError("Could not find a manifest for this title")
title.data["license_url"] = resource.get("license_server", {}).get("url")
tracks = DASH.from_url(url=manifest, session=self.session).to_tracks(language=title.language)
for track in tracks:
rep_base = track.data["dash"]["representation"].find("BaseURL")
if rep_base is not None:
base_url = os.path.dirname(track.url)
track_base = rep_base.text
track.url = f"{base_url}/{track_base}"
track.descriptor = Track.Descriptor.URL
track.downloader = aria2c
if isinstance(track, Audio):
role = track.data["dash"]["adaptation_set"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
if title.data.get("subtitles"):
tracks.add(
Subtitle(
id_=hashlib.md5(title.data["subtitles"][0]["url"].encode()).hexdigest()[0:6],
url=title.data["subtitles"][0]["url"],
codec=Subtitle.Codec.from_mime(title.data["subtitles"][0]["url"][-3:]),
language=title.data["subtitles"][0].get("lang_alpha3", title.language),
downloader=requests,
is_original_lang=True,
forced=False,
sdh=False,
)
)
return tracks
def get_chapters(self, title: Title_T) -> Chapters:
if not (cue_points := title.data.get("credit_cuepoints")):
return Chapters()
chapters = []
if cue_points.get("recap_start"):
chapters.append(Chapter(name="Recap", timestamp=float(cue_points["recap_start"])))
if cue_points.get("intro_start") and cue_points.get("intro_end"):
chapters.append(Chapter(name="Intro", timestamp=float(cue_points["intro_start"])))
chapters.append(Chapter(timestamp=float(cue_points["intro_end"])))
if cue_points.get("early_credits_start"):
chapters.append(Chapter(name="Early Credits", timestamp=float(cue_points["early_credits_start"])))
if cue_points.get("postlude"):
chapters.append(Chapter(name="End Credits", timestamp=float(cue_points["postlude"])))
return sorted(chapters, key=lambda x: x.timestamp)
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, *, challenge: bytes, title: Episode | Movie, track: Any) -> bytes | str | None:
if not (license_url := title.data.get("license_url")):
return None
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
def get_playready_license(self, *, challenge: bytes, title: Episode | Movie, track: Any) -> bytes | str | None:
if not (license_url := title.data.get("license_url")):
return None
r = self.session.post(url=license_url, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
@@ -1,5 +0,0 @@
endpoints:
content: https://uapi.adrise.tv/cms/content # https://content-cdn.production-public.tubi.io/api/v2/content
search: https://search.production-public.tubi.io/api/v1/search
@@ -1,308 +0,0 @@
from __future__ import annotations
import json
from collections.abc import Generator
from datetime import timedelta
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
from urllib.parse import urljoin, urlparse
import click
from click import Context
from lxml import etree
from pywidevine.cdm import Cdm as WidevineCdm
from requests import Request
from envied.core.credential import Credential
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapters, Tracks
class TVNZ(Service):
"""
\b
Service code for TVNZ streaming service (https://www.tvnz.co.nz).
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: Credentials
Robustness:
L3: 1080p, AAC2.0
\b
Tips:
- Input can be comlete URL or path:
SHOW: /shows/tulsa-king
EPISODE: /shows/tulsa-king/episodes/s1-e1
MOVIE: /shows/the-revenant
SPORT: /sport/tennis/wta-tour/guadalajara-open-final
"""
GEOFENCE = ("nz",)
@staticmethod
@click.command(name="TVNZ", short_help="https://www.tvnz.co.nz", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> TVNZ:
return TVNZ(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.session.headers.update(self.config["headers"])
def search(self) -> Generator[SearchResult, None, None]:
params = {
"q": self.title.strip(),
"includeTypes": "all",
}
results = self._request("GET", "/api/v1/android/play/search", params=params)["results"]
for result in results:
yield SearchResult(
id_=result["page"].get("url"),
title=result.get("title"),
description=result.get("synopsis"),
label=result.get("type"),
url="https://www.tvnz.co.nz" + result["page"].get("url"),
)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential)
if not credential:
raise EnvironmentError("Service requires Credentials for Authentication.")
cache = self.cache.get(f"tokens_{credential.sha1}")
if cache and not cache.expired:
self.log.info(" + Using cached Tokens...")
tokens = cache.data
else:
self.log.info(" + Logging in...")
payload = {"email": credential.username, "password": credential.password, "keepMeLoggedIn": True}
response = self.session.post(
self.config["endpoints"]["base_api"] + "/api/v1/androidtv/consumer/login", json=payload
)
response.raise_for_status()
if not response.headers.get("aat"):
raise ValueError("Failed to authenticate: " + response.text)
tokens = {
"access_token": response.headers.get("aat"),
"aft_token": response.headers.get("aft"), # ?
}
cache.set(tokens, expiration=response.headers.get("aat_expires_in"))
self.session.headers.update({"Authorization": "Bearer {}".format(tokens["access_token"])})
# Disable SSL verification due to issues with newer versions of requests library.
self.session.verify = False
def get_titles(self) -> Union[Movies, Series]:
try:
path = urlparse(self.title).path
except Exception as e:
raise ValueError("Could not parse ID from title: {}".format(e))
page = self._request("GET", "/api/v4/androidtv/play/page/{}".format(path))
if page["layout"].get("video"):
title = page.get("title", "").replace("Episodes", "")
video = self._request("GET", page["layout"]["video"].get("href"))
episodes = self._episode(video, title)
return Series(episodes)
else:
module = page["layout"]["slots"]["main"]["modules"][0]
label = module.get("label", "")
lists = module.get("lists")
title = page.get("title", "").replace(label, "")
seasons = [x.get("href") for x in lists]
episodes = []
for season in seasons:
data = self._request("GET", season)
episodes.extend([x for x in data["_embedded"].values()])
while data.get("nextPage"):
data = self._request("GET", data["nextPage"])
episodes.extend([x for x in data["_embedded"].values()])
if label in ("Episodes", "Stream"):
episodes = self._show(episodes, title)
return Series(episodes)
elif label in ("Movie", "Movies"):
movie = self._movie(episodes, title)
return Movies(movie)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
metadata = title.data.get("publisherMetadata") or title.data.get("media")
if not metadata:
self.log.error("Unable to find metadata for this episode")
return
source = metadata.get("type") or metadata.get("source")
video_id = metadata.get("brightcoveVideoId") or metadata.get("id")
account_id = metadata.get("brightcoveAccountId") or metadata.get("accountId")
playback = title.data.get("playbackHref", "")
self.drm_token = None
if source != "brightcove":
data = self._request("GET", playback)
self.license = (
data["encryption"]["licenseServers"]["widevine"]
if data["encryption"].get("drmEnabled")
else None
)
self.drm_token = data["encryption"].get("drmToken")
source_manifest = data["streaming"]["dash"].get("url")
else:
data = self._request(
"GET", self.config["endpoints"]["brightcove"].format(account_id, video_id),
headers={"BCOV-POLICY": self.config["policy"]},
)
self.license = next((
x["key_systems"]["com.widevine.alpha"]["license_url"]
for x in data["sources"]
if x.get("key_systems").get("com.widevine.alpha")),
None,
)
source_manifest = next((
x["src"] for x in data["sources"]
if x.get("key_systems").get("com.widevine.alpha")),
None,
)
manifest = self.trim_duration(source_manifest)
tracks = DASH.from_text(manifest, source_manifest).to_tracks(title.language)
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
return Chapters()
def get_widevine_service_certificate(self, **_: Any) -> str:
return WidevineCdm.common_privacy_cert
def get_widevine_license(self, challenge: bytes, **_: Any) -> str:
if not self.license:
return None
headers = {"Authorization": f"Bearer {self.drm_token}"} if self.drm_token else self.session.headers
r = self.session.post(self.license, headers=headers, data=challenge)
r.raise_for_status()
return r.content
# Service specific
def _show(self, episodes: list, title: str) -> Episode:
return [
Episode(
id_=episode.get("videoId"),
service=self.__class__,
title=title,
season=int(episode.get("seasonNumber")) if episode.get("seasonNumber") else 0,
number=int(episode.get("episodeNumber")) if episode.get("episodeNumber") else 0,
name=episode.get("title"),
language="en",
data=episode,
)
for episode in episodes
]
def _movie(self, movies: list, title: str) -> Movie:
return [
Movie(
id_=movie.get("videoId"),
service=self.__class__,
name=title,
year=None,
language="en",
data=movie,
)
for movie in movies
]
def _episode(self, video: dict, title: str) -> Episode:
kind = video.get("type")
name = video.get("title")
if kind == "sportVideo" and video.get("_embedded"):
_type = next((x for x in video["_embedded"].values() if x.get("type") == "competition"), None)
title = _type.get("title") if _type else title
name = video.get("title", "") + " " + video.get("phase", "")
return [
Episode(
id_=video.get("videoId"),
service=self.__class__,
title=title,
season=int(video.get("seasonNumber")) if video.get("seasonNumber") else 0,
number=int(video.get("episodeNumber")) if video.get("episodeNumber") else 0,
name=name,
language="en",
data=video,
)
]
def _request(
self,
method: str,
api: str,
params: dict = None,
headers: dict = None,
payload: dict = None,
) -> Any[dict | str]:
url = urljoin(self.config["endpoints"]["base_api"], api)
if headers:
self.session.headers.update(headers)
prep = self.session.prepare_request(Request(method, url, params=params, json=payload))
response = self.session.send(prep)
try:
data = json.loads(response.content)
if data.get("message"):
raise ConnectionError(f"{response.status_code} - {data.get('message')}")
return data
except Exception as e:
raise ConnectionError("Request failed: {} - {}".format(response.status_code, response.text))
def trim_duration(self, source_manifest: str) -> str:
"""
The last segment on all tracks return a 404 for some reason, causing a failed download.
So we trim the duration by exactly one segment to account for that.
TODO: Calculate the segment duration instead of assuming length.
"""
manifest = DASH.from_url(source_manifest, self.session).manifest
period_duration = manifest.get("mediaPresentationDuration")
period_duration = DASH.pt_to_sec(period_duration)
hours, minutes, seconds = str(timedelta(seconds=period_duration - 6)).split(":")
new_duration = f"PT{hours}H{minutes}M{seconds}S"
manifest.set("mediaPresentationDuration", new_duration)
return etree.tostring(manifest, encoding="unicode")
@@ -1,9 +0,0 @@
headers:
User-Agent: "AndroidTV/!/!"
x-tvnz-api-client-id: "androidtv/!.!.!"
endpoints:
base_api: "https://apis-public-prod.tech.tvnz.co.nz"
brightcove: "https://edge.api.brightcove.com/playback/v1/accounts/{}/videos/{}"
policy: "BCpkADawqM0IurzupiJKMb49WkxM__ngDMJ3GOQBhN2ri2Ci_lHwDWIpf4sLFc8bANMc-AVGfGR8GJNgxGqXsbjP1gHsK2Fpkoj6BSpwjrKBnv1D5l5iGPvVYCo"
@@ -1,194 +0,0 @@
from __future__ import annotations
import re
from collections.abc import Generator
from datetime import timedelta
from typing import Any, Union
import click
from click import Context
from lxml import etree
from envied.core.manifests.dash import DASH
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapter, Chapters, Tracks
class UKTV(Service):
"""
Service code for 'U' (formerly UKTV Play) streaming service (https://u.co.uk/).
\b
Version: 1.0.1
Author: stabbedbybrick
Authorization: None
Robustness:
L3: 1080p
\b
Tips:
- Use complete title URL as input:
SERIES: https://u.co.uk/shows/love-me/watch-online
EPISODE: https://u.co.uk/shows/love-me/series-1/episode-1/6355269425112
"""
GEOFENCE = ("gb",)
ALIASES = ("uktvplay", "u",)
@staticmethod
@click.command(name="UKTV", short_help="https://u.co.uk/", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> UKTV:
return UKTV(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
self.session.headers.update({"user-agent": "okhttp/4.7.2"})
self.base = self.config["endpoints"]["base"]
def search(self) -> Generator[SearchResult, None, None]:
r = self.session.get(self.base + f"search/?q={self.title}")
r.raise_for_status()
results = r.json()
for result in results:
link = "https://u.co.uk/shows/{}/watch-online"
yield SearchResult(
id_=link.format(result.get("slug")),
title=result.get("name"),
description=result.get("synopsis"),
label=result.get("type"),
url=link.format(result.get("slug")),
)
def get_titles(self) -> Union[Movies, Series]:
slug, video = self.parse_title(self.title)
r = self.session.get(self.base + f"brand/?slug={slug}")
r.raise_for_status()
data = r.json()
series = [series["id"] for series in data["series"]]
seasons = [self.session.get(self.base + f"series/?id={i}").json() for i in series]
if video:
episodes = [
Episode(
id_=episode.get("video_id"),
service=self.__class__,
title=episode.get("brand_name"),
season=int(episode.get("series_number", 0)),
number=int(episode.get("episode_number", 0)),
name=episode.get("name"),
language="en",
data=episode,
)
for season in seasons
for episode in season["episodes"]
if int(episode.get("video_id")) == int(video)
]
else:
episodes = [
Episode(
id_=episode.get("video_id"),
service=self.__class__,
title=episode.get("brand_name"),
season=int(episode.get("series_number", 0)),
number=int(episode.get("episode_number", 0)),
name=episode.get("name"),
language="en",
data=episode,
)
for season in seasons
for episode in season["episodes"]
]
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
r = self.session.get(
self.config["endpoints"]["playback"].format(id=title.id),
headers=self.config["headers"],
)
r.raise_for_status()
data = r.json()
self.license = next((
x["key_systems"]["com.widevine.alpha"]["license_url"]
for x in data["sources"]
if x.get("key_systems").get("com.widevine.alpha")),
None,
)
source_manifest = next((
x["src"] for x in data["sources"]
if x.get("key_systems").get("com.widevine.alpha")),
None,
)
if not self.license or not source_manifest:
raise ValueError("Failed to get license or manifest")
manifest = self.trim_duration(source_manifest)
tracks = DASH.from_text(manifest, source_manifest).to_tracks(title.language)
for track in tracks.audio:
role = track.data["dash"]["representation"].find("Role")
if role is not None and role.get("value") in ["description", "alternative", "alternate"]:
track.descriptive = True
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
chapters = []
if title.data.get("credits_cuepoint"):
chapters = [Chapter(name="Credits", timestamp=title.data.get("credits_cuepoint"))]
return Chapters(chapters)
def get_widevine_service_certificate(self, **_: Any) -> str:
return None
def get_widevine_license(self, challenge: bytes, **_: Any) -> bytes:
r = self.session.post(url=self.license, data=challenge)
if r.status_code != 200:
raise ConnectionError(r.text)
return r.content
# Service specific functions
@staticmethod
def parse_title(title: str) -> tuple[str, str]:
title_re = (
r"^(?:https?://(?:www\.)?u\.co.uk/shows/)?"
r"(?P<slug>[a-z0-9-]+)(?:/[a-z0-9-]+/[a-z0-9-]+/(?P<vid>[0-9-]+))?"
)
try:
slug, video = (re.match(title_re, title).group(i) for i in ("slug", "vid"))
except Exception:
raise ValueError("Could not parse ID from title - is the URL correct?")
return slug, video
@staticmethod
def trim_duration(source_manifest: str) -> str:
"""
The last segment on all tracks return a 404 for some reason, causing a failed download.
So we trim the duration by exactly one segment to account for that.
TODO: Calculate the segment duration instead of assuming length.
"""
manifest = DASH.from_url(source_manifest).manifest
period_duration = manifest.get("mediaPresentationDuration")
period_duration = DASH.pt_to_sec(period_duration)
hours, minutes, seconds = str(timedelta(seconds=period_duration - 6)).split(":")
new_duration = f"PT{hours}H{minutes}M{seconds}S"
manifest.set("mediaPresentationDuration", new_duration)
return etree.tostring(manifest, encoding="unicode")
@@ -1,9 +0,0 @@
headers:
BCOV-POLICY: BCpkADawqM2ZEz-kf0i2xEP9VuhJF_DB5boH7YAeSx5EHDSNFFl4QUoHZ3bKLQ9yWboSOBNyvZKm4HiZrqMNRxXm-laTAnmls1QOL7_kUM3Eij4KjQMz0epMs3WIedg64fnRxQTX6XubGE9p
User-Agent: Dalvik/2.1.0 (Linux; U; Android 12; SM-A226B Build/SP1A.210812.016)
Host: edge.api.brightcove.com
Connection: keep-alive
endpoints:
base: https://vschedules.uktv.co.uk/vod/
playback: https://edge.api.brightcove.com/playback/v1/accounts/1242911124001/videos/{id}
@@ -1,204 +0,0 @@
import logging
from abc import ABCMeta, abstractmethod
from http.cookiejar import CookieJar
from typing import Optional, Union, Any
from urllib.parse import urlparse
import click
import requests
from requests.adapters import HTTPAdapter, Retry
from rich.padding import Padding
from rich.rule import Rule
from envied.core.service import Service
from envied.core.titles import Movies, Movie, Titles_T, Title_T
from envied.core.cacher import Cacher
from envied.core.config import config
from envied.core.console import console
from envied.core.constants import AnyTrack
from envied.core.credential import Credential
from envied.core.tracks import Chapters, Tracks, Subtitle, Chapter
from envied.core.utilities import get_ip_info
from envied.core.manifests import HLS, DASH
from bs4 import BeautifulSoup
import hashlib
import base64
import re
import json
class YTBE(Service):
"""
\b
YTBE = Service code for Youtube vod (https://youtube.com)
\b
Version: 1.0.0
Author: sk8ord13
Authorization: Cookies
Robustness:
Widevine:
L3: 1080p
"""
GEOFENCE = ('en',)
LICENSE_SERVER_URL: str = 'https://www.youtube.com/youtubei/v1/player/get_drm_license'
YOUTUBE_VIDEO_INFO_URL: str = 'https://www.youtube.com/youtubei/v1/player'
API_KEY: str = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
@staticmethod
@click.command(name="YTBE", short_help="https://youtube.com", help=__doc__)
@click.argument("title", type=str, required=False)
@click.pass_context
def cli(ctx, **kwargs):
return YTBE(ctx, **kwargs)
def __init__(self, ctx, title):
self.title = title
self.last_response_data: dict[str, Any] = {}
super().__init__(ctx)
def get_titles(self) -> Titles_T:
youtube_url = f'https://www.youtube.com/watch?v={self.title}'
cookies_d: dict[str, str] = self.session.cookies.get_dict()
sapisid = cookies_d.get('__Secure-3PAPISID', '')
epoch = "1699161578"
origin = "https://www.youtube.com"
data = f"{epoch} {sapisid} {origin}"
sha1 = hashlib.sha1()
sha1.update(data.encode('utf-8'))
sapisidhash = f"SAPISIDHASH {epoch}_{sha1.hexdigest()}"
response = self.session.get(youtube_url)
soup = BeautifulSoup(response.text, 'html.parser')
yt_scripts = soup.find_all('script', string=re.compile('ytcfg.set'))
user_agent_extracted = "YouTube/15.49.4 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36 EdgA/46.0.0.1 GoogleTV/YouTube/16.12.34 (compatible; Widevine/1.4.8)"
client_name_extracted = "null"
client_version_extracted = "null"
id_token_extracted = "null"
vision_data_extracted = "null"
session_id_extracted = "null"
logged_yt_extracted = False
for yt_script in yt_scripts:
yt_script_content = yt_script.string
try:
match = re.search(r'ytcfg\.set\((.*?)\);', yt_script_content, re.DOTALL)
if match:
ytcfg_set_content = json.loads(match.group(1))
id_token_extracted = ytcfg_set_content.get("ID_TOKEN", id_token_extracted)
vision_data_extracted = ytcfg_set_content.get("INNERTUBE_CONTEXT", {}).get("client", {}).get("visitorData", vision_data_extracted)
session_id_extracted = ytcfg_set_content.get("SESSION_INDEX", session_id_extracted)
client_context = ytcfg_set_content.get('INNERTUBE_CONTEXT', {}).get('client', {})
user_agent_extracted = client_context.get('userAgent', user_agent_extracted)
client_version_extracted = client_context.get('clientVersion', client_version_extracted)
client_name_extracted = client_context.get('clientName', client_name_extracted)
logged_yt_extracted = ytcfg_set_content.get("LOGGED_IN", logged_yt_extracted)
except json.JSONDecodeError:
pass
headers = {
'authorization': sapisidhash,
'origin': origin,
'user-agent': user_agent_extracted,
'X-YouTube-Client-Version': client_version_extracted,
'X-Youtube-Identity-Token': id_token_extracted,
'x-goog-authuser': session_id_extracted,
'x-goog-visitor-id': vision_data_extracted,
'x-youtube-bootstrap-logged-in': f"{logged_yt_extracted}",
'x-youtube-client-version': client_version_extracted
}
params_get_titles = {'key': self.API_KEY}
json_data_payload = {
'context': {
'client': {
'userAgent': user_agent_extracted,
'clientName': client_name_extracted,
'clientVersion': client_version_extracted,
},
},
'videoId': self.title
}
response_data = self.session.post(self.YOUTUBE_VIDEO_INFO_URL, params=params_get_titles, headers=headers, json=json_data_payload).json()
streaming_data = response_data.get('streamingData')
if not streaming_data:
return Movies([])
title_name = response_data['videoDetails']['title']
self.last_response_data = response_data
self.last_response_data['ytcfg_params'] = {
'user_agent': user_agent_extracted,
'client_name': client_name_extracted,
'client_version': client_version_extracted,
'id_token': id_token_extracted,
'vision_data': vision_data_extracted,
'session_id': session_id_extracted,
'logged_yt': logged_yt_extracted,
'video_id': self.title
}
self.last_response_data['request_headers'] = headers
return Movies([Movie(
id_=self.title,
service=self.__class__,
name=title_name,
year=None,
language=None
)])
def get_tracks(self, title_obj: Title_T) -> Tracks:
response_data = self.last_response_data
dashManifestUrl = response_data.get('streamingData', {}).get('dashManifestUrl')
return DASH.from_url(dashManifestUrl, session=self.session).to_tracks(language="en")
def get_chapters(self, title: Title_T) -> Chapters:
return []
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[str]:
response_data = self.last_response_data
drm_params = response_data.get("streamingData", {}).get("drmParams")
ytcfg_params = response_data.get('ytcfg_params', {})
user_agent = ytcfg_params.get('user_agent')
clientName = ytcfg_params.get('client_name')
clientVersion = ytcfg_params.get('client_version')
video_id = ytcfg_params.get('video_id')
session_id = ytcfg_params.get('session_id')
request_headers = response_data.get('request_headers', {})
lic_url = self.LICENSE_SERVER_URL
params_license = {'key': self.API_KEY}
json_data = {
'context': {
'client': {
'userAgent': user_agent,
'clientName': clientName,
'clientVersion': clientVersion,
},
},
'drmSystem': 'DRM_SYSTEM_WIDEVINE',
'videoId': video_id,
'cpn': 'MsQQaCE9gAkD9iLF',
'sessionId': session_id,
'drmParams': drm_params
}
json_data["licenseRequest"] = base64.b64encode(challenge).decode("utf-8")
get_res = requests.post(lic_url, params=params_license, headers=request_headers, json=json_data).json()
license_b64 = get_res.get("license")
if license_b64:
return license_b64.replace("-", "+").replace("_", "/")
@@ -1,5 +0,0 @@
cookie:
cookies\YTBE\default.txt
service:
services\YTBE\__init__.py
@@ -1,173 +0,0 @@
# Netscape HTTP Cookie File
.youtube.com TRUE / FALSE 1784370918 SID g.a000yAi9zzvdmi_M0Ze8c5X8qqXPAbM56-hFbHVgrA6DD4z5dISmTmFdTrrsfZMgIvZlmc_x-wACgYKAYISARQSFQHGX2MizhHhetLRoYNmuk2aEZCyQRoVAUF8yKqzF7bSpf29WTmggHnnz0gS0076
.youtube.com TRUE / TRUE 1781524392 __Secure-1PSIDTS sidts-CjIB5H03P2jhDmsG_y7f4uI_HqL0UW7y3a2CvxAErOwihIlb7QaTWfLXaCiztkUP9b106RAA
.youtube.com TRUE / TRUE 1781524392 __Secure-3PSIDTS sidts-CjIB5H03P2jhDmsG_y7f4uI_HqL0UW7y3a2CvxAErOwihIlb7QaTWfLXaCiztkUP9b106RAA
.youtube.com TRUE / TRUE 1784370918 __Secure-1PSID g.a000yAi9zzvdmi_M0Ze8c5X8qqXPAbM56-hFbHVgrA6DD4z5dISm0UWUFcaDv3bSngT-YtzptQACgYKAU8SARQSFQHGX2MiQQcOmo2GIkSr7_bS2DFxtRoVAUF8yKoqq73_7YxHCaAkcWc8Vv-40076
.youtube.com TRUE / TRUE 1784370918 __Secure-3PSID g.a000yAi9zzvdmi_M0Ze8c5X8qqXPAbM56-hFbHVgrA6DD4z5dISmXYyhva_jPKOPYEn31p_zkAACgYKAakSARQSFQHGX2MiQVPhEE5D6OvV9WhBJDo7sxoVAUF8yKo8bFy_jXoYyyKECLRrZYE-0076
.youtube.com TRUE / FALSE 1784370918 HSID ACBOZgbd6SYdWeivd
.youtube.com TRUE / TRUE 1784370918 SSID A30Hz0ZyxnNqA0j1K
.youtube.com TRUE / FALSE 1784370918 APISID s4r2XN1pV_ZwNlsa/A7jGociFTm8siA3sH
.youtube.com TRUE / TRUE 1784370918 SAPISID PyRmBXh4-UYF06Jb/AYUJ9V69VnwRk-Ekv
.youtube.com TRUE / TRUE 1784370918 __Secure-1PAPISID PyRmBXh4-UYF06Jb/AYUJ9V69VnwRk-Ekv
.youtube.com TRUE / TRUE 1784370918 __Secure-3PAPISID PyRmBXh4-UYF06Jb/AYUJ9V69VnwRk-Ekv
.youtube.com TRUE / TRUE 1771161048 LOGIN_INFO AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug:QUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / TRUE 1770729047 __Secure-YEC CgtDdkwxVTVxcUo1QSja24m8BjIKCgJHQhIEGgAgKQ%3D%3D
.youtube.com TRUE / TRUE 1765540387 VISITOR_PRIVACY_METADATA CgJHQhIEGgAgDA%3D%3D
.youtube.com TRUE / TRUE 1765529606 __Secure-ROLLOUT_TOKEN CMO51fHrwfLMswEQoJ-lhd_tigMYhZnIzIbzjQM%3D
.youtube.com TRUE / FALSE 1781524395 SIDCC AKEyXzUkr0sNN4Mi7pHTodkzRycIaC6OCTX_kwyieFis8refmRe8qs0SSYI0AAz-IGh9sWnlDw
.youtube.com TRUE / TRUE 1781524395 __Secure-1PSIDCC AKEyXzWAuSRYdboa3aV3HRLiIU9ww5i6lr6_gPOL9iFOP5pGlT8yjYRHLQfxj59Ywgy3IrRiQw
.youtube.com TRUE / TRUE 1781524395 __Secure-3PSIDCC AKEyXzXAHZUOMDCa_OYvSMTI80GRBaqv7nGhVZYfM-a6V7LTMjz-w-Nlgjz14hlhA-qDQAjNew
.youtube.com TRUE / TRUE 1784548388 PREF f4=4000000&tz=Europe.London&f5=30000&f7=100
.youtube.com TRUE / TRUE 1765540387 VISITOR_INFO1_LIVE V4hGlzDr7Ok
.youtube.com TRUE / FALSE 1749372950 ST-1dypcsd itct=CAEQ7fkGGBMiEwip_eynuOGNAxWWPHsEHRH-E52aAQUIMhD0JA%3D%3D&csn=F_16dU8Nut7TU4Rd&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGBMiEwip_eynuOGNAxWWPHsEHRH-E52aAQUIMhD0JA%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F9Y6eA6XQEoM%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%229Y6eA6XQEoM%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYL58CrEZkKs_NSalGcXeozGqBkNBT0FyQkZ2dkw2X05xbE5pdF9sclhIam1lc19BdmlWTEZZWlo1RjZPaWxXTnVwY1VnOXBCcGVQQXNvczg5SnlJWmpFkAcCuAeb_-6nuOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F9Y6eA6XQEoM%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIqf3sp7jhjQMVljx7BB0R_hOd%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwib_-6nuOGNAxXHhfQHHT0vAjUqAEoPEg1wZXRlciBtaWxsYXJkogETCPrd8KCy4Y0DFczoSQcdU5UozA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2272df6f9e-0000-2e11-bdef-7474463f47b5%22%7D%7D
.youtube.com TRUE / FALSE 1749373013 ST-15dqge8 itct=CA8Q7fkGGAEiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=loqBZaESuFvP0ud0&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA8Q7fkGGAEiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FWsOLOqUWAjo%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22WsOLOqUWAjo%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYHfEqXvM8KmTQaVMzc1ZGwKqBkNBT0FyQkZzQkdrR2VyOTdtR2F0OXlCeldUd2FzeHhocW1IeUs2dXpkR2g0YUhtRXpuU1FBWDNkMHFZLTdCWW5Qa2J3kAcCuAeqw5T0ueGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FWsOLOqUWAjo%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBAQsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6fe4-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373051 ST-1abeef5 itct=CA0Q7fkGGAIiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=WcC327uIw2wtCk9M&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA0Q7fkGGAIiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FHjBuHH8kM38%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22HjBuHH8kM38%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYFzvYzkXrWCp5Ee_22eVdTqqBkNBT0FyQkZzTHl0d3hBSWdHSjMyOVZIdWtKT2NWanNHQ2k5TVRwMVg2cVc5MVVOTVdwY2VnamY0cERwX0NJTWZ0bkRVkAcCuAeqw5T0ueGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FHjBuHH8kM38%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA4QsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6fe5-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373056 ST-yrkwx1 itct=CAoQ7fkGGAQiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=XkIusWQlGgB_0JNC&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGAQiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fte6NUWY5csE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22te6NUWY5csE%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYHyFaQW2w0AAkaZJ5Mjwk5SqBkNBT0FyQkZ1Rmdjblp5eVJpbDg5bmg4YlN3ZFVRajhMVW1kZDNTVy1ncEdPZm4xRWpCMVFVREU1X1kzNFZQa1d3aC04kAcCuAeqw5T0ueGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fte6NUWY5csE%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A980%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6fe7-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373081 ST-1iu5721 itct=CAgQ7fkGGAUiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=aIGFV7KmJcwwNvIA&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGAUiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F9quGg5wlns4%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%229quGg5wlns4%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYJcV3GPqcy17JbBkGEDQgrGqBkNBT0FyQkZ0OXZoX1lpRUpwU1VRVGVIRWpFTDBfTzZZb19Hb05JZGtJUEpiZmNQMkg0X0RrTVFXckF0dzJGd3lmbDRZkAcCuAeqw5T0ueGNA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F9quGg5wlns4%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6fe8-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373089 ST-gxsb97 itct=CAYQ7fkGGAYiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=lNsCLotRj_XBHX9y&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAYiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fy6gtcX3K42Y%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22y6gtcX3K42Y%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYKOUZyqdatTbBsFU3s4nuxuqBkNBT0FyQkZ0c2VteVdXMFFtc3VDQUJhZ3NncjE4OXF2X1NJSkFKZFlGYl94ck5oTnBTVUptcUxrVlFDR0RZdTV6RVJVkAcCuAeqw5T0ueGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fy6gtcX3K42Y%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6fe9-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373106 ST-3zatce itct=CAMQ7fkGGAgiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=nWeCpdo-xYPJ-gVl&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAgiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F-JHrWrUhv3I%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22-JHrWrUhv3I%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYC9X8dKK2-xw_8WM5UXo18eqBkNBT0FyQkZ0cmhyanRFeWQ4VzdvRXh6WGF2ckZza0lVLUJJbkVPUDlsaDk4eXROa1dFakc2cTRhbFoyMS1ZR3A0SWxjkAcCuAeqw5T0ueGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F-JHrWrUhv3I%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6feb-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373144 ST-1sqvti0 itct=CAEQ7fkGGAkiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D&csn=oHnRYccDDOvcqiAg&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAkiEwjoyJP0ueGNAxX7EnMJHW-5N-w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F4e-UiG0PlUA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%224e-UiG0PlUA%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYCzzJriZK87U1sO2tA1IyxCqBkNBT0FyQkZzTTJiSHN0VzVJZzBWYXJQQjFDWGt4UVNITGNMMU1kY0N3aFFPbVpSbnpQMW5ZTGJlVmFSLWFFN2doUXMwkAcCuAeqw5T0ueGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F4e-UiG0PlUA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI6MiT9LnhjQMV-xJzCR1vuTfs%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiqw5T0ueGNAxXeLAYAHQ0FG18qAEoPEg1wZXRlciBtaWxsYXJkogETCKn97Ke44Y0DFZY8ewQdEf4TnQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22731b6fec-0000-296f-ae27-fc4116730059%22%7D%7D
.youtube.com TRUE / FALSE 1749373166 ST-4d25t8 itct=CB8Q7fkGGAIiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=OL7vzPUZ-xslnycr&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CB8Q7fkGGAIiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fpry3apQjhc4%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22pry3apQjhc4%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYDSav8QIFhxxVpF1nzs1tUGqBkNBT0FyQkZ1YWJKRGtEN0lkRlg4ZlJLSFJhZVE3eDZRUDZHNHZzQjZrUEVPOTZlWm5xa3QyTzk3dFV0UVZha0xMMzBBkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fpry3apQjhc4%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CCAQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f58-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373161 ST-1gjp20i itct=CCIQ7fkGGAAiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=cdz4We7g7ttoUKUC&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCIQ7fkGGAAiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FdxtVikmnLAI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22dxtVikmnLAI%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYOERiTYayhGEln6TiFPYVPOqBkNBT0FyQkZ1b0toZWt2U29qOXBXdjRJTUU3Z3ZSQ3J5NUhjRW5veWZJM2pCbXhObWhYV2NKOVdLVVVyT2RUb25HcmZNkAcCuAfziJrSuuGNA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FdxtVikmnLAI%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CCMQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f53-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373187 ST-kpjmio itct=CB0Q7fkGGAMiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=bn_uKLozM2jrxmU4&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CB0Q7fkGGAMiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FmjkKlE-cm1Y%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22mjkKlE-cm1Y%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYPG3R0j38uLrEXgBNIC6FgqqBkNBT0FyQkZ2SWJzSVBQTndkS3hWT0xvVFQ2NmhVQlpFOFE1NFBCVXpTeVUzdU5jYUV5aVpZY0taclRCUktyRWJ0VGZrkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FmjkKlE-cm1Y%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CB4QsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f59-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373195 ST-bgzyar itct=CBsQ7fkGGAQiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=AT3QoAD574FgN-2Y&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBsQ7fkGGAQiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FXsfHCOoz-pc%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22XsfHCOoz-pc%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYM1Z1aBZpz_qGGW1CXj5_XuqBkNBT0FyQkZ1ZDNodHBPVnhZN2g5OHpVN2VTYzV5dGJ4YmVwQ21lWUFYZmVJQmFEOVcyVzQxRG1fQ0RvOHhmRU50V3VrkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FXsfHCOoz-pc%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBwQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f5a-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373223 ST-19p12fx itct=CBgQ7fkGGAYiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=-8N5a1NMLInV6ulR&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBgQ7fkGGAYiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F6NCEio1aUhI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%226NCEio1aUhI%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYFfIrJ8kfK1Mn3fszGATchOqBkNBT0FyQkZ2MTkwVHpvUjBXMmlMci1Oa1FaZnd2NThvUHdMOVc1X1VBa2NxbDdrNFRaRENaa0ZBWUVjR0tKbzJUa3pzkAcCuAfziJrSuuGNA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F6NCEio1aUhI%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBkQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f5c-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373234 ST-6uhlkz itct=CBYQ7fkGGAciEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=5v_4cn1p493COCpW&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBYQ7fkGGAciEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fep2ehH49V2k%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22ep2ehH49V2k%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYO8kqchnmil2_8onCt3YdoSqBkNBT0FyQkZ0OTBrZklKTzdZV3djaXZZSXJvbUN4YmZ3QU1YNDNmWVd6bm5VNEN0VGNELURiUHNYXzRJWjZXS2ltRTB3kAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fep2ehH49V2k%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBcQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f5d-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373262 ST-1fj3sm7 itct=CBQQ7fkGGAgiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=Zu3v81wvas-1tvgg&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBQQ7fkGGAgiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F3QKL1vygH5g%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%223QKL1vygH5g%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYHvIM9KIDSbVrV_soIBDAfWqBkNBT0FyQkZ1X3FPUEUzVGJvSXFtbWg0N19uT2Q1ZUZZbUk2eFRaMlVRT1Jrem4tRXJTQ2JvbFhBMzhlZkx1VWIyTHVZkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F3QKL1vygH5g%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBUQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f5e-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373276 ST-1vz2pmn itct=CBEQ7fkGGAoiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=siyDFp69HdzCnKZj&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBEQ7fkGGAoiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FUCFDJXUUsRA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22UCFDJXUUsRA%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYI0F8IsUPpFWJ4WTgGopsfyqBkNBT0FyQkZ2UWx0R2FkRGY4dTNqUDdqcUY1NmtVcGN1Rm5jaWtsc1VTZ3BtalREcFFlTHhkNE5mN2pKU2c3ZlZRa0N3kAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FUCFDJXUUsRA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBIQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f60-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373284 ST-1sdmqg4 itct=CA8Q7fkGGAsiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=-RR73HfmMDM7hWHB&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA8Q7fkGGAsiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FkOudRAGvb7s%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22kOudRAGvb7s%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYDiqZeB4ow0dHqPb2Kh9m6yqBkNBT0FyQkZzRHA1OGJuVjFpdXY2QVg5eEdGa0NreUIyOUE4OXAyTUJPTHNERzktU1ZSRXg0akFlR3BEZm9Nc0NqZVdVkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FkOudRAGvb7s%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBAQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f61-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373300 ST-16oz5n itct=CA0Q7fkGGAwiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=24H69aeCGelg3ohM&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA0Q7fkGGAwiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FgInnRXb5le8%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22gInnRXb5le8%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYKPn9dFPobbrjdnpyqOmRxyqBkNBT0FyQkZzOHZXb0tmMzhaSVdldFdjUWt2Vk1XQWloX2Y1RmxRaUloLXN3UkdITHpycWhpVlNxRVhrVHRPVVgxRTlVkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FgInnRXb5le8%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA4QsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f62-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373336 ST-16im15o itct=CAoQ7fkGGA4iEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=RmNYSgmN30NtFp8S&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGA4iEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F60aCWGJL1V8%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%2260aCWGJL1V8%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYJsYYHbEUAXW3yUbkcsWS5iqBkNBT0FyQkZ1SVU1TTBuNzktSm5zYU1ZbnNIWXEtSGtGV2tlUmNoVm5Xa1g0dGVHdmVHM3E5cV9WckhORjBxU1ZfTHNjkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F60aCWGJL1V8%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f64-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373397 ST-1bozjj0 itct=CAgQ7fkGGA8iEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=8B2T7xuwIPF5xBjR&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGA8iEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FNecV48Z3p5Q%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22NecV48Z3p5Q%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYG6DQWocacwEd_41C4UlVeeqBkNBT0FyQkZ1aF9Hcl9qQVBSa0YwZ3dpeXRDS3RSTkI5dDdDSDZVVWNEanFCdFRhOFpDOS1vUFVrZEZOb1NWZFpUb3VnkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FNecV48Z3p5Q%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f65-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373430 ST-glheo3 itct=CAYQ7fkGGBAiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=J0psGEA1upJ4xllC&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGBAiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FyI9HiiU_uKk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22yI9HiiU_uKk%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYOzvAbPR-ZxFOITqs3cNH6-qBkNBT0FyQkZ0aXVpeXVKZm41RXVvZlpQVkRTc3BJdUkxa01QSW96NlJINnlfX3E0UHpNUG40U0xHX2JoM0JRRkFwcEJ3kAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FyI9HiiU_uKk%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f66-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373437 ST-12bckge itct=CAMQ7fkGGBIiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D&csn=4vV_1lOegrMl-55q&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGBIiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FgawaiMsu0Zk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22gawaiMsu0Zk%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYMwXMTtiEBfWUeebnMW5rQ6qBkNBT0FyQkZ1V1ZZWU04aDFrYnp0bkRTcUl4dTNOUVNSQjFfMXNGMHBZZTFVYnl0TjhHNFh6WnlRSkx2MUVseTlUcFZBkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FgawaiMsu0Zk%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f68-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373438 ST-1jkhrea endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGBMiEwi6uJjSuuGNAxVsC3sEHWhyE7k%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FSXbpN_n8m-I%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22SXbpN_n8m-I%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYKbWvnYJddMIsvmSdJ18XL-qBkNBT0FyQkZ1bmhkcGtpaEQ2Z0liTWdrOGtRSzlkS3hWZ2pzZmU1MTlPRWNrRDRkVFlvclVQT2Q1QzhMUmVVdWh6SGRRkAcCuAfziJrSuuGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FSXbpN_n8m-I%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIuriY0rrhjQMVbAt7BB1ochO5%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjziJrSuuGNAxUwJQYAHcGCBBgqAEoPEg1wZXRlciBtaWxsYXJkogETCOjIk_S54Y0DFfsScwkdb7k37A%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2271307f69-0000-2268-9da0-30fd3812b2b8%22%7D%7D
.youtube.com TRUE / FALSE 1749373443 ST-nb2m8m itct=CBAQ7fkGGAEiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=kqLHN0ZF2OGkLm3a&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBAQ7fkGGAEiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FZgG6vism2Aw%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22ZgG6vism2Aw%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYAw1CRR3NCwwE3361CLOJvOqBkNBT0FyQkZ0WG5VU0JEOHhXZFprUl9QRjdxSkR3MnhpVkZySTlMYjZjZzJpci0tYk5Ud19RdExYTU9LVnJnb0p4QkhnkAcCuAek1qzwu-GNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FZgG6vism2Aw%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBEQsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7281-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373447 ST-2t59nu itct=CA4Q7fkGGAIiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=Rnx9dSvF0eFKbLMW&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA4Q7fkGGAIiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FlVGngRWM-Og%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22lVGngRWM-Og%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYM_IO1MOL0RZzIwxihRKeaCqBkNBT0FyQkZ0OUFTM0dGR08zd3BJa2ZGczF4cm1VdjMzQzZMalltbjhPbE9vOW5wOV9wcXZxc1RaeHBuYW0tMlFYTEVnkAcCuAek1qzwu-GNA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FlVGngRWM-Og%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA8QsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7282-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373448 ST-1c7p0mc itct=CAwQ7fkGGAMiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=Jtcm5CWB7058XCQc&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAwQ7fkGGAMiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F85v0dGiQVRk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%2285v0dGiQVRk%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYMe_NTOrjkNPokqGmrWxK5KqBkNBT0FyQkZzb0FCZ3lhNmxYRE1fMFp3YnBKemc0cFEtbFRKXzMtYW9yZU52a1NkalFyS19sQ2gza3ZsTl95RzN1YXdnkAcCuAek1qzwu-GNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F85v0dGiQVRk%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA0QsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7283-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373450 ST-idmxf6 endpoint=%7B%22clickTrackingParams%22%3A%22CAkQ7fkGGAUiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FDxKQ9RCLnNw%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22DxKQ9RCLnNw%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYA2XG7uOnZ2wXzoZ-o5GiM-qBkNBT0FyQkZzRk1ma1dsNEdkQ0VKUVlnTDhnOEsxcWdoUGw0OEk5MVNDRk1YOU95aEgtN3VYSi1uVnpZZWR1NkZuVC1BkAcCuAek1qzwu-GNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FDxKQ9RCLnNw%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAoQsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7285-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373514 ST-ievrv2 itct=CAcQ7fkGGAYiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=eVxFb0oLCsTQHTXC&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAcQ7fkGGAYiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FaMrmICtRStw%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22aMrmICtRStw%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYOrQSPsD7ZmFVPlotrfvsKWqBkNBT0FyQkZ0RHpyMDZuS0FUWnJTWjhqVll5UWR6ZjNxLWR2UW1vOFJNQW03eThCdkJEaEUyLWJReUNQdmx3Wm9qQ2d3kAcCuAek1qzwu-GNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FaMrmICtRStw%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAgQsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7286-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373539 ST-1kt151p itct=CAMQ7fkGGAgiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=Bh-emeaygL4h4Ylh&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAgiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FmakItjTLGVE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22makItjTLGVE%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYK5oHhRpCX9MwjlYAVafvaSqBkNBT0FyQkZzbVpQeDZTaTUyZFZncW96bzhibXlJQ1FDNmlESnJVRjZyelpQa3NSVzc0RjBhR3cwd1hiWHpjejE3TmJNkAcCuAek1qzwu-GNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FmakItjTLGVE%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7288-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373540 ST-15u3huf itct=CAEQ7fkGGAkiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=zSia6ZtOcX5_olmt&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAkiEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fpky8w9Lq7kU%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22pky8w9Lq7kU%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYKD1zfFWbBYguAn2Mi_o6QGqBkNBT0FyQkZ0MFlrd2xhZTVyWGc3RkVadEhyNFhSQ1RJZ2wtNUdsVnlKRXdhTW9zaTluTjcwLWNnQkRPRUlWU1JyQ0RnkAcCuAek1qzwu-GNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fpky8w9Lq7kU%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7289-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373538 ST-9oozhm itct=CAUQ7fkGGAciEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D&csn=uHpQs2KmLKT8LqC-&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAUQ7fkGGAciEwjqwqvwu-GNAxU9KAYAHVUCDCw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F2vkbvqz_6Kc%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%222vkbvqz_6Kc%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYMdtgUmRRiZdMQvDBRthOSuqBkNBT0FyQkZ0a1YySGhFalFtdmpTYjhpLWNTR0JKYWVXV3NMN09pcEd0TnRpV1ZCam85dHd3cEV3Qkp4MkpKQkVDNlV3kAcCuAek1qzwu-GNA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F2vkbvqz_6Kc%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAYQsLUEIhMI6sKr8LvhjQMVPSgGAB1VAgws%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwik1qzwu-GNAxVW6UIFHRlwFLoqAEoPEg1wZXRlciBtaWxsYXJkogETCLq4mNK64Y0DFWwLewQdaHITuQ%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22734f7287-0000-2255-80fa-14223bc91526%22%7D%7D
.youtube.com TRUE / FALSE 1749373575 ST-1b5cpyt itct=CA0Q7fkGGAAiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D&csn=OETzaPEwWfAPD9d-&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA0Q7fkGGAAiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FwCNCpBOYqF0%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22wCNCpBOYqF0%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYO1abYPdOzt8zF6XDuWXm0SqBkNBT0FyQkZ1dlB0X2U1a2JEVi1ldUFIYW12dTlQdVZ4T1gzQ1NCT0lJb3BVa1ozcUxDN2RsSHNqY05KVTdJcGFZY21BkAcCuAeP8PaevOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FwCNCpBOYqF0%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA4QsLUEIhMI2Yf2nrzhjQMV4Bx7BB3jtBoY%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiP8PaevOGNAxUpxkkHHTgXKCUqAEoPEg1wZXRlciBtaWxsYXJkogETCOrCq_C74Y0DFT0oBgAdVQIMLA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268eefe35-0000-24e3-ad2d-883d24fb65f4%22%7D%7D
.youtube.com TRUE / FALSE 1749373590 ST-9du6nk itct=CAoQ7fkGGAIiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D&csn=hSfQmYbsiB9hYzVv&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGAIiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FP4ZUpacYRXE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22P4ZUpacYRXE%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYLdp9ctwiV8yKkWXfYMhvcCqBkNBT0FyQkZ1VHhMNUNfQkVUUEJnY3lYa25UZGJoRm9NVkVhM0JsNUV2SlJuSjJnVE5TM3lyRlN6WmEta2FsU3lHc3djkAcCuAeP8PaevOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FP4ZUpacYRXE%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMI2Yf2nrzhjQMV4Bx7BB3jtBoY%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiP8PaevOGNAxUpxkkHHTgXKCUqAEoPEg1wZXRlciBtaWxsYXJkogETCOrCq_C74Y0DFT0oBgAdVQIMLA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268eefe3a-0000-24e3-ad2d-883d24fb65f4%22%7D%7D
.youtube.com TRUE / FALSE 1749373591 ST-1g7uji2 itct=CAgQ7fkGGAMiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D&csn=n7btK-BO-U7ao8U2&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGAMiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F30cBkEvGAlU%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%2230cBkEvGAlU%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYKqnutOpHgiy2PCvDcaJ9uqqBkNBT0FyQkZ1V0hHejVGWXNTVlJXcWlaeXVCQ2FvRkJwdE4yQzNfSWNDbEpWeDhMWGc2eGhyNFJ3Sl8zV2k0c01sVlFjkAcCuAeP8PaevOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F30cBkEvGAlU%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMI2Yf2nrzhjQMV4Bx7BB3jtBoY%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiP8PaevOGNAxUpxkkHHTgXKCUqAEoPEg1wZXRlciBtaWxsYXJkogETCOrCq_C74Y0DFT0oBgAdVQIMLA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268eefe3b-0000-24e3-ad2d-883d24fb65f4%22%7D%7D
.youtube.com TRUE / FALSE 1749373595 ST-ogb5ga itct=CAYQ7fkGGAQiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D&csn=Z6GbS9Y3ShW0ZvsO&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAQiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fyk8lmLiD_L0%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22yk8lmLiD_L0%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYDTSa-SQ2JH_YnOPvkYCHPqqBkNBT0FyQkZ0MWVMTjhqajVNZUNiMHA3U1ZlLWQ3LXhrS1h6SFZCaDZlOUwzSnAxcEJlc2FZekV1MEhMLW5CZlZjNE44kAcCuAeP8PaevOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fyk8lmLiD_L0%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMI2Yf2nrzhjQMV4Bx7BB3jtBoY%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiP8PaevOGNAxUpxkkHHTgXKCUqAEoPEg1wZXRlciBtaWxsYXJkogETCOrCq_C74Y0DFT0oBgAdVQIMLA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268eefe3c-0000-24e3-ad2d-883d24fb65f4%22%7D%7D
.youtube.com TRUE / FALSE 1749373596 ST-7lzfni itct=CAMQ7fkGGAYiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D&csn=diXETs-9z8I26dkI&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAYiEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FbW9P3KeWYOs%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22bW9P3KeWYOs%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYISsSGT8aqHy75ssgHPn20qqBkNBT0FyQkZzQmRkd0E1RnNpVVRYSENCRFZLbXRLUXhJSHJuWThpcFVrMDBBcGRLanRVVTRkc2JBT0Fub0FJUUxjc21ZkAcCuAeP8PaevOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FbW9P3KeWYOs%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMI2Yf2nrzhjQMV4Bx7BB3jtBoY%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiP8PaevOGNAxUpxkkHHTgXKCUqAEoPEg1wZXRlciBtaWxsYXJkogETCOrCq_C74Y0DFT0oBgAdVQIMLA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268eefe3e-0000-24e3-ad2d-883d24fb65f4%22%7D%7D
.youtube.com TRUE / FALSE 1749373598 ST-1fhhjyz itct=CAEQ7fkGGAciEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D&csn=ZeOzYF0_dwnthX-l&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAciEwjZh_aevOGNAxXgHHsEHeO0Ghg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FGgyiIwrIwOQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22GgyiIwrIwOQ%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYHPUrbdvGFgFkr5V0hwnUPiqBkNBT0FyQkZ0aG0zVmlHMDhJRHhYdmpyYW9USkF2LWFla2FGZGU1ZUtONDJIaFRVUVliN083cDduYWZSLVVRd3ZoUElzkAcCuAeP8PaevOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FGgyiIwrIwOQ%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI2Yf2nrzhjQMV4Bx7BB3jtBoY%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwiP8PaevOGNAxUpxkkHHTgXKCUqAEoPEg1wZXRlciBtaWxsYXJkogETCOrCq_C74Y0DFT0oBgAdVQIMLA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268eefe3f-0000-24e3-ad2d-883d24fb65f4%22%7D%7D
.youtube.com TRUE / FALSE 1749373599 ST-gzpysm endpoint=%7B%22clickTrackingParams%22%3A%22CBMQ7fkGGAEiEwi854m8vOGNAxUgYkECHVAxM90%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FlhdIKVXeR3I%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22lhdIKVXeR3I%22%2C%22playerParams%22%3A%228AEByAMTwATMxYOk-8X50HOiBhUBdpLKYFllzccT0n6qbCbdmChw_OGqBkNBT0FyQkZ1cThDLUdwV2pUTXo4TGFLU3ptNmRma1ZkUVo4dUtQR1VaQXBfMmlkZm1VZU9EYVk4SURtdVppMTdOZDU4kAcCuAfk9oq8vOGNAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FlhdIKVXeR3I%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBQQsLUEIhMIvOeJvLzhjQMVIGJBAh1QMTPd%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CBQaEwjk9oq8vOGNAxV_6kIFHXkjGyUqAEoPEg1wZXRlciBtaWxsYXJkogETCNmH9p684Y0DFeAcewQd47QaGA%253D%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2273d92739-0000-2150-8c22-ac3eb1583cb8%22%7D%7D
.youtube.com TRUE / FALSE 1749642617 ST-kmqwf1 disableCache=false&itct=CCsQrIEJGAIiEwiur_HQpumNAxWjbHoFHaNKDHM%3D&csn=RsEFxW_TQzTMwiJ6&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCsQrIEJGAIiEwiur_HQpumNAxWjbHoFHaNKDHM%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40GBtrucklady%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCk579PPZcfzD0I0vyXQFttA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40GBtrucklady%22%7D%7D
.youtube.com TRUE / FALSE 1749642622 ST-5gwr2i itct=CC8Q8JMBGAkiEwjG-azTpumNAxW7RnoFHZQVEJY%3D&csn=2YfAbU1Tb9MC5VkU&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CC8Q8JMBGAkiEwjG-azTpumNAxW7RnoFHZQVEJY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40GBtrucklady%2Fvideos%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCk579PPZcfzD0I0vyXQFttA%22%2C%22params%22%3A%22EgZ2aWRlb3PyBgQKAjoA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40GBtrucklady%22%7D%7D
.youtube.com TRUE / FALSE 1749642721 ST-o4ysof itct=CIIGEJQ1GAAiEwjA8bD7pumNAxVIe3oFHRjhDppaGFVDQmNWUXItMDdNSC1wOWUya1JUZEIzQZoBBhDyOBijDA%3D%3D&csn=dHSGtY78500Bnk4g&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CIIGEJQ1GAAiEwjA8bD7pumNAxVIe3oFHRjhDppaGFVDQmNWUXItMDdNSC1wOWUya1JUZEIzQZoBBhDyOBijDA%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3Dme-5Ia0ZvJk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22me-5Ia0ZvJk%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr5---sn-aigl6nzr.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3D99efb921ad19bc99%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D5428750%26mt%3D1749642313%26oweuc%3D%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749642941 ST-1lnl0gt session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749643024 ST-bityqn itct=CCkQrIEJGAQiEwiur_HQpumNAxWjbHoFHaNKDHM%3D&csn=rQGe-lbYGCwVNebj&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCkQrIEJGAQiEwiur_HQpumNAxWjbHoFHaNKDHM%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40jutah%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCBcVQr-07MH-p9e2kRTdB3A%22%2C%22canonicalBaseUrl%22%3A%22%2F%40jutah%22%7D%7D
.youtube.com TRUE / FALSE 1749643070 ST-3opvp5 session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749643100 ST-94qwwt itct=CL8EEIf2BBgBIhMI_dDpl6jpjQMVX-hCBR0ePC-5Wg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHg%3D%3D&csn=wZba5OvQX8u_1rD7&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CL8EEIf2BBgBIhMI_dDpl6jpjQMVX-hCBR0ePC-5Wg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FrJgpecoXlcE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22rJgpecoXlcE%22%2C%22playerParams%22%3A%228AEBoAMByAMkuAQFogYVAXaSymD1f3Eca0bjyPy2xXOYTegFkAcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FrJgpecoXlcE%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CMMEELC1BCITCP3Q6Zeo6Y0DFV_oQgUdHjwvuQ%3D%3D%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUwAg%253D%253D%22%2C%22sequenceProvider%22%3A%22REEL_WATCH_SEQUENCE_PROVIDER_RPC%22%2C%22sequenceParams%22%3A%22CgtySmdwZWNvWGxjRSoCGAVQGWgA%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22695cc4d5-0000-2c1e-8d1d-14223bc7f0f2%22%7D%7D
.youtube.com TRUE / FALSE 1749643156 ST-7v3ayi itct=CB8Q7fkGGAIiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=8WhtGoEQ9SuwGd36&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CB8Q7fkGGAIiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FhKUUlCJPHJE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22hKUUlCJPHJE%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymB9SlilVoJdVi76u3h3xy_QqgZDQU9BckJGdGJZLTkwdXNsTHlNeEF3NHliamFNNTRFYkY2VkQyc3pzU0RGdG4zVWpKSDVQT2VGRjNSdEdVTHFoSnBzZ5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FhKUUlCJPHJE%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CCAQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34af2-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643181 ST-fm5n6h itct=CB0Q7fkGGAMiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=FNmIqTU8lW9xUuem&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CB0Q7fkGGAMiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FJn1C8Kb3-rA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Jn1C8Kb3-rA%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBY8sn73MKK2mXqn4rDTBGsqgZDQU9BckJGdFQycVU4Rnk0dUwyQmFjRWwtV0hkb2lzTDRvaTlCYnhHcUVWa2VrVnBqSVRwSFh3VGU0VDJYVEd0WVdvb5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FJn1C8Kb3-rA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CB4QsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34af3-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643205 ST-l0bcj8 itct=CBoQ7fkGGAUiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=9CRS-p-nVAkzYv70&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBoQ7fkGGAUiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FR2UGQPy_Sjk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22R2UGQPy_Sjk%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymALR9KAN4IGQ9AKf9H0Yt8nqgZDQU9BckJGc1gteDh6SS01SjY2RlcwZmtuMVpleEtwZTgzQWNvWTJWVTJlb3YxU3ZpV3hPMzZGeUlHMlI5R3duQTJkUZAHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FR2UGQPy_Sjk%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBsQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34af5-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643243 ST-bq3o81 itct=CBgQ7fkGGAYiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=LaTmVrivIVDhssjd&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBgQ7fkGGAYiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FLRXox3FFEFc%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22LRXox3FFEFc%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymCSXOfUyvtkXceamXfYYT56qgZDQU9BckJGdEo3Zm11aU1ELUppOVRhYkVEQmM3dS1tOWd6N3MyZGdweVAwME1BaEpKSmx3b3ZUaWUxMzE3WDhYQTVub5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FLRXox3FFEFc%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBkQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34af6-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643252 ST-7f3guv itct=CBUQ7fkGGAgiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=_VBmoQKKE_G9RrW2&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBUQ7fkGGAgiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fbx5NSOU9mCE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22bx5NSOU9mCE%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBJPe1k9ACWzZP0GeuYxO7qqgZDQU9BckJGc181dm1RbU9hV1M1U1hhaTR6endOMDNzaTJwMFZsVEM0Nk9vbzJzSWJFSGNzV0xUeVV4SmNReHYyMmFsVZAHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fbx5NSOU9mCE%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBYQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34af8-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643258 ST-18xyo7a itct=CBMQ7fkGGAkiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=BQcazyMkTiyTA6ZB&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBMQ7fkGGAkiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FRXPApPzudtQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22RXPApPzudtQ%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymDo-ZdhNGm-3vW9iBPafUuGqgZDQU9BckJGdEh5ak54bjJaa1pFQTlPY1Y0ajJ4bjR5UTdPLWN6bDNtd2FpZ1cydUFlclluUjBseDlvSDZtNXJ3RHBSc5AHArgHqaC8uajpjQPwBwI%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FRXPApPzudtQ%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBQQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34af9-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643266 ST-so3w5u itct=CBAQ7fkGGAsiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=5wEAeTDKXW_Jcr72&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBAQ7fkGGAsiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F2CkVpyJXyxM%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%222CkVpyJXyxM%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymDhzxh47G-t-hrq4B9dUo09qgZDQU9BckJGdVJ2TDNSZE5saW5lVVkxZ2w3elRxVGxPZDFmTlBxeVJtdHhsaFhiT1lzcFdPeXNsUDI4RTQwR1dYNFlPa5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F2CkVpyJXyxM%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBEQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34afb-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643330 ST-1l39arz itct=CAsQ7fkGGA4iEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=9c0Dl8W-BIvPR9S1&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAsQ7fkGGA4iEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fu7TvwWEcfd8%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22u7TvwWEcfd8%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymDCKdGvzniDDJZNHXKx0tohqgZDQU9BckJGdU9qQkxwa2RvRlBkZVVDMHQ1R3hINldGOXhjd09GVUl5UkdJNjBlSno3d3JuYXFaR2wzUGxQRWFkTVZqUZAHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fu7TvwWEcfd8%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAwQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34afe-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643329 ST-2h0mu itct=CA4Q7fkGGAwiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=Myfi5x3EooNcWBzX&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA4Q7fkGGAwiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FlI7f5rNY1Eg%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22lI7f5rNY1Eg%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymCb1bfQavcgNN-9xWIYBG_CqgZDQU9BckJGdEswanlRYjlzSjhqcUdGMFhWVlZMSDc5QkZnbTFfTUFHb2ZCQWw5Yl9JbUFTQ0MtRWZEZ01SS3hCQndaY5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FlI7f5rNY1Eg%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA8QsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34afc-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643359 ST-stzcyz itct=CAkQ7fkGGA8iEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=EPRRH9k2qfoHKdto&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAkQ7fkGGA8iEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FbIDY1SNx_4Q%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22bIDY1SNx_4Q%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymDUkrwfXCOldGGSIldaH-TRqgZDQU9BckJGdnZ3ZngxMm5MZ2JwQnM2ZmhxdnhHbzJSOThSNXV0Y3V0Xy1XaGRMdVNCYUVabXJtYlgxQVhvNmlCNGlnd5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FbIDY1SNx_4Q%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAoQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34aff-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643392 ST-stlk8j itct=CAYQ7fkGGBEiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=yne214G9j81i2MW7&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGBEiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F0RuFVmaiK2U%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%220RuFVmaiK2U%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymCE5ZJt531nRd9ITWLxSh8yqgZDQU9BckJGdlR3M25TQlFRZ1BDdE1paWxmQkpaWHhVaDJGc2xYaFN4RFB6bXViRVFEQ05IWkFmRXZQS0NYM1MtTEYyQZAHArgHqaC8uajpjQPwBwI%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F0RuFVmaiK2U%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34b01-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643396 ST-arsigd itct=CAQQ7fkGGBIiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=BtXxTlKSPvAIQsL2&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAQQ7fkGGBIiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FUdaAx7Ozw_w%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22UdaAx7Ozw_w%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBLEcdHcCCxnfi0XVjqt4q1qgZDQU9BckJGdEdZWFBscGhJYVJMODVsaE03bElDTnlidUxMS01WcmFvSGZJTWJDZlo1Wl95RVpyMzlEcWNRNUE4dUxWZ5AHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FUdaAx7Ozw_w%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAUQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34b02-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643397 ST-12qrboo itct=CAEQ7fkGGBQiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D&csn=vw9vbiYoBnUKp_ob&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGBQiEwjB7rq5qOmNAxWaAXsEHXnzKuCaAQUIJBCOHg%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FnEckGlknmbM%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22nEckGlknmbM%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymAgVij9RSpy8gydGyhaRpqoqgZDQU9BckJGdXgzVW5UNnN6LV9MSGZXdW41UUdaWE1DaDY5WG1mZjljTFo5RDFiVWhzcEp3Y0NlX3VpaHc3blJnNm5FOJAHArgHqaC8uajpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FnEckGlknmbM%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIwe66uajpjQMVmgF7BB158yrg%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwipoLy5qOmNAxWPhnwGHWxjC4AqAKIBEwj90OmXqOmNAxVf6EIFHR48L7k%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ac34b04-0000-2379-bc40-582429ae1b78%22%7D%7D
.youtube.com TRUE / FALSE 1749643457 ST-1be7ywn itct=CBUQ7fkGGAMiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=yMKqF_FRdvZe4EG0&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBUQ7fkGGAMiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FcekZXEXSPbk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22cekZXEXSPbk%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBYbC4Y8Ocb3AzHnOHNY5vkqgZDQU9BckJGdEF3X1h3UmZyOW01UGU2aU5TVkVvaVFURjgzcGZxR095cEtHUG9nY2kzX2RRN3dFRXBmellnTFBiRzFac5AHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FcekZXEXSPbk%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBYQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af35-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643461 ST-q1zjg3 itct=CBMQ7fkGGAQiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=uyq8BG2zfVYUIlCN&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBMQ7fkGGAQiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F1E7FmeDiPgs%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%221E7FmeDiPgs%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBn2geo6al3jd4Qcqs5if79qgZDQU9BckJGdWYwOERwdGlaREt6TG1nZWRxcWVtZEJfS2ZqQ2pqYkp1LWhBU0E4REJaTWJfbzBjNTYxU2loWUZvUEFXMJAHArgHiOHDxqnpjQPwBwE%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F1E7FmeDiPgs%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBQQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af36-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643465 ST-q4wbwx itct=CBEQ7fkGGAUiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=OOMSofJA-ARnu2g3&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBEQ7fkGGAUiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FZ5lIuP3vY0g%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Z5lIuP3vY0g%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymAvKHijYB7hDL8oRUULNDwgqgZDQU9BckJGc0FYU0h6N1JnY0xrdlBsektCY2V5SnBOQkRSckFraEVMU3Zlb29UZ01zUDhYX3BSdlFrY3U5al9hdlJmZ5AHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FZ5lIuP3vY0g%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBIQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af37-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643467 ST-6bvyk1 itct=CA8Q7fkGGAYiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=s0t7C3of00eT5G1O&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA8Q7fkGGAYiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fxa_B9WUg2uY%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22xa_B9WUg2uY%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymCQe_aDjKjzGW_LqmSoPew-qgZDQU9BckJGczByeEluc3BFZ2VLU3BTNlZWZ1ZQX2M2X3ZFMlhYbl95cVFvbVhkZUNRV0Y2cml0azAtdG9yWW1TZEh2c5AHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fxa_B9WUg2uY%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBAQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af38-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643468 ST-zaahpy itct=CA0Q7fkGGAciEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=El--Vg-_nIns05EK&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA0Q7fkGGAciEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FSc2pA4AzyaA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Sc2pA4AzyaA%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymA6huQQVdNm9E4VBmY3COIwqgZDQU9BckJGdVZLZmhmTE5rZGtaYk5hNlppTXdia0p1Vld0b3RBS1FTZDB5SGF2Qm1WU0pBYlpRSm13WEJIM0M4YUt6WZAHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FSc2pA4AzyaA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA4QsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af39-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643476 ST-1jj3qwu itct=CAoQ7fkGGAkiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=lK_BMJtZAEhge5sv&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGAkiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FprCejZ2vsIU%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22prCejZ2vsIU%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymDF6PNxfG7YtY95aDb5LeKOqgZDQU9BckJGc3NGWktnTXlfSE9LaGVXQTBvTVJXRndSbThzNVNGT2lyMHg5dTJNaVVBTlN2a1dFSk1rSm83b2liYmphOJAHArgHiOHDxqnpjQPwBwE%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FprCejZ2vsIU%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af3b-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643480 ST-oi00uy itct=CAgQ7fkGGAoiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=HWAy8QT8dC04i9cs&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGAoiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F87CnUkYOy7w%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%2287CnUkYOy7w%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymDrneeUsXGQkClNg3dtLtb2qgZDQU9BckJGc1FHNjBSWUpfSGtSdnl1QVE3WjFaMmxiMURMSHBpY3JCVmNKMU90OXhHSmNPbU5MMkE2MHl2cy1CbkFnNJAHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F87CnUkYOy7w%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af3c-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643509 ST-1q3hyeb itct=CAUQ7fkGGAwiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=yCt2ZZHUUomBXLQn&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAUQ7fkGGAwiEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FYK0V4NvgnHI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22YK0V4NvgnHI%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymAlYVROgcWA2e8dH6-_PJYrqgZDQU9BckJGdkNvR2tnX3FlZy1neE9KT1VhLW9XZG9uRDNMUEJMQzd3UF9LaDRKXzVXWVpvX0xtSm9ocUo2bVMxanNhY5AHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FYK0V4NvgnHI%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAYQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af40-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643550 ST-1393kri itct=CAMQ7fkGGA0iEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=ekF4m2L_Gb8k0Z9w&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGA0iEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FfiwHGaiaQWs%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22fiwHGaiaQWs%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBQWshBKQflCmTkuF0PevyfqgZDQU9BckJGdWJGU3M3RWpwcndjQjJrdHRxaktBNFV1MXBjdFEtSm5oMzFrbnVBOWR5RUc0eGd6RklTeWxoMC1WcG5SMJAHArgHiOHDxqnpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FfiwHGaiaQWs%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af41-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643589 ST-13ym121 itct=CAEQ7fkGGA4iEwjYi8PGqemNAxXcJHsEHQbtBX8%3D&csn=cbDUT3tUITHhiWu8&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGA4iEwjYi8PGqemNAxXcJHsEHQbtBX8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F6tX9Wzcwcvk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%226tX9Wzcwcvk%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymACVYPeBVcJDUFne7kYKNjSqgZDQU9BckJGdnI3R3R4RVAtbUZ5N3hSOUxrU0lmd2lPRmNiUDJGaXQ1UHdlOGxWX2xCU0Q5NDdHR2hVRVNDNlpXTHhGUZAHArgHiOHDxqnpjQPwBwE%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F6tX9Wzcwcvk%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI2IvDxqnpjQMV3CR7BB0G7QV_%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwiI4cPGqemNAxXDYkECHWv_BnYqAKIBEwjB7rq5qOmNAxWaAXsEHXnzKuA%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a99af42-0000-2d06-b930-001a114621fe%22%7D%7D
.youtube.com TRUE / FALSE 1749643617 ST-i9q9wq itct=CBcQ7fkGGAQiEwiJkPSPqumNAxW7AXsEHZ3dKqM%3D&csn=EEcF5Nfv3gmhutFX&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBcQ7fkGGAQiEwiJkPSPqumNAxW7AXsEHZ3dKqM%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fwn6djp_W3u8%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22wn6djp_W3u8%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymCavSRIhsfHwsVGdLa4k_iZqgZDQU9BckJGdVlTeGsyQ2JvM1dXZkV6UXR3R1FMMXFPQjhHRVNWMHBhR2pGMWNrUC0yTmxST1VET3kwRGdyNWdMRk9CY5AHArgH1uv1j6rpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fwn6djp_W3u8%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBgQsLUEIhMIiZD0j6rpjQMVuwF7BB2d3Sqj%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwjW6_WPqumNAxU-yk8IHVAnL40qAKIBEwjYi8PGqemNAxXcJHsEHQbtBX8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a0b6406-0000-2d9d-b4cb-582429a9b058%22%7D%7D
.youtube.com TRUE / FALSE 1749643618 ST-zuvb3i itct=CBUQ7fkGGAUiEwiJkPSPqumNAxW7AXsEHZ3dKqM%3D&csn=di6kbX9-JZtBfWbe&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBUQ7fkGGAUiEwiJkPSPqumNAxW7AXsEHZ3dKqM%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FDFyExjgNZ5Y%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22DFyExjgNZ5Y%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymCtMBAbgmwJ4fPCYLvQGeeGqgZDQU9BckJGczlpaVhlMUZJaHdXRTdnMkQwZThQRTlaWGRmbUJpQXR4Vll4M0ZoaV9NaEVIaU82MXRsS2htSFJPSXotWZAHArgH1uv1j6rpjQPwBwE%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FDFyExjgNZ5Y%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBYQsLUEIhMIiZD0j6rpjQMVuwF7BB2d3Sqj%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwjW6_WPqumNAxU-yk8IHVAnL40qAKIBEwjYi8PGqemNAxXcJHsEHQbtBX8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a0b6407-0000-2d9d-b4cb-582429a9b058%22%7D%7D
.youtube.com TRUE / FALSE 1749643620 ST-vmitts endpoint=%7B%22clickTrackingParams%22%3A%22CBMQ7fkGGAYiEwiJkPSPqumNAxW7AXsEHZ3dKqM%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F908C_cnsF5c%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22908C_cnsF5c%22%2C%22playerParams%22%3A%228AEByAMTwAS09bP_5aPMrLoBogYVAXaSymBZixJ9Kd6UmilX13q5iPq5qgZDQU9BckJGdnowNDVVSngtcnBjMGwwUmtrU3czOGpya3hDY1VDVG9JQ3ZXTDhWZ0Y5UUlDUnZCMVVad29lMzRtZVBDc5AHArgH1uv1j6rpjQM%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F908C_cnsF5c%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBQQsLUEIhMIiZD0j6rpjQMVuwF7BB2d3Sqj%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUaEwjW6_WPqumNAxU-yk8IHVAnL40qAKIBEwjYi8PGqemNAxXcJHsEHQbtBX8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a0b6408-0000-2d9d-b4cb-582429a9b058%22%7D%7D
.youtube.com TRUE / FALSE 1749810929 ST-m8gbol disableCache=false&itct=CCwQrIEJGAEiEwiy6M3Sme6NAxWtHwYAHeRGHvw%3D&csn=MWPLc8bGUj2ZB8_6&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCwQrIEJGAEiEwiy6M3Sme6NAxWtHwYAHeRGHvw%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%402vintage%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCnoEKvDdh9AeRBlG1Pf3v8Q%22%2C%22canonicalBaseUrl%22%3A%22%2F%402vintage%22%7D%7D
.youtube.com TRUE / FALSE 1749810950 ST-19re4kl itct=CIsDEJQ1GAAiEwiLj97Ume6NAxXAYXoFHRZqGtZaGFVDbm9FS3ZEZGg5QWVSQmxHMVBmM3Y4UZoBBhDyOBijDA%3D%3D&csn=AY3kJ9vNyMzGjSq9&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CIsDEJQ1GAAiEwiLj97Ume6NAxXAYXoFHRZqGtZaGFVDbm9FS3ZEZGg5QWVSQmxHMVBmM3Y4UZoBBhDyOBijDA%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3DqfBLM2unCDI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22qfBLM2unCDI%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr3---sn-aigl6ns6.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3Da9f04b336ba70832%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D4513750%26mt%3D1749810552%26oweuc%3D%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749811030 ST-p38c68 itct=CNADEIf2BBgBIhMIu6Ps3pnujQMVi0pBAh1NyBpFmgEFCCUQ-B0%3D&csn=DImSmkLIXPgH1cMv&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CNADEIf2BBgBIhMIu6Ps3pnujQMVi0pBAh1NyBpFmgEFCCUQ-B0%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F6ixKH6pmbqI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%226ixKH6pmbqI%22%2C%22playerParams%22%3A%228AEBoAMCyAMluAQGogYVAXaSymBU4EPXFki7bL1rZb-Qlq7akAcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F6ixKH6pmbqI%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CNUDELC1BCITCLuj7N6Z7o0DFYtKQQIdTcgaRQ%3D%3D%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYwAg%253D%253D%22%2C%22sequenceProvider%22%3A%22REEL_WATCH_SEQUENCE_PROVIDER_RPC%22%2C%22sequenceParams%22%3A%22Cgs2aXhLSDZwbWJxSSoCGAZQGWgA%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226b4ffd40-0000-284d-b2a6-3c286d454ed2%22%7D%7D
.youtube.com TRUE / FALSE 1749811090 ST-1d4z8nq itct=CAsQ7fkGGAIiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D&csn=C_dO14zUPvMRHNVB&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAsQ7fkGGAIiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FGd4YwX0wpvo%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Gd4YwX0wpvo%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYPyV_tCaK_pLnGnTnmM6YUGqBkNBT0FyQkZzZXJDdWJTM0FPb2tvaHdsc3BEclNTUnBLajl5OHhaTzZIc0EwVVp5dHRoRUs1RWtIbVFzRDExTGRKbTNRkAcCuAfL99aEmu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FGd4YwX0wpvo%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAwQsLUEIhMIwKnWhJrujQMVaiQGAB0wHTZt%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjL99aEmu6NAxV2JwYAHYfvNfEqAKIBEwi7o-zeme6NAxWLSkECHU3IGkU%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a502cc8-0000-2d30-8637-14223bc661be%22%7D%7D
.youtube.com TRUE / FALSE 1749811137 ST-j6mrhr itct=CAkQ7fkGGAMiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D&csn=-ry0cs3O-OJnLFoU&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAkQ7fkGGAMiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FyTTmVMZ_leA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22yTTmVMZ_leA%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYBlMrg3iFb2kD-n0_CG4NlSqBkNBT0FyQkZ1Nm9mcDl3WTdQOUZ5SlpMQnRTd0JROW5nNVRaMThFY3RYMUxNdjBodzNVelEyMlpoTEtETWVVekN0WEZJkAcCuAfL99aEmu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FyTTmVMZ_leA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAoQsLUEIhMIwKnWhJrujQMVaiQGAB0wHTZt%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjL99aEmu6NAxV2JwYAHYfvNfEqAKIBEwi7o-zeme6NAxWLSkECHU3IGkU%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a502cc9-0000-2d30-8637-14223bc661be%22%7D%7D
.youtube.com TRUE / FALSE 1749811253 ST-1simi12 itct=CAYQ7fkGGAUiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D&csn=WxMKcBf9dQhQqZco&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAUiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FCGWsfHtY3NQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22CGWsfHtY3NQ%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYO9bYD7aPqwWe9ho8ZonzmCqBkNBT0FyQkZ2U21oZ1ZMLWt1emxvS1Rmblo1Vl9UQmFIemxYU3g5R3hGUVJJU2dpbEtucWNfVW5rZzd6ME9pWDJBcmZrkAcCuAfL99aEmu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FCGWsfHtY3NQ%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIwKnWhJrujQMVaiQGAB0wHTZt%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjL99aEmu6NAxV2JwYAHYfvNfEqAKIBEwi7o-zeme6NAxWLSkECHU3IGkU%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a502ccb-0000-2d30-8637-14223bc661be%22%7D%7D
.youtube.com TRUE / FALSE 1749811276 ST-vevd3y itct=CAQQ7fkGGAYiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D&csn=X1YRczarASGMtZOa&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAQQ7fkGGAYiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fuswse_5E8BU%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22uswse_5E8BU%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYEGOg0aMSZBeY1tYVJBw3tyqBkNBT0FyQkZzeE5zd2N3MWNzOGNJWVlIWGFnN1FGNllXaHlad1FBTGpxMjkzM2V6M3hueDk2TUhwZjJpSFRSVlpvd0JBkAcCuAfL99aEmu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fuswse_5E8BU%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAUQsLUEIhMIwKnWhJrujQMVaiQGAB0wHTZt%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjL99aEmu6NAxV2JwYAHYfvNfEqAKIBEwi7o-zeme6NAxWLSkECHU3IGkU%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a502ccc-0000-2d30-8637-14223bc661be%22%7D%7D
.youtube.com TRUE / FALSE 1749811402 ST-mbeo3q itct=CAEQ7fkGGAgiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D&csn=dJTxLc6nZxsRFv6i&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAgiEwjAqdaEmu6NAxVqJAYAHTAdNm2aAQUIJRD4HQ%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FiuLMA30NDks%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22iuLMA30NDks%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYLdC1SM6kZWlcjomHJ6PYheqBkNBT0FyQkZ2RzhQQnJZS2hLS2E0c3BidFh0Z295UnVxQ1Y0c21QamNpREtreVNmaW0zNDd2THppMkVBNkphZjV3MkpvkAcCuAfL99aEmu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FiuLMA30NDks%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIwKnWhJrujQMVaiQGAB0wHTZt%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjL99aEmu6NAxV2JwYAHYfvNfEqAKIBEwi7o-zeme6NAxWLSkECHU3IGkU%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a502cce-0000-2d30-8637-14223bc661be%22%7D%7D
.youtube.com TRUE / FALSE 1749811428 ST-ccn68f itct=CAgQ7fkGGAMiEwi4wo_6mu6NAxUJfUECHTF8FLY%3D&csn=hbjjXGJ57SKpPR6F&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGAMiEwi4wo_6mu6NAxUJfUECHTF8FLY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FJsj3fbsb5gg%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Jsj3fbsb5gg%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYLIqGufTtFd_oZvGcfYQ4ZuqBkNBT0FyQkZ1OHduSTBYdzJqN2FzVXBBbjFTaDFHOXUxUDlMU2NRdEJXU2hFUzA5c1ltV2F4UEJLRDhTYW9OeUJUeUpnkAcCuAezk5D6mu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FJsj3fbsb5gg%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMIuMKP-prujQMVCX1BAh0xfBS2%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwizk5D6mu6NAxVgJQYAHbHcJuMqAKIBEwjAqdaEmu6NAxVqJAYAHTAdNm0%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268b43945-0000-2c31-9d1a-582429ced358%22%7D%7D
.youtube.com TRUE / FALSE 1749811431 ST-gptv5w itct=CAYQ7fkGGAQiEwi4wo_6mu6NAxUJfUECHTF8FLY%3D&csn=VnpPBCi4swIdGhta&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAQiEwi4wo_6mu6NAxUJfUECHTF8FLY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F2vyS3gG1g5w%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%222vyS3gG1g5w%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYIGFi6A6skiYOZRk7bGvA8SqBkNBT0FyQkZzYUhvdVFPMk5uTGpha283dHRZUGkwRUZ5eXFObFlBa0JRcEtMLWtOUklRVGwzNDNzOXFqSW1oeEszcWdFkAcCuAezk5D6mu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F2vyS3gG1g5w%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIuMKP-prujQMVCX1BAh0xfBS2%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwizk5D6mu6NAxVgJQYAHbHcJuMqAKIBEwjAqdaEmu6NAxVqJAYAHTAdNm0%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268b43946-0000-2c31-9d1a-582429ced358%22%7D%7D
.youtube.com TRUE / FALSE 1749811491 ST-1nvblsu itct=CAMQ7fkGGAYiEwi4wo_6mu6NAxUJfUECHTF8FLY%3D&csn=uIkV2cLnq4KB6qk1&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAYiEwi4wo_6mu6NAxUJfUECHTF8FLY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FaHLPq10R7jg%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22aHLPq10R7jg%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYCP1GSP_MpxbAzoU64_fLjKqBkNBT0FyQkZzVzQ3OHBPLV9KZ1RmbnBSSXV6MUFtLW9CcnJUTDloNUZiaURIMnA2SVZEbV80bkxiZzFEXzVUSHJYelhnkAcCuAezk5D6mu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FaHLPq10R7jg%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMIuMKP-prujQMVCX1BAh0xfBS2%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwizk5D6mu6NAxVgJQYAHbHcJuMqAKIBEwjAqdaEmu6NAxVqJAYAHTAdNm0%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268b43948-0000-2c31-9d1a-582429ced358%22%7D%7D
.youtube.com TRUE / FALSE 1749811608 ST-1m26xie itct=CAEQ7fkGGAciEwi4wo_6mu6NAxUJfUECHTF8FLY%3D&csn=PI-6ELSu4TRgH7fT&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAciEwi4wo_6mu6NAxUJfUECHTF8FLY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FfaRW9umlURg%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22faRW9umlURg%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYBZB-_tX43IoCntQL5rsTVaqBkNBT0FyQkZ0Rk9rT2treVJVdG5aMDc4Um83ekdUNkx1Q3dvX0V5WlVZb25TaFpPWGdBZi1XZ2podmlmTkwwOTdHbThjkAcCuAezk5D6mu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FfaRW9umlURg%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIuMKP-prujQMVCX1BAh0xfBS2%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwizk5D6mu6NAxVgJQYAHbHcJuMqAKIBEwjAqdaEmu6NAxVqJAYAHTAdNm0%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2268b43949-0000-2c31-9d1a-582429ced358%22%7D%7D
.youtube.com TRUE / FALSE 1749811668 ST-19p5wsm itct=CAgQ7fkGGAQiEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D&csn=2JfzciTI6BftVCPJ&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGAQiEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FWCqgBcCzyaQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22WCqgBcCzyaQ%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYA6n12EhDYpL0hDuFsaj1eaqBkNBT0FyQkZ1d3A0QVZkVFJSM1J0WUlVbVhWS3dCbUdOTzRtZm5lUlV0ajVNNEdSeGlZdWl2dEhPaHJfMnFDQ2lKSG4wkAcCuAfujpvgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FWCqgBcCzyaQ%2Fframe0.jpg%22%2C%22width%22%3A576%2C%22height%22%3A1024%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMIv5ia4JvujQMVNwd7BB1TMyqS%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjujpvgm-6NAxUVRXoFHXYzDH8qAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269b2b5b7-0000-2353-8cba-001a114ce73a%22%7D%7D
.youtube.com TRUE / FALSE 1749811670 ST-nm13n6 itct=CAYQ7fkGGAUiEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D&csn=Bv4eRdGs21mGnMWH&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAUiEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FBm2qz41qn2c%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Bm2qz41qn2c%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYCJyi6X9meevGmAvJ8n61kaqBkNBT0FyQkZ2Tk9RSnhCUG1RaHNLbEN1Nmk5dWN1czlmSTZvaHh6M3VEeFM2VEFWZk9uVU96RzFpeDJyS3JTUHB3NHhvkAcCuAfujpvgm-6NA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FBm2qz41qn2c%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIv5ia4JvujQMVNwd7BB1TMyqS%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjujpvgm-6NAxUVRXoFHXYzDH8qAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269b2b5b8-0000-2353-8cba-001a114ce73a%22%7D%7D
.youtube.com TRUE / FALSE 1749811671 ST-dchifo itct=CAMQ7fkGGAciEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D&csn=0qsvS3sYwYLkJy2T&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAciEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FGQWYQuusAmw%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22GQWYQuusAmw%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYP9BWL3mt4aq1s7eu1xPlMSqBkNBT0FyQkZ1OEpXRkdEYm55NmJXRU53dFBsb2YtSGdnZ1VuZ2VseFl2WEpFdkxPN2JYSVFWbjN1WWZKWVN4MDhyMmN3kAcCuAfujpvgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FGQWYQuusAmw%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMIv5ia4JvujQMVNwd7BB1TMyqS%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjujpvgm-6NAxUVRXoFHXYzDH8qAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269b2b5ba-0000-2353-8cba-001a114ce73a%22%7D%7D
.youtube.com TRUE / FALSE 1749811674 ST-hilr9m itct=CAEQ7fkGGAgiEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D&csn=yvn3wZtCZz6qQzV4&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAgiEwi_mJrgm-6NAxU3B3sEHVMzKpI%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FMGC1BzQwC-c%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22MGC1BzQwC-c%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYHbBYqdFop4kLM3Tkpzm6C-qBkNBT0FyQkZ1MFlVVS1vSzIxWUlqVkxZZG9iV2NZX2ZrWnhYWm1KMnZ3cXItRWlwMnV0LS1QS1pwTnk1bFc4b1dxbUJFkAcCuAfujpvgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FMGC1BzQwC-c%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIv5ia4JvujQMVNwd7BB1TMyqS%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjujpvgm-6NAxUVRXoFHXYzDH8qAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269b2b5bb-0000-2353-8cba-001a114ce73a%22%7D%7D
.youtube.com TRUE / FALSE 1749811676 ST-14vcnwh itct=CAsQ7fkGGAQiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D&csn=gpss2BBIN2-eVhWQ&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAsQ7fkGGAQiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F840KNF8D9co%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22840KNF8D9co%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYKxDY1Q1wvoG7ILK28yIScWqBkNBT0FyQkZ1YTQtdUFRWTFSUWh4NDNiVGxveDRCbHJ5RGRIMlh4Q05uRnBYdGFnblp6RXB0QXh3RlJNRVBEWDdjaktBkAcCuAe0u9Xgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F840KNF8D9co%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAwQsLUEIhMI1OXU4JvujQMVP2V6BR1T2ztc%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi0u9Xgm-6NAxX00UIFHbzJEuoqAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226b2fa594-0000-2b53-b60f-2405887ef404%22%7D%7D
.youtube.com TRUE / FALSE 1749811677 ST-1b2beav itct=CAkQ7fkGGAUiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D&csn=CUMHMJv4hc8wYshF&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAkQ7fkGGAUiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fu2NGib4dHFM%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22u2NGib4dHFM%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYHnlo_htyY0xq8XohPltqJSqBkNBT0FyQkZ2bkR4TkxJLXdsZk9KSUJXNGFGaXNPOEVSTFhIb2pIUVV2a3dXaF9BVXN6TWNDY1FxeUVEX0tfQm5yVUQ0kAcCuAe0u9Xgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fu2NGib4dHFM%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAoQsLUEIhMI1OXU4JvujQMVP2V6BR1T2ztc%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi0u9Xgm-6NAxX00UIFHbzJEuoqAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226b2fa595-0000-2b53-b60f-2405887ef404%22%7D%7D
.youtube.com TRUE / FALSE 1749811687 ST-z22di1 itct=CAYQ7fkGGAciEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D&csn=dne15L9VARUJ8KoQ&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAciEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FnW8iIy1y1nQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22nW8iIy1y1nQ%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYMpMFAb95TgTMHgQeaGrHouqBkNBT0FyQkZ2Y21PelpabWphQ2g0MEtreUh4WFJiZUphc19aOWo0U01XcVBXako3MUJ5R2RTa0VlZk5yTklzR3BvVnQwkAcCuAe0u9Xgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FnW8iIy1y1nQ%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMI1OXU4JvujQMVP2V6BR1T2ztc%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi0u9Xgm-6NAxX00UIFHbzJEuoqAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226b2fa59c-0000-2b53-b60f-2405887ef404%22%7D%7D
.youtube.com TRUE / FALSE 1749811725 ST-hwdyct itct=CAQQ7fkGGAgiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D&csn=PFHBHq6KjLO5pjrh&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAQQ7fkGGAgiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fu_yXmdOwX1w%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22u_yXmdOwX1w%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYDt5RSUsN7JqZuhJ8NmZeuOqBkNBT0FyQkZ1RDl2RlAxQ216anNuR1pydHlVTTY2YWtzQXZEdXlzOHY5UHAzbW5OMldFQUhsVGRhMjctSXVsSzBST3VZkAcCuAe0u9Xgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fu_yXmdOwX1w%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAUQsLUEIhMI1OXU4JvujQMVP2V6BR1T2ztc%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi0u9Xgm-6NAxX00UIFHbzJEuoqAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226b2fa59f-0000-2b53-b60f-2405887ef404%22%7D%7D
.youtube.com TRUE / FALSE 1749811742 ST-1cylpzt itct=CAEQ7fkGGAoiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D&csn=5m5IC_O_nn6pq_W5&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAoiEwjU5dTgm-6NAxU_ZXoFHVPbO1w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fwait0Pxf-yQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22wait0Pxf-yQ%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYGrbH77mSgFjmOxiDCE0oXCqBkNBT0FyQkZzVFhCeHZlNUNDcy1PQXV1bkF6RjVlQ0FjeXgwdFkzVmNEQVdqTkNJMGtUdko2UmF3V2hxQWV5d3RUVnZvkAcCuAe0u9Xgm-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fwait0Pxf-yQ%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI1OXU4JvujQMVP2V6BR1T2ztc%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi0u9Xgm-6NAxX00UIFHbzJEuoqAKIBEwi4wo_6mu6NAxUJfUECHTF8FLY%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226b2fa5a6-0000-2b53-b60f-2405887ef404%22%7D%7D
.youtube.com TRUE / FALSE 1749811972 ST-odg2gx itct=CBAQ7fkGGAMiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=8-eTqG0wgC-RZczn&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CBAQ7fkGGAMiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FjTs_OILkxDE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22jTs_OILkxDE%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYPB8yUPCW2LputD1p0dk0f6qBkNBT0FyQkZ1V2VraUlSeGdVTnRsZVRpcDRabVFuUjZsMjFuWmdnYXdHVVVKRjMxZGdla0FqTi1jdGUyVHlZZzdNUHBzkAcCuAe1_5_QnO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FjTs_OILkxDE%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBEQsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d90775-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749811997 ST-pytcd3 itct=CA4Q7fkGGAQiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=y-CXHtW2j-CglqxE&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA4Q7fkGGAQiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FGqdALdm2Obw%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22GqdALdm2Obw%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYLOB9qmUyiCqj3FzcGLCf3aqBkNBT0FyQkZ0NUs1bUFZcTcwWjY1VGVPaWQxUlJKWE42bFZ6RTZ5LVpuSEdhN0VoOU1Pd0RVbzZ4YjFVRFlyXzRBbUtvkAcCuAe1_5_QnO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FGqdALdm2Obw%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA8QsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d90776-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812018 ST-9dpn0y itct=CAwQ7fkGGAUiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=FA8Q5XzGjHaqErvS&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAwQ7fkGGAUiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FKWkbC2dvVHQ%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22KWkbC2dvVHQ%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYFwexsxYcUaCthHN0jffgUSqBkNBT0FyQkZ2M1hIaUp2aGYtLXdKRjIxU0drMmw0YVhSSkJ4Z19veUh3aEUwTkJNclR5VEZINzM2UWRlaGtob0pKdDVVkAcCuAe1_5_QnO6NA_AHAg%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FKWkbC2dvVHQ%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA0QsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d90777-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812042 ST-10nj9cd itct=CAoQ7fkGGAYiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=GJTC2x03F0tsfN1z&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGAYiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FWvG-wsjAoQ0%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22WvG-wsjAoQ0%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYOj5pHAnspqKH7OAR3C-VZeqBkNBT0FyQkZ2U2d4eFkxeDFsUk5IUlJEYVkxM3ZpN2tld1pjejVIRXJIRkZCYXpuYlU5MW80VUhfUkJfY0NFOEVBRjZVkAcCuAe1_5_QnO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FWvG-wsjAoQ0%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d90778-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812048 ST-1gsuta8 itct=CAcQ7fkGGAgiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=rK9SbwY9U2Xb_8-_&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAcQ7fkGGAgiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F3LEa6LIRGWM%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%223LEa6LIRGWM%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYFqMMELfnw5euykX66v19xGqBkNBT0FyQkZ0TEVxcnR0d29UWDhfdFo5Sk5CWkVQT1lPNWtTaGd4Zmg4NHhDUERZQ2dwSlJOOGNha2NQTDZQN0pweTRnkAcCuAe1_5_QnO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F3LEa6LIRGWM%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAgQsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d9077a-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812111 ST-1uc374s itct=CAUQ7fkGGAkiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=YiAFOyWdVog8F4IE&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAUQ7fkGGAkiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F3NJmZW_7x9Y%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%223NJmZW_7x9Y%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYHyMnFNuhYyT231RFxK9o12qBkNBT0FyQkZ1LXVvZEdTUmg0QVZtT04xWE02eTVuVWJ4VXhmU05Rcm1zaHdZcmthVWtjOVR4M1diYm80b3loZXotN0VzkAcCuAe1_5_QnO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F3NJmZW_7x9Y%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAYQsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d9077b-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812159 ST-rdnhu5 itct=CAMQ7fkGGAoiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=IDAqw1S7vfHZjQ_3&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAoiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FZ54Yg8iZJiw%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Z54Yg8iZJiw%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYFw6hdA1-2SWEMNQQCT56nmQBwK4B7X_n9Cc7o0D8AcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FZ54Yg8iZJiw%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d9077c-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812162 ST-p88mi3 itct=CAEQ7fkGGAsiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D&csn=ikdS1kHFqFqz4COF&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAsiEwi2hp7QnO6NAxWcO3sEHaTjHy8%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FEg_2EwaSZVI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Eg_2EwaSZVI%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYCWbn0WBSKSO69E4fRYOtiKqBkNBT0FyQkZ0d2ZwQ0NwM0FLTzhNUnUtUk02N0dBTldiODh1eE5zc3VCMmxKNmNRNEdyekNQelFmLTdRU094S1RXSXljkAcCuAe1_5_QnO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FEg_2EwaSZVI%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMItoae0JzujQMVnDt7BB2k4x8v%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi1_5_QnO6NAxUS3E8IHSlFBa4qAKIBEwjU5dTgm-6NAxU_ZXoFHVPbO1w%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%2269d9077d-0000-23a4-b9d1-d4f547f31ef4%22%7D%7D
.youtube.com TRUE / FALSE 1749812183 ST-wqezwn itct=CAwQ7fkGGAMiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D&csn=LbzaeivYbrvcHj44&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAwQ7fkGGAMiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F14U3F9SXjq0%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%2214U3F9SXjq0%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYN0LYw1SrXmcDP-vlLY0fquQBwK4B7eWi5-e7o0D8AcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F14U3F9SXjq0%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA0QsLUEIhMI6amKn57ujQMVmnt6BR3ZUQ8Y%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi3loufnu6NAxXmWU8EHZEJE3QqAKIBEwi2hp7QnO6NAxWcO3sEHaTjHy8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ad36d9a-0000-21d9-9667-34c7e9139b9b%22%7D%7D
.youtube.com TRUE / FALSE 1749812186 ST-kwtrkl itct=CAoQ7fkGGAQiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D&csn=2nTd7s2PnZSwmF81&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGAQiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FivhUt5dEhAs%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22ivhUt5dEhAs%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYC340w_b86MO-mGPNiA7giiqBkNBT0FyQkZzV002d0NtRE9IQVNoSmJ6THpJejI3Y2tsVVBuZm1uTFlzbl92UU56SHYydEFFS0hGRGt3T3h1R1Y4b3RrkAcCuAe3loufnu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FivhUt5dEhAs%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMI6amKn57ujQMVmnt6BR3ZUQ8Y%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi3loufnu6NAxXmWU8EHZEJE3QqAKIBEwi2hp7QnO6NAxWcO3sEHaTjHy8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ad36d9b-0000-21d9-9667-34c7e9139b9b%22%7D%7D
.youtube.com TRUE / FALSE 1749812354 ST-ighdnt itct=CAcQ7fkGGAYiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D&csn=IMv9V00pWfyWNrwx&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAcQ7fkGGAYiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FH-0PHnE-i-I%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22H-0PHnE-i-I%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYMGqfJ0dMVdNW90CGfRPHDeqBkNBT0FyQkZ0aGVBX0FaVzVudEM3ZnlhVWtvTVBwLXhkcnpoMjdZZkw5cGRDM3ZzelN6bzAxTEdNc3lEY1ZCdXBsXzZFkAcCuAe3loufnu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FH-0PHnE-i-I%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAgQsLUEIhMI6amKn57ujQMVmnt6BR3ZUQ8Y%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi3loufnu6NAxXmWU8EHZEJE3QqAKIBEwi2hp7QnO6NAxWcO3sEHaTjHy8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ad36d9d-0000-21d9-9667-34c7e9139b9b%22%7D%7D
.youtube.com TRUE / FALSE 1749812357 ST-frohdv itct=CAUQ7fkGGAciEwjpqYqfnu6NAxWae3oFHdlRDxg%3D&csn=SCXnC5t5Om8yUoU4&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAUQ7fkGGAciEwjpqYqfnu6NAxWae3oFHdlRDxg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FKPHG7zTWetE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22KPHG7zTWetE%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYO2RsD5N2joDMr16evLAp6mqBkNBT0FyQkZzMmRwdnJ6MkhaR0h3LVVCb1ltcmg2TjBZdGZ4RllWVlJpa09kcVlyeExPRUloc040MlRoTDd5aFNNeVZBkAcCuAe3loufnu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FKPHG7zTWetE%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAYQsLUEIhMI6amKn57ujQMVmnt6BR3ZUQ8Y%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi3loufnu6NAxXmWU8EHZEJE3QqAKIBEwi2hp7QnO6NAxWcO3sEHaTjHy8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ad36d9e-0000-21d9-9667-34c7e9139b9b%22%7D%7D
.youtube.com TRUE / FALSE 1749812359 ST-c8wzqn itct=CAMQ7fkGGAgiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D&csn=gVETebLCatZOZ7Tq&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAgiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F3P-IYOBWApk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%223P-IYOBWApk%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYBvnn6vVjha0KMivpaVXgdGQBwK4B7eWi5-e7o0D8AcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F3P-IYOBWApk%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMI6amKn57ujQMVmnt6BR3ZUQ8Y%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi3loufnu6NAxXmWU8EHZEJE3QqAKIBEwi2hp7QnO6NAxWcO3sEHaTjHy8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ad36d9f-0000-21d9-9667-34c7e9139b9b%22%7D%7D
.youtube.com TRUE / FALSE 1749812360 ST-13rr2js itct=CAEQ7fkGGAkiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D&csn=mKJ0oWLj87BOFbQl&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAkiEwjpqYqfnu6NAxWae3oFHdlRDxg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FXG8HMIsStwo%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22XG8HMIsStwo%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYOr6rGQB41EeZrn5ZVdgPRGqBkNBT0FyQkZ2YkhlaEpiNlhmbmgyX3A4R0hSbUE5R0JfWDFSRGZ0N1N5LV9MVmVSQUkyMzdua1F1NmdXakJBU1BNRWVjkAcCuAe3loufnu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FXG8HMIsStwo%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI6amKn57ujQMVmnt6BR3ZUQ8Y%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwi3loufnu6NAxXmWU8EHZEJE3QqAKIBEwi2hp7QnO6NAxWcO3sEHaTjHy8%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226ad36da0-0000-21d9-9667-34c7e9139b9b%22%7D%7D
.youtube.com TRUE / FALSE 1749812380 ST-okba0e itct=CA0Q7fkGGAMiEwivitX-nu6NAxVhIHMJHRV2Heo%3D&csn=rCLxy7KiO3gGyGPW&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA0Q7fkGGAMiEwivitX-nu6NAxVhIHMJHRV2Heo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FLF02RoV5lq4%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22LF02RoV5lq4%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYD7ysxSDSk7qeELMr2hHVDqQBwK4B4ff1v6e7o0D8AcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FLF02RoV5lq4%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA4QsLUEIhMIr4rV_p7ujQMVYSBzCR0Vdh3q%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiH39b-nu6NAxXTofQHHc0VHBEqAKIBEwjpqYqfnu6NAxWae3oFHdlRDxg%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226990399e-0000-2615-9d26-9898fb8699f5%22%7D%7D
.youtube.com TRUE / FALSE 1749812384 ST-1khw8dj itct=CAsQ7fkGGAQiEwivitX-nu6NAxVhIHMJHRV2Heo%3D&csn=BzGerOYkjCBQ9Dki&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAsQ7fkGGAQiEwivitX-nu6NAxVhIHMJHRV2Heo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FIcvlFdGsOjE%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22IcvlFdGsOjE%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYI7Zbo1T1sxnn5jD69__YcSqBkNBT0FyQkZ1ZHVWXzAtQVQ0b0xwcEtobzlCbVB2aXEydWh2WUZvWlh1R1J2OE9ldnBwbV9Gb1RKdXRaLVpab0hpY2ZnkAcCuAeH39b-nu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FIcvlFdGsOjE%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAwQsLUEIhMIr4rV_p7ujQMVYSBzCR0Vdh3q%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiH39b-nu6NAxXTofQHHc0VHBEqAKIBEwjpqYqfnu6NAxWae3oFHdlRDxg%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226990399f-0000-2615-9d26-9898fb8699f5%22%7D%7D
.youtube.com TRUE / FALSE 1749812385 ST-oh1x68 endpoint=%7B%22clickTrackingParams%22%3A%22CAgQ7fkGGAYiEwivitX-nu6NAxVhIHMJHRV2Heo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FHIhkNEpwYsU%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22HIhkNEpwYsU%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYAG7Oyx2K7SvO8-tPwI-ygKqBkNBT0FyQkZ2ejlNLXJhclJvcHIySG5UTFpsQ0lwN1l1aXA3SmhSTzE0QUxwbjBneTJmaWZSRzZka0ZhM2MxZjhlTDA0kAcCuAeH39b-nu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FHIhkNEpwYsU%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAkQsLUEIhMIr4rV_p7ujQMVYSBzCR0Vdh3q%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiH39b-nu6NAxXTofQHHc0VHBEqAKIBEwjpqYqfnu6NAxWae3oFHdlRDxg%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22699039a7-0000-2615-9d26-9898fb8699f5%22%7D%7D
.youtube.com TRUE / FALSE 1749812426 ST-31bk8w itct=CAYQ7fkGGAciEwivitX-nu6NAxVhIHMJHRV2Heo%3D&csn=n3LphWj5wlUkWS_1&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAciEwivitX-nu6NAxVhIHMJHRV2Heo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FzVze7BY2mQ8%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22zVze7BY2mQ8%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYKV67H0YDfdQXFwEYmyu5IuqBkNBT0FyQkZ2TUdYemVtamZQbloxZ185d2dRbjdheWhDMHFiVHgyNXVEelhXU0hrMS1rdnFYOGZNcjBTYXBoZ1p1VU5vkAcCuAeH39b-nu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FzVze7BY2mQ8%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIr4rV_p7ujQMVYSBzCR0Vdh3q%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiH39b-nu6NAxXTofQHHc0VHBEqAKIBEwjpqYqfnu6NAxWae3oFHdlRDxg%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22699039a8-0000-2615-9d26-9898fb8699f5%22%7D%7D
.youtube.com TRUE / FALSE 1749812435 ST-lgxx6f itct=CAMQ7fkGGAkiEwivitX-nu6NAxVhIHMJHRV2Heo%3D&csn=ojJrIIjUPgFkTlAY&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAkiEwivitX-nu6NAxVhIHMJHRV2Heo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fcl6HNHa0P9M%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22cl6HNHa0P9M%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYOKe8nt9ZJr8PWtKGAsrBzyqBkNBT0FyQkZzYjJBTkhLYVNWbDluZDRCcHpYbXVZSXBqNHpYWkxub1dSN1Zmai1KNUtOQmhkeVYzWkxxLVVwU2RVVXQ4kAcCuAeH39b-nu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fcl6HNHa0P9M%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMIr4rV_p7ujQMVYSBzCR0Vdh3q%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiH39b-nu6NAxXTofQHHc0VHBEqAKIBEwjpqYqfnu6NAxWae3oFHdlRDxg%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22699039aa-0000-2615-9d26-9898fb8699f5%22%7D%7D
.youtube.com TRUE / FALSE 1749812438 ST-s3b9kb itct=CAEQ7fkGGAoiEwivitX-nu6NAxVhIHMJHRV2Heo%3D&csn=6aRIyIIchuDMyjaC&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAoiEwivitX-nu6NAxVhIHMJHRV2Heo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F0ghIWvO7xfA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%220ghIWvO7xfA%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYEPJVWMvLGygcuOsfkbDa76qBkNBT0FyQkZzbi1mdm5MSXJQbnlvQmowaDYwc0VlQ3pXcmc3eHhoN25KaUhqc00takNIVlcwVHdXNTdUNkhpbUtMZkR3kAcCuAeH39b-nu6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F0ghIWvO7xfA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIr4rV_p7ujQMVYSBzCR0Vdh3q%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiH39b-nu6NAxXTofQHHc0VHBEqAKIBEwjpqYqfnu6NAxWae3oFHdlRDxg%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%22699039ab-0000-2615-9d26-9898fb8699f5%22%7D%7D
.youtube.com TRUE / FALSE 1749812440 ST-nppm2n itct=CAcQ7fkGGAMiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D&csn=y93td2HCkLkaZS1-&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAcQ7fkGGAMiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FbobxdlqFFwY%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22bobxdlqFFwY%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYOAjZz12ucbIkCNUIylJzJeqBkNBT0FyQkZ2Tng2UFAzdVpVejNhekdic2F5ZTBoUVZWMDMxbWVYcm9pbTRfRERTVS1HajhTZjM4Q3BTaG9PYkhaMHpFkAcCuAfj392in-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FbobxdlqFFwY%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAgQsLUEIhMI8I3dop_ujQMV6iUGAB0aMQIh%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjj392in-6NAxU5EfEFHcz3E04qAKIBEwivitX-nu6NAxVhIHMJHRV2Heo%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226aa68d63-0000-211a-8e82-14223bc7ae2a%22%7D%7D
.youtube.com TRUE / FALSE 1749812444 ST-b3uteg itct=CAUQ7fkGGAQiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D&csn=5Nhhr2TJoHissjRR&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAUQ7fkGGAQiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FY1sHKoexjpk%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Y1sHKoexjpk%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYBFCz-WTNYPn_29Hy3xCyoyqBkNBT0FyQkZzU0tnNjVMQXVHUm1KSC0wbTUtWXBtTE01OWxhMGZuNnQtMGQ1Y0dLNDlBZlJCbDM3czBwQlREVklleW53kAcCuAfj392in-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FY1sHKoexjpk%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAYQsLUEIhMI8I3dop_ujQMV6iUGAB0aMQIh%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjj392in-6NAxU5EfEFHcz3E04qAKIBEwivitX-nu6NAxVhIHMJHRV2Heo%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226aa68d64-0000-211a-8e82-14223bc7ae2a%22%7D%7D
.youtube.com TRUE / FALSE 1749812584 ST-1rns7zn itct=CAMQ7fkGGAUiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D&csn=7kJ9IiDBXZQjaCCi&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAMQ7fkGGAUiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F8OjyXgowEMA%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%228OjyXgowEMA%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYMPbANe10wIYfk_86hidQBmqBkNBT0FyQkZ1M2F6OHdmQ3ZXNkx6UVVLYXBZMXIyXzd2WDNtd3h5eHE0YVF6Vld2bnJMMGtiVHZvT3VnVkdlTXFzVXdzkAcCuAfj392in-6NA_AHAQ%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F8OjyXgowEMA%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAQQsLUEIhMI8I3dop_ujQMV6iUGAB0aMQIh%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjj392in-6NAxU5EfEFHcz3E04qAKIBEwivitX-nu6NAxVhIHMJHRV2Heo%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226aa68d65-0000-211a-8e82-14223bc7ae2a%22%7D%7D
.youtube.com TRUE / FALSE 1749812593 ST-11y9w0r itct=CAEQ7fkGGAYiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D&csn=SqaZdfnqtTKNdruN&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAYiEwjwjd2in-6NAxXqJQYAHRoxAiE%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FLz4hL0P58BY%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Lz4hL0P58BY%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYMQ3aZKf4sS2EalkO33HJbqqBkNBT0FyQkZzMXlsM3M4cHM1WXRrNmtidHhDUGx0QlAxME5yT2JRVlkzakRaRGNPSEg1ODlNWElrOThmRXRuenliZWJBkAcCuAfj392in-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FLz4hL0P58BY%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMI8I3dop_ujQMV6iUGAB0aMQIh%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjj392in-6NAxU5EfEFHcz3E04qAKIBEwivitX-nu6NAxVhIHMJHRV2Heo%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226aa68d66-0000-211a-8e82-14223bc7ae2a%22%7D%7D
.youtube.com TRUE / FALSE 1749812662 ST-13vnh0q itct=CAsQ7fkGGAQiEwip1ILqn-6NAxURYkECHeNeEsc%3D&csn=UPbvDx1b_HFOvQpV&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAsQ7fkGGAQiEwip1ILqn-6NAxURYkECHeNeEsc%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F2Bo2y3zW_V4%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%222Bo2y3zW_V4%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYEWKwDwqE_GJJupzFk0ucfmqBkNBT0FyQkZ2bUFidmZvb1VEanloRy10ejV1ZUdQN0xMQWgxNmVnM0RfMTZtY3JrNHlSSnM5cTJ6UUExMTgxTUVKNUM4kAcCuAeAs4Pqn-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F2Bo2y3zW_V4%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAwQsLUEIhMIqdSC6p_ujQMVEWJBAh3jXhLH%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiAs4Pqn-6NAxVIx0kHHXQ3DKUqAKIBEwjwjd2in-6NAxXqJQYAHRoxAiE%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a63f65a-0000-2ee3-9479-c82add7fd040%22%7D%7D
.youtube.com TRUE / FALSE 1749812667 ST-1w5ehet itct=CAkQ7fkGGAUiEwip1ILqn-6NAxURYkECHeNeEsc%3D&csn=dUQPb9pxprtt_0bF&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAkQ7fkGGAUiEwip1ILqn-6NAxURYkECHeNeEsc%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2Fb3F-kyeF-AI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22b3F-kyeF-AI%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYLDQUhp59Lg7hdEpRWlrTlKqBkNBT0FyQkZzaDJ3eW5HbHNReFFNLWtqOUV0RW1hVVE1OGJsd2RNV2dWM3ZoZGRtOVJ3bUdYajFGSDBDWktYN2F5ZWVFkAcCuAeAs4Pqn-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2Fb3F-kyeF-AI%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAoQsLUEIhMIqdSC6p_ujQMVEWJBAh3jXhLH%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiAs4Pqn-6NAxVIx0kHHXQ3DKUqAKIBEwjwjd2in-6NAxXqJQYAHRoxAiE%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a63f65b-0000-2ee3-9479-c82add7fd040%22%7D%7D
.youtube.com TRUE / FALSE 1749812691 ST-1wmw2ob itct=CAYQ7fkGGAciEwip1ILqn-6NAxURYkECHeNeEsc%3D&csn=GnLJGIxrBqNjdV3K&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAYQ7fkGGAciEwip1ILqn-6NAxURYkECHeNeEsc%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FLmtv2v9i24M%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22Lmtv2v9i24M%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYHTgYKDFC03X1SjMJe34SheqBkNBT0FyQkZzWkxZSWpKY1hFNjNtLWU3dTMtNzQwckdfeVQ5d1ZWQjJha2RaM0w3eEZOS3JkRmhfZnAwTHI2bWpWX3FnkAcCuAeAs4Pqn-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FLmtv2v9i24M%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAcQsLUEIhMIqdSC6p_ujQMVEWJBAh3jXhLH%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiAs4Pqn-6NAxVIx0kHHXQ3DKUqAKIBEwjwjd2in-6NAxXqJQYAHRoxAiE%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a63f65d-0000-2ee3-9479-c82add7fd040%22%7D%7D
.youtube.com TRUE / FALSE 1749812753 ST-1bhstkx itct=CAQQ7fkGGAgiEwip1ILqn-6NAxURYkECHeNeEsc%3D&csn=zbatCaBOBPXj-IW_&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAQQ7fkGGAgiEwip1ILqn-6NAxURYkECHeNeEsc%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F-01P9KS_H5w%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22-01P9KS_H5w%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYK3mSbxHxfZF4IZ4vlev6QWqBkNBT0FyQkZ2aERBYTd5Q3l4YTc2bnFia1RSaXV4UWkxMXJ5XzNmanUzTGh6cF9tSlRvZUJXVjduYzU5ZU9qb2ZJRk9BkAcCuAeAs4Pqn-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F-01P9KS_H5w%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAUQsLUEIhMIqdSC6p_ujQMVEWJBAh3jXhLH%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiAs4Pqn-6NAxVIx0kHHXQ3DKUqAKIBEwjwjd2in-6NAxXqJQYAHRoxAiE%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a63f65e-0000-2ee3-9479-c82add7fd040%22%7D%7D
.youtube.com TRUE / FALSE 1749812753 ST-1tedtqx session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749812801 ST-1m1x9se itct=CAEQ7fkGGAoiEwip1ILqn-6NAxURYkECHeNeEsc%3D&csn=mrUmQCePbi3AhFBv&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAEQ7fkGGAoiEwip1ILqn-6NAxURYkECHeNeEsc%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FjhSeZj5cg9w%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22jhSeZj5cg9w%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYGvCEJl-_eumngS8FgbBZS-qBkNBT0FyQkZzd0ZfTU1WWUZBN2U2NTdXSzRXdHhfbjM0RW96a0hFX05EVUg4YXFwbGxzbWlVNlNmVS1PZE9sR2o3Z3BJkAcCuAeAs4Pqn-6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FjhSeZj5cg9w%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAIQsLUEIhMIqdSC6p_ujQMVEWJBAh3jXhLH%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwiAs4Pqn-6NAxVIx0kHHXQ3DKUqAKIBEwjwjd2in-6NAxXqJQYAHRoxAiE%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a63f660-0000-2ee3-9479-c82add7fd040%22%7D%7D
.youtube.com TRUE / FALSE 1749812879 ST-f78vs5 itct=CA8Q7fkGGAMiEwiHkJ66oO6NAxW_XkECHTKqMoo%3D&csn=6HZUwriWNYlpPDbT&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CA8Q7fkGGAMiEwiHkJ66oO6NAxW_XkECHTKqMoo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FXjlZ3huoi1E%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22XjlZ3huoi1E%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYHLnqkBOEX5OLHU5fttO4q6qBkNBT0FyQkZ0cXBSUWVEZFp6bllqUnJEYmpqX3ItVldCX2JiVFF5c2hSVGh6b0s0RnRNMnBVRzRaRnU1aGUwLVhGTG9vkAcCuAfMt6C6oO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FXjlZ3huoi1E%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CBAQsLUEIhMIh5CeuqDujQMVv15BAh0yqjKK%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjMt6C6oO6NAxWg308IHVhnJZsqAKIBEwip1ILqn-6NAxURYkECHeNeEsc%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a70c0a6-0000-2a32-aa2b-3c286d4e42ea%22%7D%7D
.youtube.com TRUE / FALSE 1749812916 ST-y2gipp itct=CAwQ7fkGGAUiEwiHkJ66oO6NAxW_XkECHTKqMoo%3D&csn=tGTkuV_jLWmRf6So&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CAwQ7fkGGAUiEwiHkJ66oO6NAxW_XkECHTKqMoo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2F-ERBpFz_YYI%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22-ERBpFz_YYI%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYMkGeJM6S7R0A8d7SkN9h0CqBkNBT0FyQkZ1aHNuWHNqWjh2WVhfTnlZUEdIZFFrUFlRQ2ZrLVN5eENHUlNzRkNBMXVidWVfbUFPU3RQdHpRaXZwTmdvkAcCuAfMt6C6oO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2F-ERBpFz_YYI%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CA0QsLUEIhMIh5CeuqDujQMVv15BAh0yqjKK%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjMt6C6oO6NAxWg308IHVhnJZsqAKIBEwip1ILqn-6NAxURYkECHeNeEsc%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a70c0a8-0000-2a32-aa2b-3c286d4e42ea%22%7D%7D
.youtube.com TRUE / FALSE 1749812917 ST-1yus4er endpoint=%7B%22clickTrackingParams%22%3A%22CAoQ7fkGGAYiEwiHkJ66oO6NAxW_XkECHTKqMoo%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FS_5e1i47U5s%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22S_5e1i47U5s%22%2C%22playerParams%22%3A%228AEByAMTwATyz9eEiPjN_m6iBhUBdpLKYERkNWSaygkY3Z2tsdoERjqqBkNBT0FyQkZ0UnFtSTZIbXlDanRfY0JYSEczWURLdmF6Q3hnRDR4SXl6WjVnaS1MZVc0SUhQNGpEcW9yTWw3eG44V1ZFkAcCuAfMt6C6oO6NAw%253D%253D%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FS_5e1i47U5s%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CAsQsLUEIhMIh5CeuqDujQMVv15BAh0yqjKK%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAYaEwjMt6C6oO6NAxWg308IHVhnJZsqAKIBEwip1ILqn-6NAxURYkECHeNeEsc%253D%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%2C%22identifier%22%3A%226a70c0a9-0000-2a32-aa2b-3c286d4e42ea%22%7D%7D
.youtube.com TRUE / FALSE 1749815887 ST-eiyx3r disableCache=false&itct=CCwQrIEJGAEiEwiaivCOrO6NAxW00UkHHSm-Dlg%3D&csn=fHiSvRSPZ9iLCHFr&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCwQrIEJGAEiEwiaivCOrO6NAxW00UkHHSm-Dlg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40CuttingEdgeEngineering%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UC2wdo5vU7bPBNzyC2nnwmNQ%22%2C%22canonicalBaseUrl%22%3A%22%2F%40CuttingEdgeEngineering%22%7D%7D
.youtube.com TRUE / FALSE 1749815932 ST-1i5z18h itct=CDUQ8JMBGAkiEwiDouuirO6NAxXuIQYAHUpLAyE%3D&csn=fPLvHAw8SvKtBpBv&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CDUQ8JMBGAkiEwiDouuirO6NAxXuIQYAHUpLAyE%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40PlanesTrainsEverything%2Fvideos%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCcKw8Eg0FfRvhIAnC0cPGAA%22%2C%22params%22%3A%22EgZ2aWRlb3PyBgQKAjoA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40PlanesTrainsEverything%22%7D%7D
.youtube.com TRUE / FALSE 1749820265 ST-aaui2p disableCache=false&itct=CCwQrIEJGAEiEwif-9KJvO6NAxXz70kHHaBWFPs%3D&csn=cDmMyD_nPlbFOpGv&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCwQrIEJGAEiEwif-9KJvO6NAxXz70kHHaBWFPs%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40KrisHarbour%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UC5rT7F0PGNuD54rJ9kzgWzw%22%2C%22canonicalBaseUrl%22%3A%22%2F%40KrisHarbour%22%7D%7D
.youtube.com TRUE / FALSE 1749820274 ST-rq7u3z itct=CNkCEJQ1GAAiEwif1KK4vO6NAxX-50kHHetoHHRaGFVDNXJUN0YwUEdOdUQ1NHJKOWt6Z1d6d5oBBhDyOBijDA%3D%3D&csn=E6brKvIyJ6s0IWLD&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CNkCEJQ1GAAiEwif1KK4vO6NAxX-50kHHetoHHRaGFVDNXJUN0YwUEdOdUQ1NHJKOWt6Z1d6d5oBBhDyOBijDA%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3DZ5ZwlFY9rNc%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22Z5ZwlFY9rNc%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr1---sn-aigl6nek.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3D67967094563dacd7%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D5106250%26mt%3D1749819918%26oweuc%3D%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749888887 ST-10zfwmu disableCache=false&itct=CC0QrIEJGAEiEwjC1sCHvPCNAxUzBXMJHbniB3w%3D&csn=0pAOn4GlEyLJOaUr&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CC0QrIEJGAEiEwjC1sCHvPCNAxUzBXMJHbniB3w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40PrimeTales5%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCh7qCwKMxqTSZGPCH8CzZjA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40PrimeTales5%22%7D%7D
.youtube.com TRUE / FALSE 1749888897 ST-163o2bb session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / TRUE 1749889493 CONSISTENCY AKreu9uxIFFJ0UuiSJW4xXwEuOZru4OE_oc0aPGeMWTVwT3U1Tr3B5dHDiFxZ5kF8G-p-y04n4T83l-NKw1qANGgkDDThur-PIyKQHPoybmHzUq-XwpzT5QrBKg
.youtube.com TRUE / FALSE 1749888916 ST-ld9q6 disableCache=false&itct=CCwQrIEJGAIiEwjC1sCHvPCNAxUzBXMJHbniB3w%3D&csn=8N1ed-CMMLlmrCSz&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCwQrIEJGAIiEwjC1sCHvPCNAxUzBXMJHbniB3w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40electrictrucker%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCHHto2oaQ9OqZke5c25xtjQ%22%2C%22canonicalBaseUrl%22%3A%22%2F%40electrictrucker%22%7D%7D
.youtube.com TRUE / FALSE 1749888943 ST-2lpuvf itct=CJgCENwwIhMIlo3WmrzwjQMVFgZzCR33HBgiWhhVQ0hIdG8yb2FROU9xWmtlNWMyNXh0alGaAQMQ8jg%3D&csn=0oKNCLdxP3q3St-V&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CJgCENwwIhMIlo3WmrzwjQMVFgZzCR33HBgiWhhVQ0hIdG8yb2FROU9xWmtlNWMyNXh0alGaAQMQ8jg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3DL3luSlI9-2o%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22L3luSlI9-2o%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr1---sn-aigl6nzr.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3D2f796e4a523dfb6a%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D4961250%26mt%3D1749888543%26oweuc%3D%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749888944 ST-jqw917 session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749888958 ST-21r978 itct=CKACENwwIhMIlo3WmrzwjQMVFgZzCR33HBgiWhhVQ0hIdG8yb2FROU9xWmtlNWMyNXh0alGaAQMQ8jg%3D&csn=9Bu2QOV0IVlq8HF-&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CKACENwwIhMIlo3WmrzwjQMVFgZzCR33HBgiWhhVQ0hIdG8yb2FROU9xWmtlNWMyNXh0alGaAQMQ8jg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3DztCycoq2jhU%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22ztCycoq2jhU%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr4---sn-aigl6nze.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3Dced0b2728ab68e15%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D4961250%26mt%3D1749888543%26oweuc%3D%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749890200 ST-tql1xl itct=CC8Q8JMBGAkiEwj6lOiXvPCNAxVl1UkHHafAOsQ%3D&csn=r9Od8m5FZiQ6KvNY&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CC8Q8JMBGAkiEwj6lOiXvPCNAxVl1UkHHafAOsQ%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40electrictrucker%2Fvideos%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCHHto2oaQ9OqZke5c25xtjQ%22%2C%22params%22%3A%22EgZ2aWRlb3PyBgQKAjoA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40electrictrucker%22%7D%7D
.youtube.com TRUE / FALSE 1749890203 ST-gl93c8 disableCache=false&itct=CCsQrIEJGAMiEwjC1sCHvPCNAxUzBXMJHbniB3w%3D&csn=NiFgWTOni5v5NNPz&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCsQrIEJGAMiEwjC1sCHvPCNAxUzBXMJHbniB3w%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40FlywithHayley%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UC_Sq9GhLG8GzDYZBNytqkEQ%22%2C%22canonicalBaseUrl%22%3A%22%2F%40FlywithHayley%22%7D%7D
.youtube.com TRUE / FALSE 1749902427 ST-1kicthe itct=CPUCEJQ1GAAiEwjrh-i87vCNAxVQEnMJHTvQLghaGFVDY0t3OEVnMEZmUnZoSUFuQzBjUEdBQZoBBhDyOBijDA%3D%3D&csn=FeGoqi05t7FLcnlQ&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CPUCEJQ1GAAiEwjrh-i87vCNAxVQEnMJHTvQLghaGFVDY0t3OEVnMEZmUnZoSUFuQzBjUEdBQZoBBhDyOBijDA%3D%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3DPwFpf7oCDSM%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22PwFpf7oCDSM%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr3---sn-aigl6nzr.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3D3f01697fba020d23%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D5247500%26mt%3D1749902214%26oweuc%3D%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749904196 ST-k4wyo6 itct=CCAQrYEJGA0iEwi7uJWA7vCNAxXc6EkHHY1FOO4%3D&csn=RjUfyiq8LcnFLDK_&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCAQrYEJGA0iEwi7uJWA7vCNAxXc6EkHHY1FOO4%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40PlanesTrainsEverything%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCcKw8Eg0FfRvhIAnC0cPGAA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40PlanesTrainsEverything%22%7D%7D
.youtube.com TRUE / FALSE 1749977615 ST-18nws10 disableCache=false&itct=CCwQrIEJGAEiEwiMtrDNhvONAxXc1kkHHYI2Gio%3D&csn=2lN8qiY5cmeYPB4v&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CCwQrIEJGAEiEwiMtrDNhvONAxXc1kkHHYI2Gio%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40rctestflight%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCq2rNse2XX4Rjzmldv9GqrQ%22%2C%22canonicalBaseUrl%22%3A%22%2F%40rctestflight%22%7D%7D
.youtube.com TRUE / FALSE 1749977623 ST-12dty3n itct=CDUQ8JMBGAsiEwin5fPOhvONAxWRDnMJHRWVFDs%3D&csn=ZbQNAqnstXaJdp2F&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CDUQ8JMBGAsiEwin5fPOhvONAxWRDnMJHRWVFDs%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40rctestflight%2Fvideos%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22UCq2rNse2XX4Rjzmldv9GqrQ%22%2C%22params%22%3A%22EgZ2aWRlb3PyBgQKAjoA%22%2C%22canonicalBaseUrl%22%3A%22%2F%40rctestflight%22%7D%7D
.youtube.com TRUE / FALSE 1749977632 ST-1uz7ucq itct=CKYCENwwIhMIpIK30obzjQMVuzzxBR0GsyPdWhhVQ3Eyck5zZTJYWDRSanptbGR2OUdxclGaAQMQ8jg%3D&csn=vTzHi2M1qrClZPHQ&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CKYCENwwIhMIpIK30obzjQMVuzzxBR0GsyPdWhhVQ3Eyck5zZTJYWDRSanptbGR2OUdxclGaAQMQ8jg%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fwatch%3Fv%3DR5_rlyRI9C4%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_WATCH%22%2C%22rootVe%22%3A3832%7D%7D%2C%22watchEndpoint%22%3A%7B%22videoId%22%3A%22R5_rlyRI9C4%22%2C%22watchEndpointSupportedOnesieConfig%22%3A%7B%22html5PlaybackOnesieConfig%22%3A%7B%22commonConfig%22%3A%7B%22url%22%3A%22https%3A%2F%2Frr3---sn-aigl6nsr.googlevideo.com%2Finitplayback%3Fsource%3Dyoutube%26oeis%3D1%26c%3DWEB%26oad%3D3200%26ovd%3D3200%26oaad%3D11000%26oavd%3D11000%26ocs%3D700%26oewis%3D1%26oputc%3D1%26ofpcc%3D1%26siu%3D1%26msp%3D1%26odepv%3D1%26onvi%3D1%26id%3D479feb972448f42e%26ip%3D2a04%253A201%253A1bff%253Af700%253A6a45%253A63ec%253A5b13%253Aa97e%26initcwndbps%3D4783750%26mt%3D1749977096%26oweuc%3D%26pxtags%3DCg4KAnR4Egg1MTUwNzc2Ng%26rxtags%3DCg4KAnR4Egg1MTUwNzc2Ng%252CCg4KAnR4Egg1MTUwNzc2Nw%252CCg4KAnR4Egg1MTUwNzc2OA%252CCg4KAnR4Egg1MTUwNzc2OQ%252CCg4KAnR4Egg1MTUwNzc3MA%22%7D%7D%7D%7D%7D
.youtube.com TRUE / FALSE 1749978426 ST-amrb2j session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749978780 ST-pd28k9 session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749978836 ST-183jmdn session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / TRUE 0 YSC scaCd70S9y8
.youtube.com TRUE / FALSE 1749988393 ST-tladcw session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749988395 ST-xuwub9 session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
.youtube.com TRUE / FALSE 1749988399 ST-1b disableCache=false&itct=CDoQ8KgHGAAiEwislMDirvONAxUZ9kIFHX1VL2Y%3D&csn=ukPuXrtOE2ZwzzUb&session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR&endpoint=%7B%22clickTrackingParams%22%3A%22CDoQ8KgHGAAiEwislMDirvONAxUZ9kIFHX1VL2Y%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_BROWSE%22%2C%22rootVe%22%3A3854%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22FEwhat_to_watch%22%7D%7D
.youtube.com TRUE / FALSE 1749988399 ST-yve142 session_logininfo=AFmmF2swRQIgcrCr18aSuVOd9dRA3fuxiw6Mw1hFkNzuSxh_LINRipQCIQCkpknYouKiTmTYocqQq9mqStav0NEftzQurx2RFmX8Ug%3AQUQ3MjNmeUxPUzFVUkFUdjNmRS16TE9McG1YaWMtT1A2aWY4WkZEZ0RUajFkcjVlVjIxMDE0MWo4VVBMQXZuV2kzZENyMkN4aHFzeFJLVklua1FETjV5N1pnNzgxTUhkWFZaQ0dPMWYyVDNqMzE1WW13S3h4SnVCaDVGZnlpdEFMZkJFWGQwRGQwd1VqT285ajNzdjNYcmlzRmE0RlJNWnZR
@@ -1,213 +0,0 @@
from __future__ import annotations
from http.cookiejar import MozillaCookieJar
from typing import Any, Optional, Union
import sys
import re
import click
from click import Context
from envied.core.credential import Credential
from envied.core.service import Service
from envied.core.titles import Movie, Movies, Episode, Series
from envied.core.tracks import Track, Chapter, Tracks, Video, Subtitle
class ZDF(Service):
"""
Service code for ZDF.de (https://www.zdf.de)
\b
Version: 1.0.0
Author: lambda
Authorization: None
Robustness:
Unencrypted: 2160p HLG, AAC2.0
"""
GEOFENCE = ("de",)
VIDEO_RE = r"^https://www\.zdf\.de/(play|video)/(?P<content_type>.+)/(?P<series_slug>.+)/(?P<item_slug>[^\?]+)(\?.+)?$"
SERIES_RE = r"^https://www.zdf.de/serien/(?P<slug>[^\?]+)(\?.+)?$"
VIDEO_CODEC_MAP = {
"video/mp4": Video.Codec.AVC,
"video/webm": Video.Codec.VP9
}
@staticmethod
@click.command(name="ZDF", short_help="https://www.zdf.de", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> ZDF:
return ZDF(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
self.title = title
super().__init__(ctx)
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
# This seems to be more or less static, but it's easy enough to fetch every time
r = self.session.get("http://hbbtv.zdf.de/zdfm3/index.php")
match = re.match(r'.+GLOBALS\.apikey += +"(?P<header>[^"\n]+).+";', r.text, re.DOTALL)
self.session.headers.update({"Api-Auth": match.group('header')})
def get_titles(self) -> Union[Movies, Series]:
if match := re.match(self.SERIES_RE, self.title):
return self.handle_series_page(match.group('slug'))
if match := re.match(self.VIDEO_RE, self.title):
r = self.session.post(self.config["endpoints"]["graphql"], json={
"operationName": "VideoByCanonical",
"query": self.config["queries"]["VideoByCanonical"],
"variables": {"canonical": match.group('item_slug'), "first": 1},
}, headers={"content-type": "application/json"})
video = r.json()["data"]["videoByCanonical"]
return self.parse_video_data(video)
def get_tracks(self, title: Union[Episode, Movie]) -> Tracks:
tracks = Tracks()
for node in title.data["nodes"]:
if node["vodMediaType"] != "DEFAULT":
continue
for player_type in self.config["meta"]["player_types"]:
ptmd_url = (self.config["endpoints"]["ptmd_base"] +
node["ptmdTemplate"].format(playerId=player_type))
r = self.session.get(ptmd_url)
ptmd = r.json()
for pl in ptmd["priorityList"]:
for media_format in pl["formitaeten"]:
if "restriction_useragent" in media_format["facets"] or media_format["mimeType"] not in self.VIDEO_CODEC_MAP.keys():
continue
if 'hdr_hlg' in media_format["facets"]:
video_range = Video.Range.HLG
video_codec = Video.Codec.HEVC
else:
video_range = Video.Range.SDR
video_codec = self.VIDEO_CODEC_MAP[media_format["mimeType"]]
for quality in media_format["qualities"]:
for track in quality["audio"]["tracks"]:
if track["class"] not in ("main", "ot"):
continue
track_id = f'{video_codec}-{track["language"]}-{quality["highestVerticalResolution"]}'
if tracks.exists(by_id=track_id):
continue
tracks.add(Video(
id_=track_id,
codec=video_codec,
range_=video_range,
width=quality["highestVerticalResolution"] // 9 * 16,
height=quality["highestVerticalResolution"],
url=track["uri"],
language=track["language"],
fps=50,
))
for subs in ptmd["captions"]:
if subs["format"] == "ebu-tt-d-basic-de":
track_id = f'subs-{subs["language"]}-{subs["class"]}'
if tracks.exists(by_id=track_id):
continue
tracks.add(Subtitle(
id_=track_id,
codec=Subtitle.Codec.TimedTextMarkupLang,
language=subs["language"],
sdh=subs["class"] == "hoh",
url=subs["uri"]
))
return tracks
def get_chapters(self, title: Union[Episode, Movie]) -> list[Chapter]:
for node in title.data["nodes"]:
si = node.get("skipIntro")
if si and node["vodMediaType"] == "DEFAULT":
if si["startIntroTimeOffset"] and si["stopIntroTimeOffset"]:
intro_start = float(si["startIntroTimeOffset"])
intro_stop = float(si["stopIntroTimeOffset"])
chapters = []
if intro_start != 0:
chapters.append(Chapter(timestamp=0))
return chapters + [
Chapter(timestamp=intro_start),
Chapter(timestamp=intro_stop),
]
break
return []
def parse_video_data(self, video):
common_data = {
"id_": video["id"],
"service": self.__class__,
"year": video["editorialDate"][0:4],
"data": video["currentMedia"],
}
meta = video["structuralMetadata"]
if "publicationFormInfo" in meta and meta["publicationFormInfo"]["original"] == "Film":
return Movies([Movie(
name=video["title"],
**common_data
)])
else:
name = video["title"]
series_title = video["smartCollection"].get("title", "DUMMY")
# Ignore fake episode names like "Episode 123" or "Series Name (1/8)"
if re.match(fr"^(Folge \d+|{series_title} \(\d+/\d+\))$", name):
name = None
return Series([Episode(
**common_data,
name=name,
title=series_title,
season=video["episodeInfo"]["seasonNumber"],
number=video["episodeInfo"]["episodeNumber"],
)])
def handle_series_page(self, slug):
extensions = {
"persistedQuery": {
"version": 1,
"sha256Hash": "9412a0f4ac55dc37d46975d461ec64bfd14380d815df843a1492348f77b5c99a"
}
}
variables = {
"seasonIndex": 0,
"episodesPageSize": 24,
"canonical": slug,
"sortBy": [
{
"field": "EDITORIAL_DATE",
"direction": "ASC"
}
]
}
r = self.session.get(self.config["endpoints"]["graphql"], params={
"extensions": json.dumps(extensions),
"variables": json.dumps(variables)
}, headers={"content-type": "application/json"})
data = r.json()["data"]["smartCollectionByCanonical"]
if not data:
return
series = Series()
for season in data["seasons"]["nodes"]:
for video in season["episodes"]["nodes"]:
series += self.parse_video_data(video)
return series
@@ -1,117 +0,0 @@
headers:
Accept-Language: de-DE,de;q=0.8
User-Agent: Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36 DMOST/2.0.0 (; LGE; webOSTV; WEBOS6.3.2 03.34.95; W6_lm21a;)
endpoints:
graphql: https://api.zdf.de/graphql
ptmd_base: https://api.zdf.de
meta:
# Known options:
# ngplayer_2_5 (Web player - H.264 1080p + VP9 1080p))
# smarttv_6 (HBBTV - H.264 1080p + H.265 HLG 2160p)
# smarttv_7 (Unknown - same formats as smarttv_6)
# android_native_5 (Android - H.264 1080p + VP9 1080p + H.265 HLG 2160p)
player_types:
- android_native_5
queries:
VideoByCanonical: |
query VideoByCanonical($canonical: String!, $first: Int) {
videoByCanonical(canonical: $canonical) {
id
canonical
contentType
title
editorialDate
streamingOptions {
ad
ut
dgs
ov
ks
fsk
}
episodeInfo {
episodeNumber
seasonNumber
}
structuralMetadata {
isChildrenContent
publicationFormInfo {
original
transformed
}
visualDimension {
moods(first: $first) {
nodes {
mood
}
}
}
}
smartCollection {
id
canonical
title
collectionType
structuralMetadata {
contentFamily
publicationFormInfo {
original
transformed
}
}
}
seo {
title
}
availability {
fskBlocked
}
currentMediaType
subtitle
webUrl
publicationDate
currentMedia {
nodes {
ptmdTemplate
... on VodMedia {
duration
aspectRatio
visible
geoLocation
highestVerticalResolution
streamAnchorTags {
nodes {
anchorOffset
anchorLabel
}
}
skipIntro {
startIntroTimeOffset
stopIntroTimeOffset
skipButtonDisplayTime
skipButtonLabel
}
vodMediaType
label
contentType
}
... on LiveMedia {
geoLocation
tvService
title
start
stop
editorialStart
editorialStop
encryption
liveMediaType
label
}
id
}
}
}
}
@@ -1,452 +0,0 @@
from __future__ import annotations
import base64
import hashlib
import json
import re
import tempfile
import warnings
from collections.abc import Generator
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any, Union
import click
from bs4 import XMLParsedAsHTMLWarning
from click import Context
from envied.core.manifests import DASH, HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Audio, Chapters, Subtitle, Tracks, Video
from envied.core.utils.collections import as_list
from envied.core.utils.sslciphers import SSLCiphers
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
class iP(Service):
"""
\b
Service code for the BBC iPlayer streaming service (https://www.bbc.co.uk/iplayer).
\b
Version: 1.0.2
Author: stabbedbybrick
Authorization: None
Security: None
\b
Tips:
- Use full title URL as input for best results.
- Use --list-titles before anything, iPlayer's listings are often messed up.
\b
- Use --range HLG to request H.265 UHD tracks
- See which titles are available in UHD:
https://www.bbc.co.uk/iplayer/help/questions/programme-availability/uhd-content
"""
ALIASES = ("bbciplayer", "bbc", "iplayer")
GEOFENCE = ("gb",)
TITLE_RE = r"^(?:https?://(?:www\.)?bbc\.co\.uk/(?:iplayer/(?P<kind>episode|episodes)/|programmes/))?(?P<id>[a-z0-9]+)(?:/.*)?$"
@staticmethod
@click.command(name="iP", short_help="https://www.bbc.co.uk/iplayer", help=__doc__)
@click.argument("title", type=str)
@click.pass_context
def cli(ctx: Context, **kwargs: Any) -> "iP":
return iP(ctx, **kwargs)
def __init__(self, ctx: Context, title: str):
super().__init__(ctx)
self.title = title
self.vcodec = ctx.parent.params.get("vcodec")
self.range = ctx.parent.params.get("range_")
self.session.headers.update({"user-agent": "BBCiPlayer/5.17.2.32046"})
if self.range and self.range[0].name == "HLG":
if not self.config.get("certificate"):
raise CertificateMissingError("HLG/H.265 tracks cannot be requested without a TLS certificate.")
self.session.headers.update({"user-agent": self.config["user_agent"]})
self.vcodec = "H.265"
def search(self) -> Generator[SearchResult, None, None]:
r = self.session.get(self.config["endpoints"]["search"], params={"q": self.title})
r.raise_for_status()
results = r.json().get("new_search", {}).get("results", [])
for result in results:
programme_type = result.get("type", "unknown")
category = result.get("labels", {}).get("category", "")
path = "episode" if programme_type == "episode" else "episodes"
yield SearchResult(
id_=result.get("id"),
title=result.get("title"),
description=result.get("synopses", {}).get("small"),
label=f"{programme_type} - {category}",
url=f"https://www.bbc.co.uk/iplayer/{path}/{result.get('id')}",
)
def get_titles(self) -> Union[Movies, Series]:
match = re.match(self.TITLE_RE, self.title)
if not match:
raise ValueError("Could not parse ID from title - is the URL/ID format correct?")
groups = match.groupdict()
pid = groups.get("id")
kind = groups.get("kind")
# Attempt to get brand/series data first
data = self.get_data(pid, slice_id=None)
# Handle case where the input is a direct episode URL and get_data fails
if data is None and kind == "episode":
return Series([self.fetch_episode(pid)])
if data is None:
raise MetadataError(f"Metadata not found for '{pid}'. If it's an episode, use the full URL.")
# If it's a "series" with only one item, it might be a movie.
if data.get("count", 0) < 2:
r = self.session.get(self.config["endpoints"]["episodes"].format(pid=pid))
r.raise_for_status()
episodes_data = r.json()
if not episodes_data.get("episodes"):
raise MetadataError(f"Episode metadata not found for '{pid}'.")
movie_data = episodes_data["episodes"][0]
return Movies(
[
Movie(
id_=movie_data.get("id"),
name=movie_data.get("title"),
year=(movie_data.get("release_date_time", "") or "").split("-")[0],
service=self.__class__,
language="en",
data=data,
)
]
)
# It's a full series
seasons = [self.get_data(pid, x["id"]) for x in data.get("slices") or [{"id": None}]]
episode_ids = [
episode.get("episode", {}).get("id")
for season in seasons
for episode in season.get("entities", {}).get("results", [])
if not episode.get("episode", {}).get("live")
and episode.get("episode", {}).get("id")
]
episodes = self.get_episodes(episode_ids)
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
versions = self._get_available_versions(title.id)
if not versions:
raise NoStreamsAvailableError("No available versions for this title were found.")
connections = [self.check_all_versions(version["pid"]) for version in versions]
connections = [c for c in connections if c]
if not connections:
if self.vcodec == "H.265":
raise NoStreamsAvailableError("Selection unavailable in UHD.")
raise NoStreamsAvailableError("Selection unavailable. Title may be missing or geo-blocked.")
media = self._select_best_media(connections)
if not media:
raise NoStreamsAvailableError("Could not find a suitable media stream.")
tracks = self._select_tracks(media, title.language)
return tracks
def get_chapters(self, title: Union[Movie, Episode]) -> Chapters:
return Chapters()
def _get_available_versions(self, pid: str) -> list[dict]:
"""Fetch all available versions for a programme ID."""
r = self.session.get(url=self.config["endpoints"]["playlist"].format(pid=pid))
r.raise_for_status()
playlist = r.json()
versions = playlist.get("allAvailableVersions")
if versions:
return versions
# Fallback to scraping webpage if API returns no versions
self.log.debug("No versions in playlist API, falling back to webpage scrape.")
r = self.session.get(self.config["base_url"].format(type="episode", pid=pid))
r.raise_for_status()
match = re.search(r"window\.__IPLAYER_REDUX_STATE__\s*=\s*(.*?);\s*</script>", r.text)
if match:
redux_data = json.loads(match.group(1))
redux_versions = redux_data.get("versions")
versions = redux_versions.values() if isinstance(redux_versions, dict) else redux_versions
# Filter out audio-described versions
return [
{"pid": v.get("id")}
for v in versions
if v.get("kind") != "audio-described" and v.get("id")
]
return []
def _select_best_media(self, connections: list[list[dict]]) -> list[dict]:
"""Selects the media group corresponding to the highest available video quality."""
heights = sorted(
{
int(c["height"])
for media_list in connections
for c in media_list
if c.get("height", "").isdigit()
},
reverse=True,
)
if not heights:
self.log.warning("No video streams with height information were found.")
# Fallback: return the first available media group if any exist.
return connections[0] if connections else None
highest_height = heights[0]
self.log.debug(f"Available resolutions (p): {heights}. Selecting highest: {highest_height}p.")
best_media_list = next(
(
media_list
for media_list in connections
if any(conn.get("height") == str(highest_height) for conn in media_list)
),
None, # Default to None if no matching group is found (should be impossible if heights is not empty)
)
return best_media_list
def _select_tracks(self, media: list[dict], lang: str):
for video_stream_info in (m for m in media if m.get("kind") == "video"):
connections = sorted(video_stream_info["connection"], key=lambda x: x.get("priority", 99))
if self.vcodec == "H.265":
connection = connections[0]
else:
connection = next((c for c in connections if c["supplier"] == "mf_akamai" and c["transferFormat"] == "dash"), None)
break
if not self.vcodec == "H.265":
if connection["transferFormat"] == "dash":
connection["href"] = "/".join(
connection["href"]
.replace("dash", "hls")
.split("?")[0]
.split("/")[0:-1]
+ ["hls", "master.m3u8"]
)
connection["transferFormat"] = "hls"
elif connection["transferFormat"] == "hls":
connection["href"] = "/".join(
connection["href"]
.replace(".hlsv2.ism", "")
.split("?")[0]
.split("/")[0:-1]
+ ["hls", "master.m3u8"]
)
if connection["transferFormat"] == "dash":
tracks = DASH.from_url(url=connection["href"], session=self.session).to_tracks(language=lang)
elif connection["transferFormat"] == "hls":
tracks = HLS.from_url(url=connection["href"], session=self.session).to_tracks(language=lang)
else:
raise ValueError(f"Unsupported transfer format: {connection['transferFormat']}")
for video in tracks.videos:
# UHD DASH manifest has no range information, so we add it manually
if video.codec == Video.Codec.HEVC:
video.range = Video.Range.HLG
if any(re.search(r"-audio_\w+=\d+", x) for x in as_list(video.url)):
# create audio stream from the video stream
audio_url = re.sub(r"-video=\d+", "", as_list(video.url)[0])
audio = Audio(
# use audio_url not video url, as to ignore video bitrate in ID
id_=hashlib.md5(audio_url.encode()).hexdigest()[0:7],
url=audio_url,
codec=Audio.Codec.from_codecs(video.data["hls"]["playlist"].stream_info.codecs),
language=video.data["hls"]["playlist"].media[0].language,
bitrate=int(self.find(r"-audio_\w+=(\d+)", as_list(video.url)[0]) or 0),
channels=video.data["hls"]["playlist"].media[0].channels,
descriptive=False, # Not available
descriptor=Audio.Descriptor.HLS,
drm=video.drm,
data=video.data,
)
if not tracks.exists(by_id=audio.id):
# some video streams use the same audio, so natural dupes exist
tracks.add(audio)
# remove audio from the video stream
video.url = [re.sub(r"-audio_\w+=\d+", "", x) for x in as_list(video.url)][0]
video.codec = Video.Codec.from_codecs(video.data["hls"]["playlist"].stream_info.codecs)
video.bitrate = int(self.find(r"-video=(\d+)", as_list(video.url)[0]) or 0)
for caption in [x for x in media if x["kind"] == "captions"]:
connection = sorted(caption["connection"], key=lambda x: x["priority"])[0]
tracks.add(
Subtitle(
id_=hashlib.md5(connection["href"].encode()).hexdigest()[0:6],
url=connection["href"],
codec=Subtitle.Codec.from_codecs("ttml"),
language=lang,
is_original_lang=True,
forced=False,
sdh=True,
)
)
break
return tracks
def get_data(self, pid: str, slice_id: str) -> dict:
"""Fetches programme metadata from the GraphQL-like endpoint."""
json_data = {
"id": "9fd1636abe711717c2baf00cebb668de",
"variables": {"id": pid, "perPage": 200, "page": 1, "sliceId": slice_id},
}
r = self.session.post(self.config["endpoints"]["metadata"], json=json_data)
r.raise_for_status()
return r.json().get("data", {}).get("programme")
def check_all_versions(self, vpid: str) -> list:
"""Checks media availability for a given version PID, trying multiple mediators."""
session = self.session
cert_path = None
params = {}
if self.vcodec == "H.265":
if not self.config.get("certificate"):
raise CertificateMissingError("TLS certificate not configured.")
session.mount("https://", SSLCiphers())
endpoint_template = self.config["endpoints"]["secure"]
mediators = ["securegate.iplayer.bbc.co.uk", "ipsecure.stage.bbc.co.uk"]
mediaset = "iptv-uhd"
cert_binary = base64.b64decode(self.config["certificate"])
with tempfile.NamedTemporaryFile(mode="w+b", delete=False, suffix=".pem") as cert_file:
cert_file.write(cert_binary)
cert_path = cert_file.name
params["cert"] = cert_path
else:
endpoint_template = self.config["endpoints"]["open"]
mediators = ["open.live.bbc.co.uk", "open.stage.bbc.co.uk"]
mediaset = "iptv-all"
for mediator in mediators:
if self.vcodec == "H.265":
url = endpoint_template.format(mediator, vpid, mediaset)
else:
url = endpoint_template.format(mediator, mediaset, vpid)
try:
r = session.get(url, **params)
r.raise_for_status()
availability = r.json()
if availability.get("media"):
return availability["media"]
if availability.get("result"):
self.log.warning(
f"Mediator '{mediator}' reported an error: {availability['result']}"
)
except Exception as e:
self.log.debug(f"Failed to check mediator '{mediator}': {e}")
finally:
if cert_path is not None:
Path(cert_path).unlink(missing_ok=True)
return None
def fetch_episode(self, pid: str) -> Episode:
"""Fetches and parses data for a single episode."""
r = self.session.get(self.config["endpoints"]["episodes"].format(pid=pid))
r.raise_for_status()
data = r.json()
if not data.get("episodes"):
return None
episode_data = data["episodes"][0]
subtitle = episode_data.get("subtitle", "")
year = (episode_data.get("release_date_time", "") or "").split("-")[0]
series_match = next(re.finditer(r"Series (\d+).*?:|Season (\d+).*?:|(\d{4}/\d{2}): Episode \d+", subtitle), None)
season_num = 0
if series_match:
season_str = next(g for g in series_match.groups() if g is not None)
season_num = int(season_str.replace("/", ""))
elif not data.get("slices"): # Fallback for single-season shows
season_num = 1
num_match = next(re.finditer(r"(\d+)\.|Episode (\d+)", subtitle), None)
number = 0
if num_match:
number = int(next(g for g in num_match.groups() if g is not None))
else:
number = episode_data.get("numeric_tleo_position", 0)
name_match = re.search(r"\d+\. (.+)", subtitle)
name = ""
if name_match:
name = name_match.group(1)
elif not re.search(r"Series \d+: Episode \d+", subtitle):
name = subtitle
return Episode(
id_=episode_data.get("id"),
service=self.__class__,
title=episode_data.get("title"),
season=season_num,
number=number,
name=name,
language="en",
year=year,
)
def get_episodes(self, episode_ids: list) -> list[Episode]:
"""Fetches multiple episodes concurrently."""
with ThreadPoolExecutor(max_workers=10) as executor:
tasks = executor.map(self.fetch_episode, episode_ids)
return [task for task in tasks if task is not None]
def find(self, pattern, string, group=None):
if group:
m = re.search(pattern, string)
if m:
return m.group(group)
else:
return next(iter(re.findall(pattern, string)), None)
class iPlayerError(Exception):
"""Base exception for this service."""
pass
class CertificateMissingError(iPlayerError):
"""Raised when an TLS certificate is required but not provided."""
pass
class NoStreamsAvailableError(iPlayerError):
"""Raised when no playable streams are found for a title."""
pass
class MetadataError(iPlayerError):
"""Raised when metadata for a title cannot be found."""
pass
@@ -1,56 +0,0 @@
base_url: https://www.bbc.co.uk/iplayer/{type}/{pid}
user_agent: smarttv_AFTMM_Build_0003255372676_Chromium_41.0.2250.2
api_key: D2FgtcTxGqqIgLsfBWTJdrQh2tVdeaAp
endpoints:
episodes: https://ibl.api.bbci.co.uk/ibl/v1/episodes/{pid}?rights=mobile&availability=available
metadata: https://graph.ibl.api.bbc.co.uk/
playlist: https://www.bbc.co.uk/programmes/{pid}/playlist.json
open: https://{}/mediaselector/6/select/version/2.0/mediaset/{}/vpid/{}/
secure: https://{}/mediaselector/6/select/version/2.0/vpid/{}/format/json/mediaset/{}/proto/https
search: https://ibl.api.bbc.co.uk/ibl/v1/new-search
certificate: |
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlFT3pDQ0F5T2dBd0lCQWdJQkFUQU5CZ2txaGtpRzl3MEJBUVVGQURDQm96RU
xNQWtHQTFVRUJoTUNWVk14DQpFekFSQmdOVkJBZ1RDa05oYkdsbWIzSnVhV0V4RWpBUUJnTlZCQWNUQ1VOMWNHVnlkR2x1YnpFZU1C
d0dBMVVFDQpDeE1WVUhKdlpDQlNiMjkwSUVObGNuUnBabWxqWVhSbE1Sa3dGd1lEVlFRTEV4QkVhV2RwZEdGc0lGQnliMlIxDQpZM1
J6TVE4d0RRWURWUVFLRXdaQmJXRjZiMjR4SHpBZEJnTlZCQU1URmtGdFlYcHZiaUJHYVhKbFZGWWdVbTl2DQpkRU5CTURFd0hoY05N
VFF4TURFMU1EQTFPREkyV2hjTk16UXhNREV3TURBMU9ESTJXakNCbVRFTE1Ba0dBMVVFDQpCaE1DVlZNeEV6QVJCZ05WQkFnVENrTm
hiR2xtYjNKdWFXRXhFakFRQmdOVkJBY1RDVU4xY0dWeWRHbHViekVkDQpNQnNHQTFVRUN4TVVSR1YySUZKdmIzUWdRMlZ5ZEdsbWFX
TmhkR1V4R1RBWEJnTlZCQXNURUVScFoybDBZV3dnDQpVSEp2WkhWamRITXhEekFOQmdOVkJBb1RCa0Z0WVhwdmJqRVdNQlFHQTFVRU
F4TU5SbWx5WlZSV1VISnZaREF3DQpNVENDQVNBd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFTkFEQ0NBUWdDZ2dFQkFNRFZTNUwwVUR4
WnMwNkpGMld2DQpuZE1KajdIVGRlSlg5b0ltWWg3aytNY0VENXZ5OTA2M0p5c3FkS0tsbzVJZERvY2tuczg0VEhWNlNCVkFBaTBEDQ
p6cEI4dHRJNUFBM1l3djFZUDJiOThpQ3F2OWhQalZndE9nNHFvMXZkK0oxdFdISUh5ZkV6cWlPRXVXNTlVd2xoDQpVTmFvY3JtZGNx
bGcyWmIyZ1VybTZ2dlZqUThZcjQzY29MNnBBMk5ESXNyT0Z4c0ZZaXdaVk12cDZqMlk4dnFrDQpFOHJ2Tm04c3JkY0FhZjRXdHBuYW
gyZ3RBY3IrdTVYNExZdmEwTzZrNGhENEdnNHZQQ2xQZ0JXbDZFSHRBdnFDDQpGWm9KbDhMNTN2VVY1QWhMQjdKQk0wUTFXVERINWs4
NWNYT2tFd042NDhuZ09hZUtPMGxqYndZVG52NHhDV2NlDQo2RXNDQVFPamdZTXdnWUF3SHdZRFZSMGpCQmd3Rm9BVVo2RFJJSlNLK2
hmWCtHVnBycWlubGMraTVmZ3dIUVlEDQpWUjBPQkJZRUZOeUNPZkhja3Vpclp2QXF6TzBXbjZLTmtlR1BNQWtHQTFVZEV3UUNNQUF3
RXdZRFZSMGxCQXd3DQpDZ1lJS3dZQkJRVUhBd0l3RVFZSllJWklBWWI0UWdFQkJBUURBZ2VBTUFzR0ExVWREd1FFQXdJSGdEQU5CZ2
txDQpoa2lHOXcwQkFRVUZBQU9DQVFFQXZXUHd4b1VhV3IwV0tXRXhHdHpQOElGVUUrZis5SUZjSzNoWXl2QmxLOUxODQo3Ym9WZHhx
dWJGeEgzMFNmOC90VnNYMUpBOUM3bnMzZ09jV2Z0dTEzeUtzK0RnZGhqdG5GVkgraW4zNkVpZEZBDQpRRzM1UE1PU0ltNGNaVXkwME
4xRXRwVGpGY2VBbmF1ZjVJTTZNZmRBWlQ0RXNsL09OUHp5VGJYdHRCVlpBQmsxDQpXV2VHMEcwNDdUVlV6M2Ira0dOVTNzZEs5Ri9o
NmRiS3c0azdlZWJMZi9KNjZKSnlkQUhybFhJdVd6R2tDbjFqDQozNWdHRHlQajd5MDZWNXV6MlUzYjlMZTdZWENnNkJCanBRN0wrRW
d3OVVsSmpoN1pRMXU2R2RCNUEwcGFWM0VQDQpQTk1KN2J6Rkl1cHozdklPdk5nUVV4ZWs1SUVIczZKeXdjNXByck5MS3c9PQ0KLS0t
LS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0KLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdlFJQkFEQU5CZ2txaGtpRzl3ME
JBUUVGQUFTQ0JLY3dnZ1NqQWdFQUFvSUJBUURBMVV1UzlGQThXYk5PDQppUmRscjUzVENZK3gwM1hpVi9hQ0ptSWU1UGpIQkErYjh2
ZE90eWNyS25TaXBhT1NIUTZISko3UE9FeDFla2dWDQpRQUl0QTg2UWZMYlNPUUFOMk1MOVdEOW0vZklncXIvWVQ0MVlMVG9PS3FOYj
NmaWRiVmh5QjhueE02b2poTGx1DQpmVk1KWVZEV3FISzVuWEtwWU5tVzlvRks1dXI3MVkwUEdLK04zS0MrcVFOalF5TEt6aGNiQldJ
c0dWVEw2ZW85DQptUEw2cEJQSzd6WnZMSzNYQUduK0ZyYVoyb2RvTFFISy9ydVYrQzJMMnREdXBPSVErQm9PTHp3cFQ0QVZwZWhCDQ
o3UUw2Z2hXYUNaZkMrZDcxRmVRSVN3ZXlRVE5FTlZrd3grWlBPWEZ6cEJNRGV1UEo0RG1uaWp0SlkyOEdFNTcrDQpNUWxuSHVoTEFn
RURBb0lCQVFDQWpqSmgrRFY5a1NJMFcyVHVkUlBpQmwvTDRrNlc1VThCYnV3VW1LWGFBclVTDQpvZm8wZWhvY3h2aHNibTBNRTE4RX
d4U0tKWWhPVVlWamdBRnpWOThLL2M4MjBLcXo1ZGRUa0NwRXFVd1Z4eXFRDQpOUWpsYzN3SmNjSTlQcVcrU09XaFdvYWd6UndYcmRE
MFU0eXc2NHM1eGFIUkU2SEdRSkVQVHdEY21mSDlOK0JXDQovdVU4YVc1QWZOcHhqRzduSGF0cmhJQjU1cDZuNHNFNUVoTjBnSk9WMD
lmMEdOb1pQUVhiT1VVcEJWOU1jQ2FsDQpsK1VTalpBRmRIbUlqWFBwR1FEelJJWTViY1hVQzBZYlRwaytRSmhrZ1RjSW1LRFJmd0FC
YXRIdnlMeDlpaVY1DQp0ZWZoV1hhaDE4STdkbUF3TmRTN0U4QlpoL3d5MlIwNXQ0RHppYjlyQW9HQkFPU25yZXAybk1VRVAyNXdSQW
RBDQozWDUxenYwOFNLWkh6b0VuNExRS1krLzg5VFRGOHZWS2wwQjZLWWlaYW14aWJqU1RtaDRCWHI4ZndRaytiazFCDQpReEZ3ZHVG
eTd1MU43d0hSNU45WEFpNEtuamgxQStHcW9SYjg4bk43b1htekM3cTZzdFZRUk9peDJlRVFJWTVvDQpiREZUellaRnloNGlMdkU0bj
V1WnVHL1JBb0dCQU5mazdHMDhvYlpacmsxSXJIVXZSQmVENzZRNDlzQ0lSMGRBDQpIU0hCZjBadFBEMjdGSEZtamFDN0YwWkM2QXdU
RnBNL0FNWDR4UlpqNnhGalltYnlENGN3MFpGZ08rb0pwZjFIDQpFajNHSHdMNHFZekJFUXdRTmswSk9GbE84cDdVMm1ZL2hEVXM3bG
JQQm82YUo4VVpJMGs3SHhSOVRWYVhud0h1DQovaXhnRjlsYkFvR0JBSmh2eVViNXZkaXRmNTcxZ3ErQWs2bWozMU45aGNRdjN3REZR
SGdHN1Vxb28zaUQ5MDR4DQp1aXI4RzdCbVJ2THNTWGhpWnI2cmxIOXFnTERVU1lqV0xMWksrZXVoOUo0ejlLdmhReitQVnNsY2FYcj
RyVUVjDQphMlNvb2FKU2E2WjNYU2NuSWVPSzJKc2hPK3RnRmw3d1NDRGlpUVF1aHI3QmRLRFFhbWU3MEVxTEFvR0JBSS90DQo4dk45
d1NRN3lZamJIYU4wMkErdFNtMTdUeXNGaE5vcXZoYUEvNFJJMHRQU0RhRHZDUlhTRDRRc21ySzNaR0lxDQpBSVA3TGc3dFIyRHM3RV
NoWDY5MTRRdVZmVWF4R1ZPRXR0UFphZ0g3RzdNcllMSzFlWWl3MER1Sjl4U041dTdWDQpBczRkOURuZldiUm14UzRRd2pEU0ZMaFRp
T1JsRkt2MHFYTHF1cERuQW9HQWVFa3J4SjhJaXdhVEhnWXltM21TDQprU2h5anNWK01tVkJsVHNRK0ZabjFTM3k0YVdxbERhNUtMZF
QvWDEwQXg4NHNQTmVtQVFVMGV4YTN0OHM5bHdIDQorT3NEaktLb3hqQ1Q3S2wzckdQeUFISnJmVlZ5U2VFZVgrOERLZFZKcjByU1Bk
Qkk4Y2tFQ3kzQXpsVmphK3d3DQpST0N0emMxVHVyeG5OQTVxV0QzbjNmND0NCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg==