Upload files to "Services"
This commit is contained in:
parent
41fdef6696
commit
0bd08b7c2a
236
Services/mubi.py
Normal file
236
Services/mubi.py
Normal file
@ -0,0 +1,236 @@
|
||||
import base64
|
||||
import json
|
||||
import click
|
||||
import os
|
||||
|
||||
import uuid
|
||||
import xmltodict
|
||||
|
||||
from langcodes import Language
|
||||
from vinetrimmer.objects import AudioTrack, TextTrack, Title, Tracks, VideoTrack
|
||||
from vinetrimmer.services.BaseService import BaseService
|
||||
from vinetrimmer.vendor.pymp4.parser import Box
|
||||
|
||||
|
||||
class Mubi(BaseService):
|
||||
"""
|
||||
Made By redd / Edit by superman
|
||||
and Widevine Group - Chrome CDM API dont share this
|
||||
|
||||
|
||||
\b
|
||||
Authorization: Credentials
|
||||
Security: UHD@L3, doesn't care about releases.
|
||||
"""
|
||||
|
||||
ALIASES = ["MUBI"]
|
||||
|
||||
TITLE_RE = [
|
||||
r"^(?:https?://(?:www\.)?mubi\.com\/)?(?P<id>[a-z0-9-]+)",
|
||||
# r"^(?:https?://(?:www\.)?mubi\.com/([a-z0-9-]+/[a-z0-9-]+/films)/)?(?P<id>[a-z0-9-]+)?" with country code url
|
||||
# r"^(?:https?://(?:www\.)?mubi\.com/(films)/)?(?P<id>[a-z0-9-]+)?"
|
||||
]
|
||||
|
||||
AUDIO_CODEC_MAP = {
|
||||
"AAC": "mp4a",
|
||||
"AC3": "ac-3",
|
||||
"EC3": "ec-3"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@click.command(name="Mubi", short_help="https://mubi.com/")
|
||||
@click.argument("title", type=str, required=False)
|
||||
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
return Mubi(ctx, **kwargs)
|
||||
|
||||
def __init__(self, ctx, title):
|
||||
super().__init__(ctx)
|
||||
self.parse_title(ctx, title)
|
||||
|
||||
|
||||
self.vcodec = ctx.parent.params["vcodec"].lower()
|
||||
self.acodec = ctx.parent.params["acodec"]
|
||||
self.range = ctx.parent.params["range_"]
|
||||
self.bearer= None
|
||||
self.dtinfo= None
|
||||
self.quality = ctx.parent.params["quality"]
|
||||
self.headers = {
|
||||
"accept": "application/json",
|
||||
"accept-language": "en-US",
|
||||
"client": self.config["device"]["client_name"],
|
||||
"client-version": "20.2",
|
||||
"client-device-identifier": self.config["device"]["device_identifier"],
|
||||
"client-app": "mubi",
|
||||
"client-device-brand": "Google",
|
||||
'client-accept-audio-codecs': 'ac3',
|
||||
"client-device-model": self.config["device"]["device_model"],
|
||||
"client-device-os": self.config["device"]["device_os"],
|
||||
"client-country": "US",
|
||||
"content-type": "application/json; charset=UTF-8",
|
||||
"host": "api.mubi.com",
|
||||
"connection": "Keep-Alive",
|
||||
"accept-encoding": "gzip",
|
||||
"user-agent": self.config["device"]["user_agent"],
|
||||
}
|
||||
if self.vcodec=="vp9":
|
||||
self.headers["client-accept-video-codecs"]="vp9"
|
||||
elif self.vcodec=="h265":
|
||||
self.headers["client-accept-video-codecs"]="h265"
|
||||
elif self.vcodec=="h264":
|
||||
self.headers["client-accept-video-codecs"]="h264"
|
||||
else:
|
||||
self.headers["client-accept-video-codecs"]="vp9,h265,h264"
|
||||
|
||||
|
||||
self.configure()
|
||||
|
||||
|
||||
def get_titles(self):
|
||||
|
||||
self.log.info(" + Getting Metadata.")
|
||||
res = self.session.get(
|
||||
self.config["endpoints"]["metadata"].format(title_id=self.title)
|
||||
,headers=self.headers).json()
|
||||
try:
|
||||
res
|
||||
except json.JSONDecodeError:
|
||||
raise self.log.exit(f" - Failed to load title metadata: {res.text}")
|
||||
|
||||
original_language = res["title_locale"]
|
||||
self.title = res['id']
|
||||
return Title(
|
||||
id_=self.title,
|
||||
type_=Title.Types.MOVIE,
|
||||
name=res["original_title"],
|
||||
year=res["year"],
|
||||
original_lang=original_language,
|
||||
source=self.ALIASES[0],
|
||||
service_data=res,
|
||||
)
|
||||
|
||||
def create_pssh_from_kid(self, kid: str):
|
||||
WV_SYSTEM_ID = [237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]
|
||||
kid = uuid.UUID(kid).bytes
|
||||
|
||||
init_data = bytearray(b'\x12\x10')
|
||||
init_data.extend(kid)
|
||||
init_data.extend(b'H\xe3\xdc\x95\x9b\x06')
|
||||
|
||||
pssh = bytearray([0, 0, 0])
|
||||
pssh.append(32 + len(init_data))
|
||||
pssh[4:] = bytearray(b'pssh')
|
||||
pssh[8:] = [0, 0, 0, 0]
|
||||
pssh[13:] = WV_SYSTEM_ID
|
||||
pssh[29:] = [0, 0, 0, 0]
|
||||
pssh[31] = len(init_data)
|
||||
pssh[32:] = init_data
|
||||
|
||||
return base64.b64encode(pssh).decode('UTF-8')
|
||||
|
||||
def get_pssh_from_mpd(self, mpd_url):
|
||||
r = self.session.get(mpd_url, headers=self.headers)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise Exception(r.text)
|
||||
|
||||
mpd = xmltodict.parse(r.text, dict_constructor=dict)
|
||||
|
||||
for adaption in mpd['MPD']['Period']['AdaptationSet']:
|
||||
if adaption['@mimeType'] == 'video/mp4':
|
||||
if 'ContentProtection' in adaption:
|
||||
for protection in adaption['ContentProtection']:
|
||||
if protection['@schemeIdUri'].lower() == 'urn:mpeg:dash:mp4protection:2011':
|
||||
return self.create_pssh_from_kid(protection['@cenc:default_KID'])
|
||||
|
||||
|
||||
def get_tracks(self, title):
|
||||
|
||||
self.session.post(self.config["endpoints"]["viewing"].format(title_id=self.title),headers=self.headers)
|
||||
|
||||
program_data = self.session.get(
|
||||
self.config["endpoints"]["manifest"].format(title_id=self.title),headers=self.headers
|
||||
).json()
|
||||
|
||||
if self.quality==2160:
|
||||
mpd_url=program_data["url"].replace("AVC1.1080p.mpd","vp09.2160p.mpd")
|
||||
else:
|
||||
mpd_url=program_data["url"]
|
||||
|
||||
pssh = self.get_pssh_from_mpd(mpd_url)
|
||||
video_pssh = Box.parse(base64.b64decode(pssh))
|
||||
|
||||
tracks=Tracks.from_mpd(
|
||||
url=mpd_url,
|
||||
session=self.session,
|
||||
source=self.ALIASES[0],
|
||||
)
|
||||
|
||||
for track in tracks.videos:
|
||||
if not track.pssh:
|
||||
track.pssh = video_pssh
|
||||
|
||||
for track in tracks.audios:
|
||||
if not track.pssh:
|
||||
track.pssh = video_pssh
|
||||
|
||||
if self.acodec:
|
||||
tracks.audios = [x for x in tracks.audios if (x.codec or "")[:4] == self.AUDIO_CODEC_MAP[self.acodec]]
|
||||
|
||||
return tracks
|
||||
|
||||
def get_chapters(self, title):
|
||||
return []
|
||||
|
||||
def certificate(self, **kwargs):
|
||||
# TODO: Hardcode the certificate
|
||||
# return self.license(**kwargs)
|
||||
return None
|
||||
|
||||
def license(self, challenge, **_):
|
||||
|
||||
lic = self.session.post(
|
||||
url=self.config["endpoints"]["license"],
|
||||
headers={"dt-custom-data": self.dtinfo},
|
||||
data=challenge # expects bytes
|
||||
)
|
||||
|
||||
return lic.content # bytes
|
||||
|
||||
|
||||
def configure(self):
|
||||
|
||||
tokens_cache_path = self.get_cache("tokens_mubi.json")
|
||||
self.log.info(" + Loading Cached Token...")
|
||||
if os.path.isfile(tokens_cache_path):
|
||||
with open(tokens_cache_path, encoding="utf-8") as fd:
|
||||
tokens = json.load(fd)
|
||||
self.bearer=tokens["authorization"]
|
||||
self.dtinfo=tokens["dt-custom-data"]
|
||||
self.headers["authorization"]=self.bearer
|
||||
|
||||
else:
|
||||
self.log.info(" + Retrieving API configuration")
|
||||
if not self.credentials.username:
|
||||
raise self.log.exit(" - No cookies provided, cannot log in.")
|
||||
req_payload = "{\"identifier\":\"%s\",\"magic_link\":true}" % self.credentials.username
|
||||
auth_resp = self.session.post(url=self.config["endpoints"]["authtok_url"], data=req_payload, headers=self.headers).json()
|
||||
payload = "{\"auth_request_token\":\"%s\",\"identifier\":\"%s\",\"password\":\"%s\"}" % (auth_resp["auth_request_token"], self.credentials.username, self.credentials.password)
|
||||
response = self.session.post(url=self.config["endpoints"]["loginurl"], data=payload, headers=self.headers).json()
|
||||
json_str = {
|
||||
|
||||
"merchant":"mubi",
|
||||
"sessionId":response["token"],
|
||||
"userId":str(response["user"]["id"])
|
||||
}
|
||||
|
||||
self.bearer="Bearer "+response["token"]
|
||||
self.dtinfo = base64.b64encode((str(json_str).replace("'", '"').encode('utf-8'))).decode('utf-8')
|
||||
|
||||
save_data={"authorization": f"{self.bearer}","dt-custom-data":self.dtinfo}
|
||||
|
||||
os.makedirs(os.path.dirname(tokens_cache_path), exist_ok=True)
|
||||
with open(tokens_cache_path, "w", encoding="utf-8") as fd:
|
||||
json.dump(save_data, fd)
|
||||
|
||||
16
Services/mubi.yml
Normal file
16
Services/mubi.yml
Normal file
@ -0,0 +1,16 @@
|
||||
needs_auth: false
|
||||
|
||||
endpoints:
|
||||
loginurl: 'https://api.mubi.com/v3/authentication/login'
|
||||
authtok_url: 'https://api.mubi.com/v3/authentication/login/request'
|
||||
metadata: 'https://api.mubi.com/v3/films/{title_id}'
|
||||
viewing: 'https://api.mubi.com/v3/films/{title_id}/viewing'
|
||||
manifest: 'https://api.mubi.com/v3/films/{title_id}/viewing/secure_url'
|
||||
license: 'https://lic.drmtoday.com/license-proxy-widevine/cenc/?specConform=true'
|
||||
|
||||
device:
|
||||
device_identifier: '421c9b8e-67b2-4d61-b1c5-02cb319a1894'
|
||||
device_model: 'Android SDK built for x86'
|
||||
client_name: 'android'
|
||||
device_os: '13'
|
||||
user_agent: 'okhttp/4.9.2'
|
||||
Loading…
Reference in New Issue
Block a user