MA + NF changes
This commit is contained in:
parent
7766581007
commit
3910a39571
BIN
vinetrimmer/devices/hisense_smarttv_32e5600eu_sl3000.prd
Normal file
BIN
vinetrimmer/devices/hisense_smarttv_32e5600eu_sl3000.prd
Normal file
Binary file not shown.
Binary file not shown.
BIN
vinetrimmer/devices/hisense_smarttv_ltdn55k2203gwus_sl2000.prd
Normal file
BIN
vinetrimmer/devices/hisense_smarttv_ltdn55k2203gwus_sl2000.prd
Normal file
Binary file not shown.
BIN
vinetrimmer/devices/ktc_t31_43f_sl3000.prd
Normal file
BIN
vinetrimmer/devices/ktc_t31_43f_sl3000.prd
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,12 +2,20 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import click
|
import click
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
from requests import JSONDecodeError
|
from requests import JSONDecodeError
|
||||||
from httpx import URL
|
from httpx import URL
|
||||||
import uuid
|
import uuid
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
import yt_dlp
|
||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from langcodes import Language
|
from langcodes import Language
|
||||||
from vinetrimmer.objects import AudioTrack, TextTrack, Title, Tracks, VideoTrack
|
from vinetrimmer.objects import AudioTrack, TextTrack, Title, Tracks, VideoTrack
|
||||||
@ -27,7 +35,7 @@ class MoviesAnywhere(BaseService):
|
|||||||
Accounts can only mount services when its US based though.
|
Accounts can only mount services when its US based though.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ALIASES = ["MA", "moviesanywhere"]
|
ALIASES = ["MA", "MoviesAnywhere"]
|
||||||
|
|
||||||
TITLE_RE = r"https://moviesanywhere\.com(?P<id>.+)"
|
TITLE_RE = r"https://moviesanywhere\.com(?P<id>.+)"
|
||||||
|
|
||||||
@ -57,6 +65,12 @@ class MoviesAnywhere(BaseService):
|
|||||||
self.atmos = ctx.parent.params["atmos"]
|
self.atmos = ctx.parent.params["atmos"]
|
||||||
self.vcodec = ctx.parent.params["vcodec"]
|
self.vcodec = ctx.parent.params["vcodec"]
|
||||||
self.acodec = ctx.parent.params["acodec"]
|
self.acodec = ctx.parent.params["acodec"]
|
||||||
|
self.range = ctx.parent.params["range_"]
|
||||||
|
self.quality = ctx.parent.params["quality"] or 1080
|
||||||
|
|
||||||
|
if self.range != "SDR" or self.quality > 1080:
|
||||||
|
self.log.info(" + Setting VideoCodec to H265")
|
||||||
|
self.vcodec = "H265"
|
||||||
|
|
||||||
def get_titles(self):
|
def get_titles(self):
|
||||||
self.headers={
|
self.headers={
|
||||||
@ -108,8 +122,6 @@ class MoviesAnywhere(BaseService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_pssh_init(self, url):
|
def get_pssh_init(self, url):
|
||||||
import os, yt_dlp
|
|
||||||
from pathlib import Path
|
|
||||||
init = 'init.mp4'
|
init = 'init.mp4'
|
||||||
|
|
||||||
files_to_delete = [init]
|
files_to_delete = [init]
|
||||||
@ -117,12 +129,6 @@ class MoviesAnywhere(BaseService):
|
|||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
os.remove(file_name)
|
os.remove(file_name)
|
||||||
|
|
||||||
def read_pssh(path: str):
|
|
||||||
raw = Path(path).read_bytes()
|
|
||||||
wv = raw.rfind(bytes.fromhex('edef8ba979d64acea3c827dcd51d21ed'))
|
|
||||||
if wv == -1: return None
|
|
||||||
return base64.b64encode(raw[wv-12:wv-12+raw[wv-9]]).decode('utf-8')
|
|
||||||
|
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'format': 'bestvideo[ext=mp4]/bestaudio[ext=m4a]/best',
|
'format': 'bestvideo[ext=mp4]/bestaudio[ext=m4a]/best',
|
||||||
'allow_unplayable_formats': True,
|
'allow_unplayable_formats': True,
|
||||||
@ -141,12 +147,81 @@ class MoviesAnywhere(BaseService):
|
|||||||
raise ValueError("Failed to download the video")
|
raise ValueError("Failed to download the video")
|
||||||
video_file_name = ydl.prepare_filename(info_dict)
|
video_file_name = ydl.prepare_filename(info_dict)
|
||||||
|
|
||||||
pssh = read_pssh(init)
|
raw = Path(init).read_bytes()
|
||||||
|
wv = raw.rfind(bytes.fromhex('edef8ba979d64acea3c827dcd51d21ed'))
|
||||||
|
if wv != -1:
|
||||||
|
psshWV = base64.b64encode(raw[wv-12:wv-12+raw[wv-9]]).decode('utf-8')
|
||||||
|
|
||||||
|
playready_system_id = binascii.unhexlify("9A04F07998404286AB92E65BE0885F95")
|
||||||
|
pssh_boxes = []
|
||||||
|
mp4_file = "init.mp4"
|
||||||
|
|
||||||
|
with open(mp4_file, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
while index < len(data):
|
||||||
|
if index + 8 > len(data):
|
||||||
|
break
|
||||||
|
|
||||||
|
box_size, box_type = struct.unpack_from(">I4s", data, index)
|
||||||
|
if box_size < 8 or index + box_size > len(data):
|
||||||
|
break
|
||||||
|
|
||||||
|
if box_type == b'moov' or box_type == b'moof':
|
||||||
|
sub_index = index + 8
|
||||||
|
while sub_index < index + box_size:
|
||||||
|
sub_size, sub_type = struct.unpack_from(">I4s", data, sub_index)
|
||||||
|
if sub_type == b'pssh':
|
||||||
|
system_id = data[sub_index + 12: sub_index + 28]
|
||||||
|
if system_id == playready_system_id:
|
||||||
|
pssh_data_size = struct.unpack_from(">I", data, sub_index + 28)[0]
|
||||||
|
pssh_data = data[sub_index + 32: sub_index + 32 + pssh_data_size]
|
||||||
|
pssh_boxes.append(pssh_data)
|
||||||
|
sub_index += sub_size
|
||||||
|
|
||||||
|
if box_type == b'pssh':
|
||||||
|
system_id = data[index + 12: index + 28]
|
||||||
|
if system_id == playready_system_id:
|
||||||
|
pssh_data_size = struct.unpack_from(">I", data, index + 28)[0]
|
||||||
|
pssh_data = data[index + 32: index + 32 + pssh_data_size]
|
||||||
|
pssh_boxes.append(pssh_data)
|
||||||
|
|
||||||
|
index += box_size
|
||||||
|
|
||||||
|
if pssh_boxes:
|
||||||
|
for i, pssh_data in enumerate(pssh_boxes):
|
||||||
|
pssh_box = (
|
||||||
|
struct.pack(">I", len(pssh_data) + 32) +
|
||||||
|
b"pssh" +
|
||||||
|
struct.pack(">I", 0) +
|
||||||
|
playready_system_id +
|
||||||
|
struct.pack(">I", len(pssh_data)) +
|
||||||
|
pssh_data
|
||||||
|
)
|
||||||
|
base64_pssh = base64.b64encode(pssh_box).decode()
|
||||||
|
#print(base64_pssh)
|
||||||
|
psshPR = base64_pssh
|
||||||
|
|
||||||
|
header_offset = 6
|
||||||
|
xml_data = pssh_data[header_offset:].decode("utf-16le", errors='ignore')
|
||||||
|
xml_start = xml_data.find("<WRMHEADER")
|
||||||
|
xml_end = xml_data.find("</WRMHEADER>")
|
||||||
|
|
||||||
|
if xml_start != -1 and xml_end != -1:
|
||||||
|
xml_content = xml_data[xml_start:xml_end + len("</WRMHEADER>")]
|
||||||
|
xml_root = ET.fromstring(xml_content)
|
||||||
|
#print(ET.tostring(xml_root, encoding="utf-8").decode())
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to locate XML content in PSSH.")
|
||||||
|
else:
|
||||||
|
raise Exception("No PlayReady PSSH boxes found.")
|
||||||
|
|
||||||
|
|
||||||
for file_name in files_to_delete:
|
for file_name in files_to_delete:
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
os.remove(file_name)
|
os.remove(file_name)
|
||||||
return pssh
|
return psshWV, psshPR
|
||||||
|
|
||||||
def get_tracks(self, title):
|
def get_tracks(self, title):
|
||||||
player_data = self.content["data"]["page"]["components"][0]["mainAction"]["playerData"]["playable"]
|
player_data = self.content["data"]["page"]["components"][0]["mainAction"]["playerData"]["playable"]
|
||||||
@ -163,9 +238,9 @@ class MoviesAnywhere(BaseService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for video in tracks.videos:
|
for video in tracks.videos:
|
||||||
pssh = self.get_pssh_init(manifest["url"])
|
psshWV, psshPR = self.get_pssh_init(manifest["url"])
|
||||||
video_pssh = Box.parse(base64.b64decode(pssh))
|
video.psshWV = psshWV
|
||||||
video.pssh = video_pssh
|
video.psshPR = psshPR
|
||||||
video.license_url = manifest["playreadyLaUrl"] if self.playready else manifest["widevineLaUrl"]
|
video.license_url = manifest["playreadyLaUrl"] if self.playready else manifest["widevineLaUrl"]
|
||||||
video.contentId = URL(video.license_url).params._dict["ContentId"][
|
video.contentId = URL(video.license_url).params._dict["ContentId"][
|
||||||
0
|
0
|
||||||
@ -173,7 +248,8 @@ class MoviesAnywhere(BaseService):
|
|||||||
videos += [video]
|
videos += [video]
|
||||||
# Extract Atmos audio track if available.
|
# Extract Atmos audio track if available.
|
||||||
for audio in tracks.audios:
|
for audio in tracks.audios:
|
||||||
audio.pssh = video_pssh
|
audio.psshWV = psshWV
|
||||||
|
audio.psshPR = psshPR
|
||||||
audio.license_url = manifest["playreadyLaUrl"] if self.playready else manifest["widevineLaUrl"]
|
audio.license_url = manifest["playreadyLaUrl"] if self.playready else manifest["widevineLaUrl"]
|
||||||
audio.contentId = URL(audio.license_url).params._dict["ContentId"][
|
audio.contentId = URL(audio.license_url).params._dict["ContentId"][
|
||||||
0
|
0
|
||||||
@ -215,11 +291,39 @@ class MoviesAnywhere(BaseService):
|
|||||||
return None # will use common privacy cert
|
return None # will use common privacy cert
|
||||||
|
|
||||||
def license(self, challenge: bytes, track: Tracks, **_) -> bytes:
|
def license(self, challenge: bytes, track: Tracks, **_) -> bytes:
|
||||||
license_message = self.session.post(
|
if not isinstance(challenge, bytes):
|
||||||
|
challenge = bytes(challenge, 'utf-8')
|
||||||
|
|
||||||
|
playback_session_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
license_message = requests.post(
|
||||||
|
headers = {
|
||||||
|
'accept': '*/*',
|
||||||
|
'accept-language': 'en-US,en;q=0.9,en-IN;q=0.8',
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
'content-type': 'application/octet-stream',
|
||||||
|
'dnt': '1',
|
||||||
|
'origin': 'https://moviesanywhere.com',
|
||||||
|
'pragma': 'no-cache',
|
||||||
|
'priority': 'u=1, i',
|
||||||
|
'referer': 'https://moviesanywhere.com/',
|
||||||
|
'sec-ch-ua-mobile': '?0',
|
||||||
|
'sec-fetch-dest': 'empty',
|
||||||
|
'sec-fetch-mode': 'cors',
|
||||||
|
'sec-fetch-site': 'same-site',
|
||||||
|
'soapaction': '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"',
|
||||||
|
'user_agent': 'Dalvik/2.1.0 (Linux; U; Android 14; SM-S911B Build/UP1A.231005.007)',
|
||||||
|
},
|
||||||
|
params = {
|
||||||
|
"authorization": self.access_token,
|
||||||
|
"playbackSessionId": playback_session_id
|
||||||
|
},
|
||||||
url=track.license_url,
|
url=track.license_url,
|
||||||
data=challenge, # expects bytes
|
data=challenge, # expects bytes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.log.debug(license_message.text)
|
||||||
|
|
||||||
if "errorCode" in license_message.text:
|
if "errorCode" in license_message.text:
|
||||||
self.log.exit(f" - Cannot complete license request: {license_message.text}")
|
self.log.exit(f" - Cannot complete license request: {license_message.text}")
|
||||||
|
|
||||||
|
|||||||
@ -790,8 +790,8 @@ class Netflix(BaseService):
|
|||||||
needs_repack=False,
|
needs_repack=False,
|
||||||
# decryption
|
# decryption
|
||||||
encrypted=x["isDrm"],
|
encrypted=x["isDrm"],
|
||||||
psshPR=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if psshPR else None,
|
psshPR=manifest["video_tracks"][0]["drmHeader"]["bytes"] if psshPR else None,
|
||||||
psshWV=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if not psshPR else None,
|
psshWV=manifest["video_tracks"][0]["drmHeader"]["bytes"] if not psshPR else None,
|
||||||
kid=x["drmHeaderId"] if x["isDrm"] else None,
|
kid=x["drmHeaderId"] if x["isDrm"] else None,
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -811,8 +811,8 @@ class Netflix(BaseService):
|
|||||||
needs_repack=False,
|
needs_repack=False,
|
||||||
# decryption
|
# decryption
|
||||||
encrypted=x["isDrm"],
|
encrypted=x["isDrm"],
|
||||||
psshPR=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if psshPR else None,
|
psshPR=manifest["video_tracks"][0]["drmHeader"]["bytes"] if psshPR else None,
|
||||||
psshWV=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if not psshPR else None,
|
psshWV=manifest["video_tracks"][0]["drmHeader"]["bytes"] if not psshPR else None,
|
||||||
kid=x.get("drmHeaderId") if x["isDrm"] else None,
|
kid=x.get("drmHeaderId") if x["isDrm"] else None,
|
||||||
) for x in manifest["audio_tracks"]]
|
) for x in manifest["audio_tracks"]]
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import httpx
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@ -40,22 +41,26 @@ class MSL:
|
|||||||
"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:AES256-SHA"
|
"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:AES256-SHA"
|
||||||
)
|
)
|
||||||
|
|
||||||
class TlsAdapter(HTTPAdapter):
|
#class TlsAdapter(HTTPAdapter):
|
||||||
|
# def __init__(self, ssl_options=0, **kwargs):
|
||||||
def __init__(self, ssl_options=0, **kwargs):
|
# self.ssl_options = ssl_options
|
||||||
self.ssl_options = ssl_options
|
# super(TlsAdapter, self).__init__(**kwargs)
|
||||||
super(TlsAdapter, self).__init__(**kwargs)
|
# def init_poolmanager(self, *pool_args, **pool_kwargs):
|
||||||
|
# ctx = ssl_.create_urllib3_context(ciphers=CIPHERS, cert_reqs=ssl.CERT_REQUIRED, options=self.ssl_options)
|
||||||
def init_poolmanager(self, *pool_args, **pool_kwargs):
|
# self.poolmanager = PoolManager(*pool_args,
|
||||||
ctx = ssl_.create_urllib3_context(ciphers=CIPHERS, cert_reqs=ssl.CERT_REQUIRED, options=self.ssl_options)
|
# ssl_context=ctx,
|
||||||
self.poolmanager = PoolManager(*pool_args,
|
# **pool_kwargs)
|
||||||
ssl_context=ctx,
|
|
||||||
**pool_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
session = requests.session()
|
#session = requests.session()
|
||||||
adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
|
#adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
|
||||||
session.mount("https://", adapter)
|
#session.mount("https://", adapter)
|
||||||
|
session = httpx.Client(
|
||||||
|
params=session.params,
|
||||||
|
headers=session.headers,
|
||||||
|
cookies=session.cookies,
|
||||||
|
verify=True
|
||||||
|
)
|
||||||
self.session = session
|
self.session = session
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
|
|||||||
@ -14,6 +14,7 @@ cdm:
|
|||||||
DisneyPlus: 'mtc_mtc_atv_atv_sl3000'
|
DisneyPlus: 'mtc_mtc_atv_atv_sl3000'
|
||||||
Sunnxt: 'xiaomi_mi_a1_15.0.0_60ceee88_8159_l3'
|
Sunnxt: 'xiaomi_mi_a1_15.0.0_60ceee88_8159_l3'
|
||||||
iTunes: 'mtc_mtc_atv_atv_sl3000'
|
iTunes: 'mtc_mtc_atv_atv_sl3000'
|
||||||
|
MoviesAnywhere: 'ktc_t31_43f_sl3000'
|
||||||
|
|
||||||
cdm_api:
|
cdm_api:
|
||||||
- name: 'playready'
|
- name: 'playready'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user