Upload files to "Services"

This commit is contained in:
Mike 2024-11-29 12:05:34 +00:00
parent be0166fcb2
commit 9536ab8e52
2 changed files with 442 additions and 0 deletions

416
Services/skyshowtime.py Normal file
View File

@ -0,0 +1,416 @@
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import re
import time
import requests,json
import os
from datetime import datetime
import click
from click import Context
from vinetrimmer.objects import MenuTrack, Title, Tracks
from vinetrimmer.services.BaseService import BaseService
class Skyshowtime(BaseService):
"""
Service code for NBC's Skyshowtime streaming service (https://skyshowtime.com).
Edited L4M
\b
Authorization: Cookies
Security: UHD@-- FHD@L3, doesn't care about releases.
"""
ALIASES = ["SKY", "Skyshowtime"]
#GEOFENCE = ["es"]
VIDEO_RANGE_MAP = {
"DV": "DOLBY_VISION"
}
AUDIO_CODEC_MAP = {
"AAC": "mp4a",
"AC3": "ac-3",
"EC3": "ec-3"
}
@staticmethod
@click.command(name="Skyshowtime", short_help="https://skyshowtime.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 Skyshowtime(ctx, **kwargs)
def __init__(self, ctx, title: str, movie: bool):
self.title = title
self.movie = movie
super().__init__(ctx)
self.profile = ctx.obj.profile
self.range = ctx.parent.params["range_"]
self.vcodec = ctx.parent.params["vcodec"]
self.acodec = ctx.parent.params["acodec"]
if (ctx.parent.params.get("quality") or 0) > 1080 and self.vcodec != "H265":
self.log.info(" + Switched video codec to H265 to be able to get 2160p video track")
self.vcodec = "H265"
if self.range in ("HDR10", "DV") and self.vcodec != "H265":
self.log.info(f" + Switched video codec to H265 to be able to get {self.range} dynamic range")
self.vcodec = "H265"
self.service_config = None
self.hmac_key: bytes
self.tokens: dict
self.license_api = None
self.license_bt = None
self.configure()
def get_titles(self):
# Title is a slug, example: `/tv/the-office/4902514835143843112`.
res = self.session.get(
url=self.config["endpoints"]["node"],
params={
"slug": self.title,#'provider_series_id/'+self.title.split('/')[3],
"represent": "(items(items))",
# "represent": "(items(items),recs[take=8],collections(items(items[take=8])),trailers)"
},
headers={
"Accept": "*",
"Referer": f"https://www.skyshowtime.com/watch/asset{self.title}",
"x-skyott-Activeterritory": self.config["client"]["activeterritory"],
"x-skyott-device": self.config["client"]["device"],
"x-skyott-language": self.config["client"]["language"],
"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"]
}
)
if not res.ok:
self.log.exit(f" - HTTP Error {res.status_code}: {res.reason}")
raise
data = res.json()
titles = []
if "relationships" in data:
for season in data["relationships"]["items"]["data"]:
for episode in season["relationships"]["items"]["data"]:
titles.append(episode)
else:
return [Title(
id_=self.title,
type_=Title.Types.MOVIE,
name=data["attributes"]["title"],
year=data["attributes"].get("year"),
original_lang="en", # TODO: Don't assume
source=self.ALIASES[0],
service_data=data
)]
return [Title(
id_=self.title,
type_=Title.Types.TV,
name=data["attributes"]["title"],
year=x["attributes"].get("year"),
season=x["attributes"].get("seasonNumber"),
episode=x["attributes"].get("episodeNumber"),
episode_name=x["attributes"].get("title"),
original_lang="en", # TODO: Don't assume
source=self.ALIASES[0],
service_data=x
) for x in titles]
def get_tracks(self, title: Title) -> Tracks:
content_id = title.service_data["attributes"]["formats"]["HD"]["contentId"]
variant_id = title.service_data["attributes"]["providerVariantId"]
sky_headers = {
# order of these matter!
"x-skyott-Activeterritory": self.config["client"]["activeterritory"],
"x-skyott-agent": ".".join([
self.config["client"]["proposition"].lower(),
self.config["client"]["device"].lower(),
self.config["client"]["platform"].lower()
]),
# "x-skyott-coppa": "false",
"x-skyott-device": self.config["client"]["device"],
"x-skyott-language": self.config["client"]["language"],
"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"]
}
body = json.dumps({
"contentId": content_id,
"providerVariantId": variant_id,
"device": {
"capabilities": [
{
"transport": "DASH",
"protection": "NONE",
"vcodec": "H265",
"acodec": "AAC",
"container": "ISOBMFF"
},
{
"transport": "DASH",
"protection": "WIDEVINE",
"vcodec": "H265",
"acodec": "AAC",
"container": "ISOBMFF"
},
{
"transport": "DASH",
"protection": "NONE",
"vcodec": "H264",
"acodec": "AAC",
"container": "ISOBMFF"
},
{
"transport": "DASH",
"protection": "WIDEVINE",
"vcodec": "H264",
"acodec": "AAC",
"container": "ISOBMFF"
},
],
"maxVideoFormat": "HD",
"hdcpEnabled": "false",
},
"client": {
"thirdParties": [
"COMSCORE",
"CONVIVA",
"FREEWHEEL"
]
},
"personaParentalControlRating": 9
}, 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:
self.log.exit(f" - An error occurred: {manifest['description']} [{manifest['errorCode']}]")
raise
self.license_api = manifest["protection"]["licenceAcquisitionUrl"]
self.license_bt = manifest["protection"]["licenceToken"]
tracks = Tracks.from_mpd(
url=manifest["asset"]["endpoints"][0]["url"]+'&audio=all&subtitle=all',
session=self.session,
#lang=title.original_lang,
source=self.ALIASES[0]
)
for track in tracks:
track.needs_proxy = True
if self.acodec:
tracks.audios = [
x for x in tracks.audios
if x.codec and x.codec[:4] == self.AUDIO_CODEC_MAP[self.acodec]
]
return tracks
def get_chapters(self, title: Title) -> list[MenuTrack]:
return []
def certificate(self, challenge, **_):
return self.license(challenge)
def license(self, challenge: bytes, **_) -> bytes:
assert self.license_api is not None
return 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 # expects bytes
).content
# Service specific functions
def configure(self) -> None:
self.session.headers.update({"Origin": "https://www.skyshowtime.com"})
self.log.info("Getting Skyshowtime 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"],
platform=self.config["client"]["platform"],
version=self.config["client"]["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():
# self.log.info(" - Failed! Cookies might be outdated.")
# raise
@staticmethod
def calculate_sky_header_md5(headers: dict) -> str:
if len(headers.items()) > 0:
headers_str = "\n".join(list(map(lambda x: f"{x[0].lower()}: {x[1]}", headers.items()))) + "\n"
else:
headers_str = "{}"
return str(hashlib.md5(headers_str.encode()).hexdigest())
@staticmethod
def calculate_body_md5(body: str) -> str:
return str(hashlib.md5(body.encode()).hexdigest())
def calculate_signature(self, msg: str) -> str:
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: str, path: str, sky_headers: dict, body: str, timestamp: int) -> str:
data = "\n".join([
method.upper(),
path,
"", # important!
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
tokens_cache_path = self.get_cache("tokens_{profile}_{id}.json".format(
profile=self.profile,
id=self.config["client"]["id"]
))
if os.path.isfile(tokens_cache_path):
with open(tokens_cache_path, encoding="utf-8") as fd:
tokens = json.load(fd)
tokens_expiration = tokens.get("tokenExpiryTime", None)
if tokens_expiration and datetime.strptime(tokens_expiration, "%Y-%m-%dT%H:%M:%S.%fZ") > datetime.now():
return tokens
# Get all SkyOTT headers
sky_headers = {
# order of these matter!
"x-skyott-Activeterritory": self.config["client"]["activeterritory"],
"x-skyott-device": self.config["client"]["device"],
"x-skyott-language": self.config["client"]["language"],
"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"]
}
# Craft the body data that will be sent to the tokens endpoint, being minified and order matters!
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"],
},
"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()
os.makedirs(os.path.dirname(tokens_cache_path), exist_ok=True)
with open(tokens_cache_path, "w", encoding="utf-8") as fd:
json.dump(tokens, fd)
return tokens
def verify_tokens(self) -> bool:
"""Verify the tokens by calling the /auth/users/me endpoint and seeing if it works"""
sky_headers = {
# order of these matter!
"x-skyott-Activeterritory": self.config["client"]["activeterritory"],
"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"]
}
me = 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 me.status_code == 200

