mirror of
https://github.com/vinefeeder/TwinVine.git
synced 2026-07-15 18:10:04 +02:00
updates
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -328,6 +333,12 @@ class ITV(Service):
|
||||
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
|
||||
|
||||
def get_data(self, url: str) -> dict:
|
||||
|
||||
@@ -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"(?P<entity>tv-series|tv-program|sports-series|movie|watch)"
|
||||
r"(?P<entity>tv-series|tv-program|sports-series|sports-program|movie|watch)"
|
||||
r"(?:/|/.*/)"
|
||||
r"(?P<id>[^/]+)/?$"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
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")]
|
||||
if not provider:
|
||||
raise ValueError("Unable to find manifest for this title")
|
||||
|
||||
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", ""),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user