diff --git a/packages/envied/src/envied/services/ITV/__init__.py b/packages/envied/src/envied/services/ITV/__init__.py index c7b1492..489a74e 100644 --- a/packages/envied/src/envied/services/ITV/__init__.py +++ b/packages/envied/src/envied/services/ITV/__init__.py @@ -11,6 +11,7 @@ from typing import Any, Optional, Union import click from bs4 import BeautifulSoup from click import Context +from envied.core.cdm.detect import is_playready_cdm from envied.core.credential import Credential from envied.core.manifests.dash import DASH from envied.core.search_result import SearchResult @@ -24,11 +25,16 @@ class ITV(Service): Service code for ITVx streaming service (https://www.itv.com/). \b - Version: 1.0.3 + Version: 1.0.4 Author: stabbedbybrick Authorization: Cookies (Optional for free content | Required for premium content) Robustness: - L3: 1080p + Widevine: + L1: 1080p + L3: 720p + PlayReady: + SL3000: 1080p + SL2000: 720p \b Tips: @@ -45,11 +51,6 @@ class ITV(Service): - 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",) @@ -70,6 +71,10 @@ class ITV(Service): if not self.profile: self.profile = "default" + self.cdm = ctx.obj.cdm + self.drm_system = "playready" if is_playready_cdm(self.cdm) else "widevine" + self.security_level = f"SL{self.cdm.security_level}" if self.drm_system == "playready" else f"L{self.cdm.security_level}" + self.session.headers.update(self.config["headers"]) def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None: @@ -261,15 +266,15 @@ class ITV(Service): "player": "dash", "featureset": [ "mpeg-dash", - "widevine", + self.drm_system, "outband-webvtt", "hd", "single-track", ], "platformTag": "ctv", "drm": { - "system": "widevine", - "maxSupported": "L3", + "system": self.drm_system, + "maxSupported": self.security_level, }, }, } @@ -327,6 +332,12 @@ class ITV(Service): if r.status_code != 200: raise ConnectionError(r.text) return r.content + + def get_playready_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 diff --git a/packages/envied/src/envied/services/SBS/__init__.py b/packages/envied/src/envied/services/SBS/__init__.py index d0c526b..daf87d3 100644 --- a/packages/envied/src/envied/services/SBS/__init__.py +++ b/packages/envied/src/envied/services/SBS/__init__.py @@ -4,18 +4,20 @@ import hashlib import json import re from collections.abc import Generator +from http.cookiejar import MozillaCookieJar +from typing import Any, Optional from urllib.parse import urljoin, urlparse -from typing import Any import click +import jwt from click import Context from requests import Request +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 Chapters, Subtitle, Tracks -from envied.core.utils.xml import load_xml class SBS(Service): @@ -24,9 +26,9 @@ class SBS(Service): Service code for SBS ondemand streaming service (https://www.sbs.com.au/ondemand/). \b - Version: 1.0.1 + Version: 1.0.2 Author: stabbedbybrick - Authorization: None + Authorization: Credentials Geofence: AU (API and downloads) Robustness: AES: 720p, AAC2.0 @@ -39,11 +41,6 @@ class SBS(Service): MOVIE: https://www.sbs.com.au/ondemand/movie/silence/1363535939614 SPORT: https://www.sbs.com.au/ondemand/sports-series/australian-championship-2025/football-australian-championship-2025/australian-championship-2025-s2025-ep40/2457638979614 - \b - Notes: - - SBS 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. - """ GEOFENCE = ("au",) @@ -61,6 +58,51 @@ class SBS(Service): 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}") + + if cache and not cache.expired: + self.log.info(" + Using cached tokens") + tokens = cache.data + elif cache and cache.expired: + self.log.info("Refreshing cached tokens...") + payload = { + "refreshToken": cache.data["refreshToken"], + "deviceName": "Android TV", + "refreshTokenInBody": True, + } + + r = self.session.post( + self.config["endpoints"]["refresh"], + json=payload, + ) + r.raise_for_status() + tokens = r.json() + expiry = jwt.decode(tokens.get("accessToken"), options={"verify_signature": False}).get("exp") + cache.set(tokens, expiration=expiry - 60) + else: + self.log.info(" + Logging in...") + payload = { + "email": credential.username, + "password": credential.password, + "deviceName": "Android TV", + "refreshTokenInBody": True, + } + + response = self.session.post( + self.config["endpoints"]["auth"], json=payload + ) + response.raise_for_status() + tokens = response.json() + expiry = jwt.decode(tokens.get("accessToken"), options={"verify_signature": False}).get("exp") + cache.set(tokens, expiration=expiry - 60) + + self.session.headers.update({"Authorization": "Bearer {}".format(tokens["accessToken"])}) + def search(self) -> Generator[SearchResult, None, None]: params = { "q": self.title.strip(), @@ -88,7 +130,7 @@ class SBS(Service): def get_titles(self) -> Movies | Series: regex = re.compile( r"^https://www.sbs.com.au/ondemand/" - r"(?Ptv-series|tv-program|sports-series|movie|watch)" + r"(?Ptv-series|tv-program|sports-series|sports-program|movie|watch)" r"(?:/|/.*/)" r"(?P[^/]+)/?$" ) @@ -99,7 +141,7 @@ class SBS(Service): entity_type, entity_id = (match.group(i) for i in ("entity", "id")) - if entity_type in ("movie", "tv-program") and entity_id.isdigit(): + if entity_type in ("movie", "tv-program", "sports-program") and entity_id.isdigit(): movie = self._movie(entity_id) return Movies(movie) @@ -112,28 +154,39 @@ class SBS(Service): return Series(episodes) def get_tracks(self, title: Movie | Episode) -> Tracks: - smil = self._request("GET", f"/api/v3/video_smil?id={title.id}") + json_data = { + "deviceClass": "androidtv", + "advertising": { + "headerBidding": True, + "telariaID": "", + "subtitle": "en", + "resume": False, + }, + "streamOptions": { + "audio": "demuxed", + }, + } + content = self._request("POST", f"{self.config['endpoints']['playback']}/{title.id}", json=json_data) + provider = next((x for x in content.get("streamProviders", []) if x.get("type", "").lower() == "hls"), None) - body = load_xml(smil).find("body").find("seq") - section = body.find("par") or body + if not provider: + raise ValueError("Unable to find manifest for this title") - manifest = next((x.get("src") for x in section.findall("video")), None) - subtitles = [(x.get("src"), x.get("lang"), x.get("type")) for x in section.findall("textstream")] - + manifest = provider.get("url") tracks = HLS.from_url(manifest, self.session).to_tracks(title.language) - if subtitles: - for url, lang, type in subtitles: - if "ttaf+xml" in type: - continue - codec = type.split("/")[-1] + if subtitles := provider.get("textTracks", []): + for subtitle in subtitles: + url = subtitle.get("url") + lang = subtitle.get("lang") + codec = subtitle.get("url").split(".")[-1] tracks.add( Subtitle( id_=hashlib.md5(url.encode()).hexdigest()[0:6], url=url, - codec=Subtitle.Codec.from_mime(codec), + codec=Subtitle.Codec.from_codecs(codec), language=lang, - sdh="_CC" in url, + sdh="CC" in subtitle.get("name", ""), ) ) diff --git a/packages/envied/src/envied/services/SBS/config.yaml b/packages/envied/src/envied/services/SBS/config.yaml index fad5044..cd76e1c 100644 --- a/packages/envied/src/envied/services/SBS/config.yaml +++ b/packages/envied/src/envied/services/SBS/config.yaml @@ -1,7 +1,10 @@ headers: - User-Agent: "AndroidTV/!/!" + User-Agent: "okhttp/4.10.0" + x-api-key: "ce3f270a-31fb-486e-855e-4f608a62640e" # mobile: "325a2c15-17ad-48d7-802e-87e8ffe0e6fe", web: 49a46461-b9eb-4904-b519-176c59c386ef endpoints: base_url: "https://www.sbs.com.au" - dai: "http://pubads.g.doubleclick.net/ondemand/hls/content/2488267/vid/{vid}/streams" + playback: "https://playback.pr.sbsod.com/stream" catalogue: "https://catalogue.pr.sbsod.com" + auth: "https://auth.sbs.com.au/login" + refresh: "https://auth.sbs.com.au/refresh"