26
Services/skyshowtime.yml Normal file
View File

@ -0,0 +1,26 @@
endpoints:
config: 'https://config.clients.skyshowtime.com/GLOBAL/{provider}/{proposition}/{platform}/PROD/{version}/config.json'
tokens: 'https://ovp.skyshowtime.com/auth/tokens'
me: 'https://ovp.skyshowtime.com/auth/users/me'
node: 'https://atom.skyshowtime.com/adapter-calypso/v3/query/node/'
vod: 'https://ovp.skyshowtime.com/video/playouts/vod'
client:
version: '4.1.10'
config_version: '4.1.11'
territory: 'RO'
activeterritory: 'RO'
language: 'en-US'
provider: 'SKYSHOWTIME'
proposition: 'SKYSHOWTIME'
platform: 'ANDROID'
device: 'MOBILE'
id: 'Ptudy2gGV4nNa9nUyFbl'
drm_device_id: 'UNKNOWN'
client_sdk: 'SKYSHOWTIME-ANDROID-v1'
auth_scheme: 'MESSO'
auth_issuer: 'NOWTV'
security:
signature_hmac_key_v4: 'jfj9qGg6aDHaBbFpH6wNEvN6cHuHtZVppHRvBgZs'
signature_format: 'SkyOTT client="{client}",signature="{signature}",timestamp="{timestamp}",version="1.0"'