MA + NF changes

This commit is contained in:
chu23465 2025-05-12 14:41:21 +05:30
parent 7766581007
commit 3910a39571
10 changed files with 730 additions and 620 deletions

Binary file not shown.

Binary file not shown.

View File

@ -2,12 +2,20 @@ import base64
import json
import click
import re
import requests
from requests import JSONDecodeError
from httpx import URL
import uuid
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
from datetime import datetime
from langcodes import Language
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.
"""
ALIASES = ["MA", "moviesanywhere"]
ALIASES = ["MA", "MoviesAnywhere"]
TITLE_RE = r"https://moviesanywhere\.com(?P<id>.+)"
@ -57,6 +65,12 @@ class MoviesAnywhere(BaseService):
self.atmos = ctx.parent.params["atmos"]
self.vcodec = ctx.parent.params["vcodec"]
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):
self.headers={
@ -108,8 +122,6 @@ class MoviesAnywhere(BaseService):
)
def get_pssh_init(self, url):
import os, yt_dlp
from pathlib import Path
init = 'init.mp4'
files_to_delete = [init]
@ -117,12 +129,6 @@ class MoviesAnywhere(BaseService):
if os.path.exists(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 = {
'format': 'bestvideo[ext=mp4]/bestaudio[ext=m4a]/best',
'allow_unplayable_formats': True,
@ -141,12 +147,81 @@ class MoviesAnywhere(BaseService):
raise ValueError("Failed to download the video")
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:
if os.path.exists(file_name):
os.remove(file_name)
return pssh
return psshWV, psshPR
def get_tracks(self, title):
player_data = self.content["data"]["page"]["components"][0]["mainAction"]["playerData"]["playable"]
@ -163,9 +238,9 @@ class MoviesAnywhere(BaseService):
)
for video in tracks.videos:
pssh = self.get_pssh_init(manifest["url"])
video_pssh = Box.parse(base64.b64decode(pssh))
video.pssh = video_pssh
psshWV, psshPR = self.get_pssh_init(manifest["url"])
video.psshWV = psshWV
video.psshPR = psshPR
video.license_url = manifest["playreadyLaUrl"] if self.playready else manifest["widevineLaUrl"]
video.contentId = URL(video.license_url).params._dict["ContentId"][
0
@ -173,7 +248,8 @@ class MoviesAnywhere(BaseService):
videos += [video]
# Extract Atmos audio track if available.
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.contentId = URL(audio.license_url).params._dict["ContentId"][
0
@ -215,11 +291,39 @@ class MoviesAnywhere(BaseService):
return None # will use common privacy cert
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,
data=challenge, # expects bytes
)
self.log.debug(license_message.text)
if "errorCode" in license_message.text:
self.log.exit(f" - Cannot complete license request: {license_message.text}")

View File

@ -790,8 +790,8 @@ class Netflix(BaseService):
needs_repack=False,
# decryption
encrypted=x["isDrm"],
psshPR=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if psshPR else None,
psshWV=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if not psshPR else None,
psshPR=manifest["video_tracks"][0]["drmHeader"]["bytes"] if psshPR else None,
psshWV=manifest["video_tracks"][0]["drmHeader"]["bytes"] if not psshPR else None,
kid=x["drmHeaderId"] if x["isDrm"] else None,
))
@ -811,8 +811,8 @@ class Netflix(BaseService):
needs_repack=False,
# decryption
encrypted=x["isDrm"],
psshPR=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if psshPR else None,
psshWV=base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"]) if not psshPR else None,
psshPR=manifest["video_tracks"][0]["drmHeader"]["bytes"] if psshPR else None,
psshWV=manifest["video_tracks"][0]["drmHeader"]["bytes"] if not psshPR else None,
kid=x.get("drmHeaderId") if x["isDrm"] else None,
) for x in manifest["audio_tracks"]]

View File

@ -4,6 +4,7 @@ import json
import logging
import os
import random
import httpx
import re
import sys
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"
)
class TlsAdapter(HTTPAdapter):
def __init__(self, ssl_options=0, **kwargs):
self.ssl_options = ssl_options
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)
self.poolmanager = PoolManager(*pool_args,
ssl_context=ctx,
**pool_kwargs)
#class TlsAdapter(HTTPAdapter):
# def __init__(self, ssl_options=0, **kwargs):
# self.ssl_options = ssl_options
# 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)
# self.poolmanager = PoolManager(*pool_args,
# ssl_context=ctx,
# **pool_kwargs)
session = requests.session()
adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
session.mount("https://", adapter)
#session = requests.session()
#adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
#session.mount("https://", adapter)
session = httpx.Client(
params=session.params,
headers=session.headers,
cookies=session.cookies,
verify=True
)
self.session = session
self.endpoint = endpoint
self.sender = sender

View File

@ -14,6 +14,7 @@ cdm:
DisneyPlus: 'mtc_mtc_atv_atv_sl3000'
Sunnxt: 'xiaomi_mi_a1_15.0.0_60ceee88_8159_l3'
iTunes: 'mtc_mtc_atv_atv_sl3000'
MoviesAnywhere: 'ktc_t31_43f_sl3000'
cdm_api:
- name: 'playready'