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 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}")

View File

@ -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"]]

View File

@ -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

View File

@ -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'