This commit is contained in:
widevinedump 2021-12-25 10:43:24 +05:30
parent 98dd671f16
commit 4297654052
41 changed files with 9534 additions and 2 deletions

2
DOWNLOAD.cmd Normal file
View File

@ -0,0 +1,2 @@
python bad37.py --url https://www.paramountplus.com/shows/mayor-of-kingstown/ -s 1 -e 3 --alang es-la en --slang es-la en --flang es-la
pause

1
KEYS/PARAMOUNTPLUS.txt Normal file
View File

@ -0,0 +1 @@
##### One KEY per line. #####

View File

@ -1,2 +1,38 @@
# Paramount Plus 4k Downloader
Tool To Get Downloads up to 4k from Paramount+
<div size='20px'> Paramount 4K Downloader
</div>
<div size='20px'> Tool To Get Downloads up to 4k from Paramount+ :smile:
</div>
<p align="center">
<img width="200" src="https://github.com/Kathryn-Jie/Kathryn-Jie/blob/main/kathryn.png">
</p>
<h1> Hello Fellow < Developers/ >! <img src = "https://raw.githubusercontent.com/MartinHeinz/MartinHeinz/master/wave.gif" width = 30px> </h1>
<p align='center'>
</p>
<div size='20px'> Hi! My name is WVDUMP. I am Leaking the scripts to punish few idiots :smile:
</div>
<h2> About Me <img src = "https://media0.giphy.com/media/KDDpcKigbfFpnejZs6/giphy.gif?cid=ecf05e47oy6f4zjs8g1qoiystc56cu7r9tb8a1fe76e05oty&rid=giphy.gif" width = 100px></h2>
<img width="55%" align="right" alt="Github" src="https://raw.githubusercontent.com/onimur/.github/master/.resources/git-header.svg" />
- 🔭 Im currently working on Java scripts
- 🌱 Im currently learning Python
- 👯 Sharing is caring
- ⚡ CDM IS NOT INCLUDED BUY it from wvfuck@cyberfiends.net ⚡
<br>
<br>
<br>

128
bad37.py Normal file
View File

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Module: BAD Project
# Created on: 01-06-2021
# Authors: JUNi
# Version: 1.0
import argparse
import os
import sys
parser = argparse.ArgumentParser()
#Common:
parser.add_argument("content", nargs="?", help="Content URL or ID")
parser.add_argument("--url", dest="url_season", help="If set, it will download all assets from the season provided.")
parser.add_argument("--tqdm", dest="tqmd_mode", help="If set, will download with threading", action="store_true")
parser.add_argument("--nv", "--no-video", dest="novideo", help="If set, don't download video", action="store_true")
parser.add_argument("--na", "--no-audio", dest="noaudio", help="If set, don't download audio", action="store_true")
parser.add_argument("--ns", "--no-subs", dest="nosubs", help="If set, don't download subs", action="store_true")
parser.add_argument("--all-season", dest="all_season", help="If set, active download mode.", action="store_true")
parser.add_argument("-e", "--episode", dest="episodeStart", help="If set, it will start downloading the season from that episode.")
parser.add_argument("-s", dest="season", help="If set, it will download all assets from the season provided.")
parser.add_argument("--tag", type=str, required=False, help="Release group tag to use for filenames")
parser.add_argument("-q", "--quality", dest="customquality", type=lambda x: [x.rstrip('p')], help="For configure quality of video.", default=[])
parser.add_argument("-o", "--output", dest="output", default="downloads", help="If set, it will download all assets to directory provided.")
parser.add_argument("--keep", dest="keep", help="If set, it will list all formats available.", action="store_true")
parser.add_argument("--info", help="If set, it will print manifest infos and exit.", action="store_true")
parser.add_argument("--no-mux", dest="nomux", help="If set, dont mux.", action="store_true")
#parser.add_argument("--force-mux", dest="force_mux", nargs=1, help="If set, force mux.", default=[])
#parser.add_argument("--langtag", dest="langtag", nargs=1, help="For configure language tag of MKV.", default=[])
parser.add_argument("--only-2ch-audio", dest="only_2ch_audio", help="If set, no clean tag subtitles.", action="store_true")
parser.add_argument("--alang", "--audio-language", dest="audiolang", nargs="*", help="If set, download only selected audio languages", default=[])
parser.add_argument("--slang", "--subtitle-language", dest="sublang", nargs="*", help="If set, download only selected subtitle languages", default=[])
parser.add_argument("--flang", "--forced-language", dest="forcedlang", nargs="*", help="If set, download only selected forced subtitle languages", default=[])
parser.add_argument("--no-cleansubs", dest="nocleansubs", help="If set, no clean tag subtitles.", action="store_true")
parser.add_argument("--hevc", dest="hevc", help="If set, it will return HEVC manifest", action="store_true")
parser.add_argument("--uhd", dest="uhd", help="If set, it will return UHD manifest", action="store_true")
parser.add_argument("--license", dest="license", help="Only print keys, don't download", action="store_true")
parser.add_argument("-licenses-as-json", help="Save the wv keys as json instead", action="store_true")
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
parser.add_argument("--aformat-51ch", "--audio-format-51ch", dest="aformat_51ch", help="For configure format of audio.")
parser.add_argument("--nc", "--no-chapters", dest="nochpaters", help="If set, don't download chapters", action="store_true")
parser.add_argument("-c", "--codec", choices=["widevine", "playready"], help="Video type to download", default="playready")
parser.add_argument("--ap", dest="audiocodec", default="atmos", choices=["aac", "ac3", "atmos"], help="audio codec profile")
#HBOMAX
parser.add_argument("--atmos", dest="atmos", help="If set, it will return Atmos MPDs", action="store_true")
parser.add_argument("--ad", "--desc-audio", action="store_true", dest="desc_audio", help="Download descriptive audio instead of normal dialogue")
parser.add_argument("--hdr", dest="hdr", help="If set, it will return HDR manifest", action="store_true")
parser.add_argument("-r", "--region", choices=["la", "us"], help="HBO Max video region", default="us")
parser.add_argument("--vp", dest="videocodec", default="h264", choices=["h264", "hevc", "hdr"], help="video codec profile")
#Clarovideo:
parser.add_argument("--m3u8", dest="m3u8mode", help="If set, it will return M3U8 manifest", action="store_true")
parser.add_argument("--file", dest="txtpath", help="If set, it will download links of an txt file")
#DisneyPlus:
parser.add_argument("--tlang", "--title-language", dest="titlelang", nargs=1, help="If set, it will change title language", default="es-419")
parser.add_argument("--scenario1", dest="scenarioDSNP", help="Video API from DisneyPlus", default="chromecast-drm-cbcs")
parser.add_argument("--scenario2", dest="scenarioSTAR", help="Video API from DisneyPlus", default="restricted-drm-ctr-sw")
#PROXY:
parser.add_argument("--proxy", dest="proxy", help="Proxy URL to use for both fetching metadata and downloading")
#proxy format: http://email@email:password@host:port
args = parser.parse_args()
if args.debug:
import logging
logging.basicConfig(level=logging.DEBUG)
currentFile = '__main__'
realPath = os.path.realpath(currentFile)
dirPath = os.path.dirname(realPath)
dirName = os.path.basename(dirPath)
if __name__ == "__main__":
if args.content:
args.url_season = args.content
if not args.url_season:
print('Please specify the URL of the content to download.')
sys.exit(1)
if (args.url_season and 'hbomax' in args.url_season):
mode = 'hbomax'
import hbomax
hbomax.main(args)
elif (args.url_season and 'clarovideo' in args.url_season):
mode = 'clarovideo'
import clarovideo
clarovideo.main(args)
elif (args.url_season and 'blim' in args.url_season):
mode = 'blimtv'
import blimtv
blimtv.main(args)
elif (args.url_season and 'nowonline' in args.url_season):
mode = 'nowonline'
import nowonline
nowonline.main(args)
elif (args.url_season and 'globo' in args.url_season):
mode = 'globoplay'
import globoplay
globoplay.main(args)
elif (args.url_season and 'paramountplus.com' in args.url_season):
mode = 'paramountplus'
import paramountplus
paramountplus.main(args)
elif (args.url_season and 'disneyplus' in args.url_season):
mode = 'disneyplus'
import disneyplus
disneyplus.main(args)
elif (args.url_season and 'starplus.com' in args.url_season):
mode = 'starplus'
import starplus
starplus.main(args)
elif (args.url_season and 'tv.apple.com' in args.url_season):
mode = 'appletv'
import appletv
appletv.main(args)
elif (args.url_season and 'telecine' in args.url_season):
mode = 'telecine'
import telecineplay
telecineplay.main(args)
else:
print("Error! This url or mode is not recognized.")
sys.exit(0)

0
cookies/cookies_pmnp.txt Normal file
View File

910
paramountplus.py Normal file
View File

@ -0,0 +1,910 @@
# -*- coding: utf-8 -*-
# Module: Paramount Plus Downloader
# Created on: 19-02-2021
# Authors: JUNi
# Version: 2.0
import urllib.parse
import re, base64, requests, sys, os
import subprocess, shutil
import xmltodict, isodate
import json, ffmpy, math
import http, html, time, pathlib, glob
from unidecode import unidecode
from http.cookiejar import MozillaCookieJar
from titlecase import titlecase
from pymediainfo import MediaInfo
from m3u8 import parse as m3u8parser
import pywidevine.clients.paramountplus.config as pmnp_cfg
from pywidevine.clients.proxy_config import ProxyConfig
from pywidevine.muxer.muxer import Muxer
from pywidevine.clients.paramountplus.downloader import WvDownloader
from pywidevine.clients.paramountplus.config import WvDownloaderConfig
currentFile = 'paramountplus'
realPath = os.path.realpath(currentFile)
dirPath = os.path.dirname(realPath)
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
SESSION = requests.Session()
def main(args):
global _id
proxies = {}
proxy_meta = args.proxy
if proxy_meta == 'none':
proxies['meta'] = {'http': None, 'https': None}
elif proxy_meta:
proxies['meta'] = {'http': proxy_meta, 'https': proxy_meta}
SESSION.proxies = proxies.get('meta')
proxy_cfg = ProxyConfig(proxies)
if not os.path.exists(dirPath + '/KEYS'):
os.makedirs(dirPath + '/KEYS')
else:
keys_file = dirPath + '/KEYS/PARAMOUNTPLUS.txt'
try:
keys_file_pmnp = open(keys_file, 'r')
keys_file_txt = keys_file_pmnp.readlines()
except Exception:
with open(keys_file, 'a', encoding='utf8') as (file):
file.write('##### One KEY per line. #####\n')
keys_file_pmnp = open(keys_file, 'r', encoding='utf8')
keys_file_txt = keys_file_pmnp.readlines()
def alphanumericSort(l):
def convert(text):
if text.isdigit():
return int(text)
else:
return text
def alphanum_key(key):
return [convert(c) for c in re.split('([0-9]+)', key)]
return sorted(l, key=alphanum_key)
def convert_size(size_bytes):
if size_bytes == 0:
return '0bps'
else:
s = round(size_bytes / 1000, 0)
return '%ikbps' % s
def get_size(size):
power = 1024
n = 0
Dic_powerN = {0:'', 1:'K', 2:'M', 3:'G', 4:'T'}
while size > power:
size /= power
n += 1
return str(round(size, 2)) + Dic_powerN[n] + 'B'
def getKeyId(name):
mp4dump = subprocess.Popen([pmnp_cfg.MP4DUMP, name], stdout=(subprocess.PIPE))
mp4dump = str(mp4dump.stdout.read())
A = find_str(mp4dump, 'default_KID')
KEY_ID_ORI = ''
KEY_ID_ORI = mp4dump[A:A + 63].replace('default_KID = ', '').replace('[', '').replace(']', '').replace(' ', '')
if KEY_ID_ORI == '' or KEY_ID_ORI == "'":
KEY_ID_ORI = 'nothing'
return KEY_ID_ORI
def find_str(s, char):
index = 0
if char in s:
c = char[0]
for ch in s:
if ch == c:
if s[index:index + len(char)] == char:
return index
index += 1
return -1
def mediainfo_(file):
mediainfo_output = subprocess.Popen([pmnp_cfg.MEDIAINFO, '--Output=JSON', '-f', file], stdout=(subprocess.PIPE))
mediainfo_json = json.load(mediainfo_output.stdout)
return mediainfo_json
def replace_words(x):
x = re.sub(r'[]¡!"#$%\'()*+,:;<=>¿?@\\^_`{|}~[-]', '', x)
x = x.replace('/', '-')
return unidecode(x)
def ReplaceSubs1(X):
pattern1 = re.compile('(?!<i>|<b>|<u>|<\\/i>|<\\/b>|<\\/u>)(<)(?:[A-Za-z0-9_ -=]*)(>)')
pattern2 = re.compile('(?!<\\/i>|<\\/b>|<\\/u>)(<\\/)(?:[A-Za-z0-9_ -=]*)(>)')
X = X.replace('&rlm;', '').replace('{\\an1}', '').replace('{\\an2}', '').replace('{\\an3}', '').replace('{\\an4}', '').replace('{\\an5}', '').replace('{\\an6}', '').replace('{\\an7}', '').replace('{\\an8}', '').replace('{\\an9}', '').replace('&lrm;', '')
X = pattern1.sub('', X)
X = pattern2.sub('', X)
return X
def replace_code_lang(X):
X = X.lower()
X = X.replace('es-mx', 'es-la').replace('pt-BR', 'pt-br').replace('dolby digital', 'en').replace('dd+', 'en')
return X
def get_cookies(file_path):
try:
cj = http.cookiejar.MozillaCookieJar(file_path)
cj.load()
except Exception:
print('\nCookies not found! Please dump the cookies with the Chrome extension https://chrome.google.com/webstore/detail/cookiestxt/njabckikapfpffapmjgojcnbfjonfjfg and place the generated file in ' + file_path)
print('\nWarning, do not click on "download all cookies", you have to click on "click here".\n')
sys.exit(0)
cookies = str()
for cookie in cj:
cookie.value = urllib.parse.unquote(html.unescape(cookie.value))
cookies = cookies + cookie.name + '=' + cookie.value + ';'
cookies = list(cookies)
del cookies[-1]
cookies = ''.join(cookies)
return cookies
cookies_file = 'cookies_pmnp.txt'
cookies = get_cookies(dirPath + '/cookies/' + cookies_file)
pmnp_headers = {
'Accept':'application/json, text/plain, */*',
'Access-Control-Allow-Origin':'*',
'cookie':cookies,
'User-Agent':USER_AGENT
}
def mpd_parsing(mpd_url):
base_url = mpd_url.split('stream.mpd')[0]
r = SESSION.get(url=mpd_url)
r.raise_for_status()
xml = xmltodict.parse(r.text)
mpdf = json.loads(json.dumps(xml))
length = isodate.parse_duration(mpdf['MPD']['@mediaPresentationDuration']).total_seconds()
tracks = mpdf['MPD']['Period']['AdaptationSet']
def get_pssh(track):
pssh = ''
for t in track["ContentProtection"]:
if t['@schemeIdUri'].lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
pssh = t["cenc:pssh"]
return pssh
def force_instance(x):
if isinstance(x['Representation'], list):
X = x['Representation']
else:
X = [x['Representation']]
return X
baseUrl = ''
video_list = []
for video_tracks in tracks:
if video_tracks['@contentType'] == 'video':
pssh = get_pssh(video_tracks)
for x in force_instance(video_tracks):
try:
codecs = x['@codecs']
except (KeyError, TypeError):
codecs = video_tracks['@codecs']
try:
baseUrl = x["BaseURL"]
except (KeyError, TypeError):
pass
video_dict = {
'Height':x['@height'],
'Width':x['@width'],
'Bandwidth':x['@bandwidth'],
'ID':x['@id'],
'TID':video_tracks['@id'],
'URL':baseUrl,
'Codec':codecs}
video_list.append(video_dict)
video_list = sorted(video_list, key=(lambda k: int(k['Bandwidth'])))
while args.customquality != [] and int(video_list[(-1)]['Height']) > int(args.customquality[0]):
video_list.pop(-1)
audio_list = []
for audio_tracks in tracks:
if audio_tracks['@contentType'] == 'audio':
for x in force_instance(audio_tracks):
try:
codecs = x['@codecs']
except (KeyError, TypeError):
codecs = audio_tracks['@codecs']
try:
baseUrl = x["BaseURL"]
except (KeyError, TypeError):
pass
audio_dict = {
'Bandwidth':x['@bandwidth'],
'ID':x['@id'],
'TID':audio_tracks['@id'],
'Language':replace_code_lang(audio_tracks['@lang']),
'URL':baseUrl,
'Codec':codecs}
audio_list.append(audio_dict)
audio_list = sorted(audio_list, key=(lambda k: (str(k['Language']))), reverse=True)
if args.only_2ch_audio:
c = 0
while c != len(audio_list):
if '-3' in audio_list[c]['Codec'].split('=')[0]:
audio_list.remove(audio_list[c])
else:
c += 1
BitrateList = []
AudioLanguageList = []
for x in audio_list:
BitrateList.append(x['Bandwidth'])
AudioLanguageList.append(x['Language'])
BitrateList = alphanumericSort(list(set(BitrateList)))
AudioLanguageList = alphanumericSort(list(set(AudioLanguageList)))
audioList_new = []
audio_Dict_new = {}
for y in AudioLanguageList:
counter = 0
for x in audio_list:
if x['Language'] == y and counter == 0:
audio_Dict_new = {
'Language':x['Language'],
'Bandwidth':x['Bandwidth'],
'Codec': x['Codec'],
'TID':x['TID'],
'URL':x['URL'],
'ID':x['ID']}
audioList_new.append(audio_Dict_new)
counter = counter + 1
audioList = audioList_new
audio_list = sorted(audioList, key=(lambda k: (int(k['Bandwidth']), str(k['Language']))))
audioList_new = []
if args.audiolang:
for x in audio_list:
langAbbrev = x['Language']
if langAbbrev in list(args.audiolang):
audioList_new.append(x)
audio_list = audioList_new
if 'precon' in mpd_url:
sub_url = mpd_url.replace('_cenc_precon_dash/stream.mpd', '_fp_precon_hls/master.m3u8')
else:
sub_url = mpd_url.replace('_cenc_dash/stream.mpd', '_fp_hls/master.m3u8')
print(sub_url)
return base_url, length, video_list, audio_list, get_subtitles(sub_url), pssh, mpdf
def download_subs(filename, sub_url):
urlm3u8_base_url = re.split('(/)(?i)', sub_url)
del urlm3u8_base_url[-1]
urlm3u8_base_url = ''.join(urlm3u8_base_url)
urlm3u8_request = requests.get(sub_url).text
m3u8_json = m3u8parser(urlm3u8_request)
urls = []
for segment in m3u8_json['segments']:
if 'https://' not in segment['uri']:
segment_url = urlm3u8_base_url + segment['uri']
urls.append(segment_url)
print('\n' + filename)
aria2c_infile = ""
num_segments = len(urls)
digits = math.floor(math.log10(num_segments)) + 1
for (i, url) in enumerate(urls):
aria2c_infile += f"{url}\n"
aria2c_infile += f"\tout={filename}.{i:0{digits}d}.vtt\n"
aria2c_infile += f"\tdir={filename}\n"
subprocess.run([pmnp_cfg.ARIA2C, "--allow-overwrite=true", "--file-allocation=none",
"--console-log-level=warn", "--download-result=hide", "--summary-interval=0",
"-x16", "-j16", "-s1", "-i-"],
input=aria2c_infile.encode("utf-8"))
source_files = pathlib.Path(filename).rglob(r'./*.vtt')
with open(filename + '.vtt', mode='wb') as (destination):
for vtt in source_files:
with open(vtt, mode='rb') as (source):
shutil.copyfileobj(source, destination)
if os.path.exists(filename):
shutil.rmtree(filename)
print('\nConverting subtitles...')
for f in glob.glob(f'{filename}*.vtt'):
with open(f, 'r+', encoding='utf-8-sig') as (x):
old = x.read().replace('STYLE\n::cue() {\nfont-family: Arial, Helvetica, sans-serif;\n}', '').replace('WEBVTT', '').replace('X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000', '').replace('\n\n\n', '\n')
with open(f, 'w+', encoding='utf-8-sig') as (x):
x.write(ReplaceSubs1(old))
SubtitleEdit_process = subprocess.Popen([pmnp_cfg.SUBTITLE_EDIT, '/convert', filename + ".vtt", "srt", "/MergeSameTexts"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).wait()
for f in glob.glob(f'{filename}*.vtt'):
os.remove(f)
'''
for f in glob.glob(f'{filename}*.srt'):
with open(f, 'r+', encoding='utf-8-sig') as (x):
old = x.read().replace('STYLE\n::cue() {\nfont-family: Arial, Helvetica, sans-serif;\n}', '').replace('WEBVTT', '').replace('\nX-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000\n', '').replace('\n\n\n', '\n')
with open(f, 'w+', encoding='utf-8-sig') as (x):
if not args.nocleansubs:
x.write(ReplaceSubs1(old))
else:
x.write(ReplaceSubs2(old))
for f in glob.glob(f'{filename}*.vtt'):
os.remove(f)
'''
print('Done!')
def get_episodes(ep_str, num_eps):
eps = ep_str.split(',')
eps_final = []
for ep in eps:
if '-' in ep:
(start, end) = ep.split('-')
start = int(start)
end = int(end or num_eps)
eps_final += list(range(start, end + 1))
else:
eps_final.append(int(ep))
return eps_final
_id = args.url_season.split('/')[-2]
if '/video/' in args.url_season:
content_regex = r'(\/shows\/)([\w-]+)(\/video\/)([\w-]+)'
url_match = re.search(content_regex, args.url_season)
_id = url_match[2]
def get_content_info():
if 'shows' in args.url_season:
pmnp_season_url = 'https://www.paramountplus.com/shows/{}/xhr/episodes/page/0/size/100/xs/0/season/{}/'.format(_id, '')
season_req = requests.get(url=pmnp_season_url, headers=pmnp_headers, proxies=proxy_cfg.get_proxy('meta'))
if not args.season:
args.season = 'all'
seasons = []
if args.season:
if args.season == 'all':
seasons = 'all'
elif ',' in args.season:
seasons = [int(x) for x in args.season.split(',')]
elif '-' in args.season:
(start, end) = args.season.split('-')
seasons = list(range(int(start), int(end) + 1))
else:
seasons = [int(args.season)]
if seasons == 'all':
seasons_list = [x['season_number'] for x in season_req.json()['result']['data']]
seasons = sorted(set(seasons_list))
for season_num in seasons:
pmnp_season_url = 'https://www.paramountplus.com/shows/{}/xhr/episodes/page/0/size/500/xs/0/season/{}/'.format(_id, season_num)
season_req = requests.get(url=pmnp_season_url, headers=pmnp_headers, proxies=proxy_cfg.get_proxy('meta'))
if season_req.json()['result']['total'] < 1:
print('This season doesnt exist!')
exit()
for num, ep in enumerate(season_req.json()['result']['data'], start=1):
episodeNumber = ep['episode_number']
seasonNumber = ep['season_number']
if ' - ' in ep['series_title']:
seriesTitle = ep['series_title'].split(' - ')[0]
else:
seriesTitle = ep['series_title']
episodeTitle = replace_words(ep['label'])
seriesName = f'{replace_words(seriesTitle)} S{seasonNumber:0>2}E{episodeNumber:0>2}'
folderName = f'{replace_words(seriesTitle)} S{seasonNumber:0>2}'
raw_url = urllib.parse.urljoin('https://www.paramountplus.com', ep['metaData']['contentUrl'])
episodes_list_new = []
episodes_dict = {
'id': ep['content_id'],
'raw_url': raw_url,
'pid':ep['metaData']['pid'],
'seriesName':seriesName,
'folderName':folderName,
'episodeNumber': num,
'seasonNumber':seasonNumber,
'pmnpType': 'show'}
episodes_list_new.append(episodes_dict)
episodes_list = []
for x in episodes_list_new:
episodes_list.append(x)
#episodes_list = sorted(episodes_list, key=lambda x: x['episodeNumber'])
if args.episodeStart:
eps = get_episodes(args.episodeStart, len(episodes_list))
episodes_list = [x for x in episodes_list if x['episodeNumber'] in eps]
if 'video' in args.url_season:
episodes_list = [x for x in episodes_list if x['id'] in url_match.group(4)]
for content_json in episodes_list:
start_process(content_json)
if 'movies' in args.url_season:
while 1:
resp = requests.get(url=args.url_season + '/', headers=pmnp_headers, proxies=proxy_cfg.get_proxy('meta'))
if resp.ok:
break
html_data = resp
html_data = html_data.text.replace('\r\n', '').replace('\n', '').replace('\r', '').replace('\t', '').replace(' ', '')
html_data_list = re.split('(</div>)(?i)', html_data)
json_web = []
for div in html_data_list:
if 'player.paramsVO.adCallParams' in div:
print()
rg = re.compile('(player.metaData = )(.*)(;player.tms_program_id)')
m = rg.search(div)
if m:
json_web = m.group(2)
json_web = json.loads(json_web)
content_dict = {}
episodes_list = []
year_regex = r'(\d{4})'
movieTitle = replace_words(json_web['seriesTitle'])
try:
r = re.search(year_regex, json_web['airdate'])
except KeyError:
r = re.search(year_regex, json_web['airdate_tv'])
seriesName = f'{movieTitle} ({r.group(0)})'
content_dict = {
'id':json_web['contentId'],
'raw_url': str(args.url_season),
'pid': json_web['pid'],
'seriesName':seriesName,
'folderName':None,
'episodeNumber':1,
'seasonNumber':1,
'pmnpType': 'movie'}
episodes_list.append(content_dict)
for content_json in episodes_list:
start_process(content_json)
def get_license(id_json):
while 1:
resp = requests.get(url=id_json['raw_url'], headers=pmnp_headers, proxies=proxy_cfg.get_proxy('meta'))
if resp.ok:
break
html_data = resp
html_data = html_data.text.replace('\r\n', '').replace('\n', '').replace('\r', '').replace('\t', '').replace(' ', '')
html_data_list = re.split('(</div>)(?i)', html_data)
json_web = []
for div in html_data_list:
if '(!window.CBS.Registry.drmPromise) {' in div:
rg = re.compile('(player.drm = )(.*)(;}player.enableCP)')
m = rg.search(div)
if m:
json_web = m.group(2)
json_web = json.loads(json_web)
lic_url = json_web['widevine']['url']
header_auth = json_web['widevine']['header']['Authorization']
if not lic_url:
print('Too many requests...')
return lic_url, header_auth
global folderdownloader
if args.output:
if not os.path.exists(args.output):
os.makedirs(args.output)
os.chdir(args.output)
if ":" in str(args.output):
folderdownloader = str(args.output).replace('/','\\').replace('.\\','\\')
else:
folderdownloader = dirPath + '\\' + str(args.output).replace('/','\\').replace('.\\','\\')
else:
folderdownloader = dirPath.replace('/','\\').replace('.\\','\\')
def get_subtitles(url):
master_base_url = re.split('(/)(?i)', url)
del master_base_url[-1]
master_base_url = ''.join(master_base_url)
urlm3u8_request = requests.get(url).text
m3u8_json = m3u8parser(urlm3u8_request)
subs_list = []
for media in m3u8_json['media']:
if media['type'] == 'SUBTITLES':
if 'https://' not in media['uri']:
full_url = master_base_url + media['uri']
Full_URL_Type = False
else:
full_url = media['uri']
Full_URL_Type = True
subs_dict = {
'Type':'subtitles',
'trackType':'NORMAL',
'Language':media['name'],
'LanguageID':replace_code_lang(media['language']),
'Profile':media['group_id'],
'URL':full_url}
subs_list.append(subs_dict)
subsList_new = []
if args.sublang:
for x in subs_list:
sub_lang = x['LanguageID']
if sub_lang in list(args.sublang):
subsList_new.append(x)
subs_list = subsList_new
return subs_list
def get_manifest(id_json):
api_manifest = 'https://link.theplatform.com/s/dJ5BDC/{}?format=SMIL&manifest=m3u&Tracking=true&mbr=true'.format(id_json['pid'])
r = requests.get(url=api_manifest, headers=pmnp_headers, proxies=proxy_cfg.get_proxy('meta'))
smil = json.loads(json.dumps(xmltodict.parse(r.text)))
videoSrc = []
try:
for x in smil['smil']['body']['seq']['switch']:
videoSrc = x['video']['@src']
except Exception:
videoSrc = smil['smil']['body']['seq']['switch']['video']['@src']
lic_url, header_auth = get_license(id_json)
return {'mpd_url': videoSrc, 'wvLicense': lic_url, 'wvHeader': header_auth}
def start_process(content_info):
drm_info = get_manifest(content_info)
base_url, length, video_list, audio_list, subs_list, pssh, xml = mpd_parsing(drm_info['mpd_url'])
video_bandwidth = dict(video_list[(-1)])['Bandwidth']
video_height = str(dict(video_list[(-1)])['Height'])
video_width = str(dict(video_list[(-1)])['Width'])
video_codec = str(dict(video_list[(-1)])['Codec'])
video_format_id = str(dict(video_list[(-1)])['ID'])
video_track_id = str(dict(video_list[(-1)])['TID'])
if not args.license:
if not args.novideo:
print('\nVIDEO - Bitrate: ' + convert_size(int(video_bandwidth)) + ' - Profile: ' + video_codec.split('=')[0] + ' - Size: ' + get_size(length * float(video_bandwidth) * 0.125) + ' - Dimensions: ' + video_width + 'x' + video_height)
print()
if not args.noaudio:
if audio_list != []:
for x in audio_list:
audio_bandwidth = x['Bandwidth']
audio_representation_id = str(x['Codec'])
audio_lang = x['Language']
print('AUDIO - Bitrate: ' + convert_size(int(audio_bandwidth)) + ' - Profile: ' + audio_representation_id.split('=')[0] + ' - Size: ' + get_size(length * float(audio_bandwidth) * 0.125) + ' - Language: ' + audio_lang)
print()
if not args.nosubs:
if subs_list != []:
for z in subs_list:
sub_lang = z['LanguageID']
print('SUBTITLE - Profile: NORMAL - Language: ' + sub_lang)
print()
print('Name: ' + content_info['seriesName'])
if content_info['pmnpType'] == 'show':
CurrentName = content_info['seriesName']
CurrentHeigh = str(video_height)
VideoOutputName = folderdownloader + '\\' + str(content_info['folderName']) + '\\' + str(CurrentName) + ' [' + str(CurrentHeigh) + 'p].mkv'
else:
CurrentName = content_info['seriesName']
CurrentHeigh = str(video_height)
VideoOutputName = folderdownloader + '\\' + str(CurrentName) + '\\' + ' [' + str(CurrentHeigh) + 'p].mkv'
if args.license:
keys_all = get_keys(drm_info, pssh)
with open(keys_file, 'a', encoding='utf8') as (file):
file.write(CurrentName + '\n')
print('\n' + CurrentName)
for key in keys_all:
with open(keys_file, 'a', encoding='utf8') as (file):
file.write(key + '\n')
print(key)
else:
if not args.novideo or (not args.noaudio):
print("\nGetting KEYS...")
try:
keys_all = get_keys(drm_info, pssh)
except KeyError:
print('License request failed, using keys from txt')
keys_all = keys_file_txt
print("Done!")
if not os.path.isfile(VideoOutputName):
aria2c_input = ''
if not args.novideo:
inputVideo = CurrentName + ' [' + str(CurrentHeigh) + 'p].mp4'
if os.path.isfile(inputVideo):
print('\n' + inputVideo + '\nFile has already been successfully downloaded previously.\n')
else:
try:
wvdl_cfg = WvDownloaderConfig(xml, base_url, inputVideo, video_track_id, video_format_id)
wvdownloader = WvDownloader(wvdl_cfg)
wvdownloader.run()
except KeyError:
url = urllib.parse.urljoin(base_url, video_list[(-1)]['URL'])
aria2c_input += f'{url}\n'
aria2c_input += f'\tdir={folderdownloader}\n'
aria2c_input += f'\tout={inputVideo}\n'
if not args.noaudio:
for x in audio_list:
langAbbrev = x['Language']
format_id = x['ID']
inputAudio = CurrentName + ' ' + '(' + langAbbrev + ').mp4'
inputAudio_demuxed = CurrentName + ' ' + '(' + langAbbrev + ')' + '.m4a'
if os.path.isfile(inputAudio) or os.path.isfile(inputAudio_demuxed):
print('\n' + inputAudio + '\nFile has already been successfully downloaded previously.\n')
else:
try:
wvdl_cfg = WvDownloaderConfig(xml, base_url, inputAudio, x['TID'], x['ID'])
wvdownloader = WvDownloader(wvdl_cfg)
wvdownloader.run()
except KeyError:
url = urllib.parse.urljoin(base_url, x['URL'])
aria2c_input += f'{url}\n'
aria2c_input += f'\tdir={folderdownloader}\n'
aria2c_input += f'\tout={inputAudio}\n'
aria2c_infile = os.path.join(folderdownloader, 'aria2c_infile.txt')
with open(aria2c_infile, 'w') as fd:
fd.write(aria2c_input)
aria2c_opts = [
pmnp_cfg.ARIA2C,
'--allow-overwrite=true',
'--download-result=hide',
'--console-log-level=warn',
'-x16', '-s16', '-j16',
'-i', aria2c_infile]
subprocess.run(aria2c_opts, check=True)
if not args.nosubs:
if subs_list != []:
for z in subs_list:
langAbbrev = z['LanguageID']
inputSubtitle = CurrentName + ' ' + '(' + langAbbrev + ')'
if os.path.isfile(inputSubtitle + '.vtt') or os.path.isfile(inputSubtitle + '.srt'):
print('\n' + inputSubtitle + '\nFile has already been successfully downloaded previously.\n')
else:
download_subs(inputSubtitle, z['URL'])
CorrectDecryptVideo = False
if not args.novideo:
inputVideo = CurrentName + ' [' + str(CurrentHeigh) + 'p].mp4'
if os.path.isfile(inputVideo):
CorrectDecryptVideo = DecryptVideo(inputVideo=inputVideo, keys_video=keys_all)
else:
CorrectDecryptVideo = True
CorrectDecryptAudio = False
if not args.noaudio:
for x in audio_list:
langAbbrev = x['Language']
inputAudio = CurrentName + ' ' + '(' + langAbbrev + ')' + '.mp4'
if os.path.isfile(inputAudio):
CorrectDecryptAudio = DecryptAudio(inputAudio=inputAudio, keys_audio=keys_all)
else:
CorrectDecryptAudio = True
if not args.nomux:
if not args.novideo:
if not args.noaudio:
if CorrectDecryptVideo == True:
if CorrectDecryptAudio == True:
print('\nMuxing...')
pmnpType = content_info['pmnpType']
folderName = content_info['folderName']
if pmnpType=="show":
MKV_Muxer=Muxer(CurrentName=CurrentName,
SeasonFolder=folderName,
CurrentHeigh=CurrentHeigh,
Type=pmnpType,
mkvmergeexe=pmnp_cfg.MKVMERGE)
else:
MKV_Muxer=Muxer(CurrentName=CurrentName,
SeasonFolder=None,
CurrentHeigh=CurrentHeigh,
Type=pmnpType,
mkvmergeexe=pmnp_cfg.MKVMERGE)
MKV_Muxer.mkvmerge_muxer(lang="English")
if args.tag:
inputName = CurrentName + ' [' + CurrentHeigh + 'p].mkv'
release_group(base_filename=inputName,
default_filename=CurrentName,
folder_name=folderName,
type=pmnpType,
video_height=CurrentHeigh)
if not args.keep:
for f in os.listdir():
if re.fullmatch(re.escape(CurrentName) + r'.*\.(mp4|m4a|h264|h265|eac3|ac3|srt|txt|avs|lwi|mpd)', f):
os.remove(f)
print("Done!")
else:
print("\nFile '" + str(VideoOutputName) + "' already exists.")
def release_group(base_filename, default_filename, folder_name, type, video_height):
if type=='show':
video_mkv = os.path.join(folder_name, base_filename)
else:
video_mkv = base_filename
mediainfo = mediainfo_(video_mkv)
for v in mediainfo['media']['track']: # mediainfo do video
if v['@type'] == 'Video':
video_format = v['Format']
video_codec = ''
if video_format == "AVC":
video_codec = 'H.264'
elif video_format == "HEVC":
video_codec = 'H.265'
for m in mediainfo['media']['track']: # mediainfo do audio
if m['@type'] == 'Audio':
codec_name = m['Format']
channels_number = m['Channels']
audio_codec = ''
audio_channels = ''
if codec_name == "AAC":
audio_codec = 'AAC'
elif codec_name == "AC-3":
audio_codec = "DD"
elif codec_name == "E-AC-3":
audio_codec = "DDP"
elif codec_name == "E-AC-3 JOC":
audio_codec = "ATMOS"
if channels_number == "2":
audio_channels = "2.0"
elif channels_number == "6":
audio_channels = "5.1"
audio_ = audio_codec + audio_channels
# renomear arquivo
default_filename = default_filename.replace('&', '.and.')
default_filename = re.sub(r'[]!"#$%\'()*+,:;<=>?@\\^_`{|}~[-]', '', default_filename)
default_filename = default_filename.replace(' ', '.')
default_filename = re.sub(r'\.{2,}', '.', default_filename)
default_filename = unidecode(default_filename)
output_name = '{}.{}p.PMNP.WEB-DL.{}.{}-{}'.format(default_filename, video_height, audio_, video_codec, args.tag)
if type=='show':
outputName = os.path.join(folder_name, output_name + '.mkv')
else:
outputName = output_name + '.mkv'
os.rename(video_mkv, outputName)
print("{} -> {}".format(base_filename, output_name))
from pywidevine.decrypt.wvdecryptcustom import WvDecrypt
from pywidevine.cdm import cdm, deviceconfig
def get_keys(licInfo, pssh):
device = deviceconfig.device_asus_x00dd
wvdecrypt = WvDecrypt(init_data_b64=bytes(pssh.encode()), cert_data_b64=None, device=device)
license_req = SESSION.post(url=licInfo['wvLicense'], headers={'authorization':licInfo['wvHeader']}, data=wvdecrypt.get_challenge(), proxies=proxy_cfg.get_proxy('meta')).content
license_b64 = base64.b64encode(license_req)
wvdecrypt.update_license(license_b64)
status, keys = wvdecrypt.start_process()
return keys
def DecryptAudio(inputAudio, keys_audio):
key_audio_id_original = getKeyId(inputAudio)
outputAudioTemp = inputAudio.replace(".mp4", "_dec.mp4")
if key_audio_id_original != "nothing":
for key in keys_audio:
key_id=key[0:32]
if key_id == key_audio_id_original:
print("\nDecrypting audio...")
print ("Using KEY: " + key)
wvdecrypt_process = subprocess.Popen([pmnp_cfg.MP4DECRYPT, "--show-progress", "--key", key, inputAudio, outputAudioTemp])
stdoutdata, stderrdata = wvdecrypt_process.communicate()
wvdecrypt_process.wait()
time.sleep (50.0/1000.0)
os.remove(inputAudio)
print("\nDemuxing audio...")
mediainfo = MediaInfo.parse(outputAudioTemp)
audio_info = next(x for x in mediainfo.tracks if x.track_type == "Audio")
codec_name = audio_info.format
ext = ''
if codec_name == "AAC":
ext = '.m4a'
elif codec_name == "E-AC-3":
ext = ".eac3"
elif codec_name == "AC-3":
ext = ".ac3"
outputAudio = outputAudioTemp.replace("_dec.mp4", ext)
print("{} -> {}".format(outputAudioTemp, outputAudio))
ff = ffmpy.FFmpeg(executable=pmnp_cfg.FFMPEG, inputs={outputAudioTemp: None}, outputs={outputAudio: '-c copy'}, global_options="-y -hide_banner -loglevel warning")
ff.run()
time.sleep (50.0/1000.0)
os.remove(outputAudioTemp)
print("Done!")
return True
elif key_audio_id_original == "nothing":
return True
def DecryptVideo(inputVideo, keys_video):
key_video_id_original = getKeyId(inputVideo)
inputVideo = inputVideo
outputVideoTemp = inputVideo.replace('.mp4', '_dec.mp4')
outputVideo = inputVideo
if key_video_id_original != 'nothing':
for key in keys_video:
key_id = key[0:32]
if key_id == key_video_id_original:
print('\nDecrypting video...')
print('Using KEY: ' + key)
wvdecrypt_process = subprocess.Popen([pmnp_cfg.MP4DECRYPT, '--show-progress', '--key', key, inputVideo, outputVideoTemp])
stdoutdata, stderrdata = wvdecrypt_process.communicate()
wvdecrypt_process.wait()
print('\nRemuxing video...')
ff = ffmpy.FFmpeg(executable=pmnp_cfg.FFMPEG, inputs={outputVideoTemp: None}, outputs={outputVideo: '-c copy'}, global_options='-y -hide_banner -loglevel warning')
ff.run()
time.sleep(0.05)
os.remove(outputVideoTemp)
print('Done!')
return True
elif key_video_id_original == 'nothing':
return True
def DemuxAudio(inputAudio):
if os.path.isfile(inputAudio):
print('\nDemuxing audio...')
mediainfo = mediainfo_(inputAudio)
for m in mediainfo['media']['track']:
if m['@type'] == 'Audio':
codec_name = m['Format']
try:
codec_tag_string = m['Format_Commercial_IfAny']
except Exception:
codec_tag_string = ''
ext = ''
if codec_name == 'AAC':
ext = '.m4a'
else:
if codec_name == 'E-AC-3':
ext = '.eac3'
else:
if codec_name == 'AC-3':
ext = '.ac3'
outputAudio = inputAudio.replace('.mp4', ext)
print('{} -> {}'.format(inputAudio, outputAudio))
ff = ffmpy.FFmpeg(executable=pmnp_cfg.FFMPEG,
inputs={inputAudio: None},
outputs={outputAudio: '-c copy'},
global_options='-y -hide_banner -loglevel warning')
ff.run()
time.sleep(0.05)
os.remove(inputAudio)
print('Done!')
get_content_info()

View File

380
pywidevine/cdm/cdm.py Normal file
View File

@ -0,0 +1,380 @@
import base64
import os
import time
import binascii
from google.protobuf.message import DecodeError
from google.protobuf import text_format
from pywidevine.cdm.formats import wv_proto2_pb2 as wv_proto2
from pywidevine.cdm.session import Session
from pywidevine.cdm.key import Key
from Crypto.Random import get_random_bytes
from Crypto.Random import random
from Crypto import Random
from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.Hash import CMAC, SHA256, HMAC, SHA1
from Crypto.PublicKey import RSA
from Crypto.Signature import pss
from Crypto.Util import Padding
from pywidevine.cdm import cdmapi
import logging
class Cdm:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.sessions = {}
def open_session(self, init_data_b64, device, raw_init_data=None, offline=False):
self.logger.debug("open_session(init_data_b64={}, device={}".format(init_data_b64, device))
self.logger.info("opening new cdm session")
if device.session_id_type == 'android':
# format: 16 random hexdigits, 2 digit counter, 14 0s
rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16))
counter = '01' # this resets regularly so its fine to use 01
rest = '00000000000000'
session_id = rand_ascii + counter + rest
session_id = session_id.encode('ascii')
elif device.session_id_type == 'chrome':
rand_bytes = get_random_bytes(16)
session_id = rand_bytes
else:
# other formats NYI
self.logger.error("device type is unusable")
return 1
if raw_init_data and isinstance(raw_init_data, (bytes, bytearray)):
# used for NF key exchange, where they don't provide a valid PSSH
init_data = raw_init_data
self.raw_pssh = True
else:
init_data = self._parse_init_data(init_data_b64)
self.raw_pssh = False
if init_data:
new_session = Session(session_id, init_data, device, offline)
else:
self.logger.error("unable to parse init data")
return 1
self.sessions[session_id] = new_session
self.logger.info("session opened and init data parsed successfully")
return session_id
def _parse_init_data(self, init_data_b64):
parsed_init_data = wv_proto2.WidevineCencHeader()
try:
self.logger.debug("trying to parse init_data directly")
parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
self.logger.debug("unable to parse as-is, trying with removed pssh box header")
try:
id_bytes = parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
self.logger.error("unable to parse, unsupported init data format")
return None
self.logger.debug("init_data:")
for line in text_format.MessageToString(parsed_init_data).splitlines():
self.logger.debug(line)
return parsed_init_data
def close_session(self, session_id):
self.logger.debug("close_session(session_id={})".format(session_id))
self.logger.info("closing cdm session")
if session_id in self.sessions:
self.sessions.pop(session_id)
self.logger.info("cdm session closed")
return 0
else:
self.logger.info("session {} not found".format(session_id))
return 1
def set_service_certificate(self, session_id, cert_b64):
self.logger.debug("set_service_certificate(session_id={}, cert={})".format(session_id, cert_b64))
self.logger.info("setting service certificate")
if session_id not in self.sessions:
self.logger.error("session id doesn't exist")
return 1
session = self.sessions[session_id]
message = wv_proto2.SignedMessage()
try:
message.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
self.logger.error("failed to parse cert as SignedMessage")
service_certificate = wv_proto2.SignedDeviceCertificate()
if message.Type:
self.logger.debug("service cert provided as signedmessage")
try:
service_certificate.ParseFromString(message.Msg)
except DecodeError:
self.logger.error("failed to parse service certificate")
return 1
else:
self.logger.debug("service cert provided as signeddevicecertificate")
try:
service_certificate.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
self.logger.error("failed to parse service certificate")
return 1
self.logger.debug("service certificate:")
for line in text_format.MessageToString(service_certificate).splitlines():
self.logger.debug(line)
session.service_certificate = service_certificate
session.privacy_mode = True
return 0
def sign_license_request(self, data):
em = binascii.b2a_hex((pss._EMSA_PSS_ENCODE(data, 2047, Random.get_random_bytes, lambda x, y: pss.MGF1(x, y, data), 20)))
sig = cdmapi.encrypt(em.decode('utf-8'))
return (binascii.a2b_hex(sig))
def get_license_request(self, session_id):
self.logger.debug("get_license_request(session_id={})".format(session_id))
self.logger.info("getting license request")
if session_id not in self.sessions:
self.logger.error("session ID does not exist")
return 1
session = self.sessions[session_id]
# raw pssh will be treated as bytes and not parsed
if self.raw_pssh:
license_request = wv_proto2.SignedLicenseRequestRaw()
else:
license_request = wv_proto2.SignedLicenseRequest()
client_id = wv_proto2.ClientIdentification()
if not os.path.exists(session.device_config.device_client_id_blob_filename):
self.logger.error("no client ID blob available for this device")
return 1
with open(session.device_config.device_client_id_blob_filename, "rb") as f:
try:
cid_bytes = client_id.ParseFromString(f.read())
except DecodeError:
self.logger.error("client id failed to parse as protobuf")
return 1
self.logger.debug("building license request")
if not self.raw_pssh:
license_request.Type = wv_proto2.SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data)
else:
license_request.Type = wv_proto2.SignedLicenseRequestRaw.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes
if session.offline:
license_type = wv_proto2.LicenseType.Value('OFFLINE')
else:
license_type = wv_proto2.LicenseType.Value('DEFAULT')
license_request.Msg.ContentId.CencId.LicenseType = license_type
license_request.Msg.ContentId.CencId.RequestId = session_id
license_request.Msg.Type = wv_proto2.LicenseRequest.RequestType.Value('NEW')
license_request.Msg.RequestTime = int(time.time())
license_request.Msg.ProtocolVersion = wv_proto2.ProtocolVersion.Value('CURRENT')
if session.device_config.send_key_control_nonce:
license_request.Msg.KeyControlNonce = random.randrange(1, 2**31)
if session.privacy_mode:
if session.device_config.vmp:
self.logger.debug("vmp required, adding to client_id")
self.logger.debug("reading vmp hashes")
vmp_hashes = wv_proto2.FileHashes()
with open(session.device_config.device_vmp_blob_filename, "rb") as f:
try:
vmp_bytes = vmp_hashes.ParseFromString(f.read())
except DecodeError:
self.logger.error("vmp hashes failed to parse as protobuf")
return 1
client_id._FileHashes.CopyFrom(vmp_hashes)
self.logger.debug("privacy mode & service certificate loaded, encrypting client id")
self.logger.debug("unencrypted client id:")
for line in text_format.MessageToString(client_id).splitlines():
self.logger.debug(line)
cid_aes_key = get_random_bytes(16)
cid_iv = get_random_bytes(16)
cid_cipher = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv)
encrypted_client_id = cid_cipher.encrypt(Padding.pad(client_id.SerializeToString(), 16))
service_public_key = RSA.importKey(session.service_certificate._DeviceCertificate.PublicKey)
service_cipher = PKCS1_OAEP.new(service_public_key)
encrypted_cid_key = service_cipher.encrypt(cid_aes_key)
encrypted_client_id_proto = wv_proto2.EncryptedClientIdentification()
encrypted_client_id_proto.ServiceId = session.service_certificate._DeviceCertificate.ServiceId
encrypted_client_id_proto.ServiceCertificateSerialNumber = session.service_certificate._DeviceCertificate.SerialNumber
encrypted_client_id_proto.EncryptedClientId = encrypted_client_id
encrypted_client_id_proto.EncryptedClientIdIv = cid_iv
encrypted_client_id_proto.EncryptedPrivacyKey = encrypted_cid_key
license_request.Msg.EncryptedClientId.CopyFrom(encrypted_client_id_proto)
else:
license_request.Msg.ClientId.CopyFrom(client_id)
if session.device_config.private_key_available:
key = RSA.importKey(open(session.device_config.device_private_key_filename).read())
print(key)
session.device_key = key
else:
self.logger.info("need device private key, other methods unimplemented")
# return 1
self.logger.debug("signing license request")
hash = SHA1.new(license_request.Msg.SerializeToString())
if(session.device_key is not None):
signature = pss.new(key).sign(hash)
else:
signature = self.sign_license_request(hash)
license_request.Signature = signature
session.license_request = license_request
self.logger.debug("license request:")
for line in text_format.MessageToString(session.license_request).splitlines():
self.logger.debug(line)
self.logger.info("license request created")
self.logger.debug("license request b64: {}".format(base64.b64encode(license_request.SerializeToString())))
return license_request.SerializeToString()
def provide_license(self, session_id, license_b64):
self.logger.debug("provide_license(session_id={}, license_b64={})".format(session_id, license_b64))
self.logger.info("decrypting provided license")
if session_id not in self.sessions:
self.logger.error("session does not exist")
return 1
session = self.sessions[session_id]
if not session.license_request:
self.logger.error("generate a license request first!")
return 1
license = wv_proto2.SignedLicense()
try:
license.ParseFromString(base64.b64decode(license_b64))
except DecodeError:
self.logger.error("unable to parse license - check protobufs")
return 1
session.license = license
self.logger.debug("license:")
for line in text_format.MessageToString(license).splitlines():
self.logger.debug(line)
self.logger.debug("deriving keys from session key")
oaep_cipher = PKCS1_OAEP.new(session.device_key)
if(session.device_key is not None):
session.session_key = oaep_cipher.decrypt(license.SessionKey)
else:
session_key = cdmapi.decrypt(binascii.b2a_hex(license.SessionKey).decode('utf-8'))
session.session_key = binascii.a2b_hex(session_key)
lic_req_msg = session.license_request.Msg.SerializeToString()
enc_key_base = b"ENCRYPTION\000" + lic_req_msg + b"\0\0\0\x80"
auth_key_base = b"AUTHENTICATION\0" + lic_req_msg + b"\0\0\2\0"
enc_key = b"\x01" + enc_key_base
auth_key_1 = b"\x01" + auth_key_base
auth_key_2 = b"\x02" + auth_key_base
auth_key_3 = b"\x03" + auth_key_base
auth_key_4 = b"\x04" + auth_key_base
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(enc_key)
enc_cmac_key = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_1)
auth_cmac_key_1 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_2)
auth_cmac_key_2 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_3)
auth_cmac_key_3 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_4)
auth_cmac_key_4 = cmac_obj.digest()
auth_cmac_combined_1 = auth_cmac_key_1 + auth_cmac_key_2
auth_cmac_combined_2 = auth_cmac_key_3 + auth_cmac_key_4
session.derived_keys['enc'] = enc_cmac_key
session.derived_keys['auth_1'] = auth_cmac_combined_1
session.derived_keys['auth_2'] = auth_cmac_combined_2
self.logger.debug('verifying license signature')
lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256)
lic_hmac.update(license.Msg.SerializeToString())
self.logger.debug("calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature)))
if lic_hmac.digest() != license.Signature:
self.logger.info("license signature doesn't match - writing bin so they can be debugged")
with open("original_lic.bin", "wb") as f:
f.write(base64.b64decode(license_b64))
with open("parsed_lic.bin", "wb") as f:
f.write(license.SerializeToString())
self.logger.info("continuing anyway")
self.logger.debug("key count: {}".format(len(license.Msg.Key)))
for key in license.Msg.Key:
if key.Id:
key_id = key.Id
else:
key_id = wv_proto2.License.KeyContainer.KeyType.Name(key.Type).encode('utf-8')
encrypted_key = key.Key
iv = key.Iv
type = wv_proto2.License.KeyContainer.KeyType.Name(key.Type)
cipher = AES.new(session.derived_keys['enc'], AES.MODE_CBC, iv=iv)
decrypted_key = cipher.decrypt(encrypted_key)
if type == "OPERATOR_SESSION":
permissions = []
perms = key._OperatorSessionKeyPermissions
for (descriptor, value) in perms.ListFields():
if value == 1:
permissions.append(descriptor.name)
else:
permissions = []
session.keys.append(Key(key_id, type, Padding.unpad(decrypted_key, 16), permissions))
self.logger.info("decrypted all keys")
return 0
def get_keys(self, session_id):
if session_id in self.sessions:
return self.sessions[session_id].keys
else:
self.logger.error("session not found")
return 1

324
pywidevine/cdm/cdm_debug.py Normal file
View File

@ -0,0 +1,324 @@
import base64
import os
import time
import binascii
from google.protobuf.message import DecodeError
from google.protobuf import text_format
from pywidevine.cdm.formats import wv_proto2_pb2 as wv_proto2
from pywidevine.cdm.session import Session
from pywidevine.cdm.key import Key
from Cryptodome.Random import get_random_bytes
from Cryptodome.Random import random
from Cryptodome.Cipher import PKCS1_OAEP, AES
from Cryptodome.Hash import CMAC, SHA256, HMAC, SHA1
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import pss
from Cryptodome.Util import Padding
import logging
class Cdm:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.sessions = {}
def open_session(self, init_data_b64, device):
print("open_session(init_data_b64={}, device={}".format(init_data_b64, device))
print("opening new cdm session")
if device.session_id_type == 'android':
# format: 16 random hexdigits, 2 digit counter, 14 0s
rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16))
counter = '01' # this resets regularly so its fine to use 01
rest = '00000000000000'
session_id = rand_ascii + counter + rest
session_id = session_id.encode('ascii')
elif device.session_id_type == 'chrome':
rand_bytes = get_random_bytes(16)
session_id = rand_bytes
else:
# other formats NYI
print("device type is unusable")
return 1
init_data = self._parse_init_data(init_data_b64)
if init_data:
new_session = Session(session_id, init_data, device)
else:
print("unable to parse init data")
return 1
self.sessions[session_id] = new_session
print("session opened and init data parsed successfully")
return session_id
def _parse_init_data(self, init_data_b64):
parsed_init_data = wv_proto2.WidevineCencHeader()
try:
print("trying to parse init_data directly")
parsed_init_data.ParseFromString(base64.b64decode(init_data_b64))
except DecodeError:
print("unable to parse as-is, trying with removed pssh box header")
try:
id_bytes = parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
print("unable to parse, unsupported init data format")
return None
print("init_data:")
for line in text_format.MessageToString(parsed_init_data).splitlines():
print(line)
return parsed_init_data
def close_session(self, session_id):
print("close_session(session_id={})".format(session_id))
print("closing cdm session")
if session_id in self.sessions:
self.sessions.pop(session_id)
print("cdm session closed")
return 0
else:
print("session {} not found".format(session_id))
return 1
def set_service_certificate(self, session_id, cert_b64):
print("set_service_certificate(session_id={}, cert={})".format(session_id, cert_b64))
print("setting service certificate")
if session_id not in self.sessions:
print("session id doesn't exist")
return 1
session = self.sessions[session_id]
message = wv_proto2.SignedMessage()
try:
message.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
print("failed to parse cert as SignedMessage")
service_certificate = wv_proto2.SignedDeviceCertificate()
if message.Type:
print("service cert provided as signedmessage")
try:
service_certificate.ParseFromString(message.Msg)
except DecodeError:
print("failed to parse service certificate")
return 1
else:
print("service cert provided as signeddevicecertificate")
try:
service_certificate.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
print("failed to parse service certificate")
return 1
print("service certificate:")
for line in text_format.MessageToString(service_certificate).splitlines():
print(line)
session.service_certificate = service_certificate
session.privacy_mode = True
return 0
def get_license_request(self, session_id):
print("get_license_request(session_id={})".format(session_id))
print("getting license request")
if session_id not in self.sessions:
print("session ID does not exist")
return 1
session = self.sessions[session_id]
license_request = wv_proto2.SignedLicenseRequest()
client_id = wv_proto2.ClientIdentification()
if not os.path.exists(session.device_config.device_client_id_blob_filename):
print("no client ID blob available for this device")
return 1
with open(session.device_config.device_client_id_blob_filename, "rb") as f:
try:
cid_bytes = client_id.ParseFromString(f.read())
except DecodeError:
print("client id failed to parse as protobuf")
return 1
print("building license request")
license_request.Type = wv_proto2.SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data)
license_request.Msg.ContentId.CencId.LicenseType = wv_proto2.LicenseType.Value('DEFAULT')
license_request.Msg.ContentId.CencId.RequestId = session_id
license_request.Msg.Type = wv_proto2.LicenseRequest.RequestType.Value('NEW')
license_request.Msg.RequestTime = int(time.time())
license_request.Msg.ProtocolVersion = wv_proto2.ProtocolVersion.Value('CURRENT')
if session.device_config.send_key_control_nonce:
license_request.Msg.KeyControlNonce = random.randrange(1, 2**31)
if session.privacy_mode:
print("privacy mode & service certificate loaded, encrypting client id")
print("unencrypted client id:")
for line in text_format.MessageToString(client_id).splitlines():
print(line)
cid_aes_key = get_random_bytes(16)
cid_iv = get_random_bytes(16)
cid_cipher = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv)
encrypted_client_id = cid_cipher.encrypt(Padding.pad(client_id.SerializeToString(), 16))
service_public_key = RSA.importKey(session.service_certificate._DeviceCertificate.PublicKey)
service_cipher = PKCS1_OAEP.new(service_public_key)
encrypted_cid_key = service_cipher.encrypt(cid_aes_key)
encrypted_client_id_proto = wv_proto2.EncryptedClientIdentification()
encrypted_client_id_proto.ServiceId = session.service_certificate._DeviceCertificate.ServiceId
encrypted_client_id_proto.ServiceCertificateSerialNumber = session.service_certificate._DeviceCertificate.SerialNumber
encrypted_client_id_proto.EncryptedClientId = encrypted_client_id
encrypted_client_id_proto.EncryptedClientIdIv = cid_iv
encrypted_client_id_proto.EncryptedPrivacyKey = encrypted_cid_key
license_request.Msg.EncryptedClientId.CopyFrom(encrypted_client_id_proto)
else:
license_request.Msg.ClientId.CopyFrom(client_id)
if session.device_config.private_key_available:
key = RSA.importKey(open(session.device_config.device_private_key_filename).read())
session.device_key = key
else:
print("need device private key, other methods unimplemented")
return 1
print("signing license request")
hash = SHA1.new(license_request.Msg.SerializeToString())
signature = pss.new(key).sign(hash)
license_request.Signature = signature
session.license_request = license_request
print("license request:")
for line in text_format.MessageToString(session.license_request).splitlines():
print(line)
print("license request created")
print("license request b64: {}".format(base64.b64encode(license_request.SerializeToString())))
return license_request.SerializeToString()
def provide_license(self, session_id, license_b64):
print("provide_license(session_id={}, license_b64={})".format(session_id, license_b64))
print("decrypting provided license")
if session_id not in self.sessions:
print("session does not exist")
return 1
session = self.sessions[session_id]
if not session.license_request:
print("generate a license request first!")
return 1
license = wv_proto2.SignedLicense()
try:
license.ParseFromString(base64.b64decode(license_b64))
except DecodeError:
print("unable to parse license - check protobufs")
return 1
session.license = license
print("license:")
for line in text_format.MessageToString(license).splitlines():
print(line)
print("deriving keys from session key")
oaep_cipher = PKCS1_OAEP.new(session.device_key)
session.session_key = oaep_cipher.decrypt(license.SessionKey)
lic_req_msg = session.license_request.Msg.SerializeToString()
enc_key_base = b"ENCRYPTION\000" + lic_req_msg + b"\0\0\0\x80"
auth_key_base = b"AUTHENTICATION\0" + lic_req_msg + b"\0\0\2\0"
enc_key = b"\x01" + enc_key_base
auth_key_1 = b"\x01" + auth_key_base
auth_key_2 = b"\x02" + auth_key_base
auth_key_3 = b"\x03" + auth_key_base
auth_key_4 = b"\x04" + auth_key_base
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(enc_key)
enc_cmac_key = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_1)
auth_cmac_key_1 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_2)
auth_cmac_key_2 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_3)
auth_cmac_key_3 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_4)
auth_cmac_key_4 = cmac_obj.digest()
auth_cmac_combined_1 = auth_cmac_key_1 + auth_cmac_key_2
auth_cmac_combined_2 = auth_cmac_key_3 + auth_cmac_key_4
session.derived_keys['enc'] = enc_cmac_key
session.derived_keys['auth_1'] = auth_cmac_combined_1
session.derived_keys['auth_2'] = auth_cmac_combined_2
print('verifying license signature')
lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256)
lic_hmac.update(license.Msg.SerializeToString())
print("calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature)))
if lic_hmac.digest() != license.Signature:
print("license signature doesn't match - writing bin so they can be debugged")
with open("original_lic.bin", "wb") as f:
f.write(base64.b64decode(license_b64))
with open("parsed_lic.bin", "wb") as f:
f.write(license.SerializeToString())
print("continuing anyway")
print("key count: {}".format(len(license.Msg.Key)))
for key in license.Msg.Key:
if key.Id:
key_id = key.Id
else:
key_id = wv_proto2.License.KeyContainer.KeyType.Name(key.Type).encode('utf-8')
encrypted_key = key.Key
iv = key.Iv
type = wv_proto2.License.KeyContainer.KeyType.Name(key.Type)
cipher = AES.new(session.derived_keys['enc'], AES.MODE_CBC, iv=iv)
decrypted_key = cipher.decrypt(encrypted_key)
session.keys.append(Key(key_id, type, Padding.unpad(decrypted_key, 16)))
print("decrypted all keys")
return 0
def get_keys(self, session_id):
if session_id in self.sessions:
return self.sessions[session_id].keys
else:
print("session not found")
return 1

BIN
pywidevine/cdm/cdmapi.pyd Normal file

Binary file not shown.

View File

@ -0,0 +1,54 @@
import os
device_asus_x00dd = {
'name': 'asus_x00dd',
'description': 'asus cdm',
'security_level': 1,
'session_id_type': 'android',
'private_key_available': True,
'vmp': False,
'send_key_control_nonce': True
}
devices_available = [device_asus_x00dd]
FILES_FOLDER = 'devices'
class DeviceConfig:
def __init__(self, device):
self.device_name = device['name']
self.description = device['description']
self.security_level = device['security_level']
self.session_id_type = device['session_id_type']
self.private_key_available = device['private_key_available']
self.vmp = device['vmp']
self.send_key_control_nonce = device['send_key_control_nonce']
if 'keybox_filename' in device:
self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['keybox_filename'])
else:
self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'keybox')
if 'device_cert_filename' in device:
self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_cert_filename'])
else:
self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_cert')
if 'device_private_key_filename' in device:
self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_private_key_filename'])
else:
self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_private_key')
if 'device_client_id_blob_filename' in device:
self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_client_id_blob_filename'])
else:
self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_client_id_blob')
if 'device_vmp_blob_filename' in device:
self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_vmp_blob_filename'])
else:
self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_vmp_blob')
def __repr__(self):
return "DeviceConfig(name={}, description={}, security_level={}, session_id_type={}, private_key_available={}, vmp={})".format(self.device_name, self.description, self.security_level, self.session_id_type, self.private_key_available, self.vmp)

View File

@ -0,0 +1,88 @@
import os
device_nexus6_lvl1 = {
'name': 'nexus6_lvl1',
'description': 'nexus 6 firmware 5.1.1 lvl1 security level',
'security_level': 1,
'session_id_type': 'android',
'private_key_available': True,
'vmp': False,
'send_key_control_nonce': True
}
device_chromecdm_903 = {
'name': 'chromecdm_903',
'description': 'chrome cdm windows 903',
'security_level': 3,
'session_id_type': 'chrome',
'private_key_available': True,
'vmp': False,
'send_key_control_nonce': False
}
device_chromecdm_984 = {
'name': 'chromecdm_984',
'description': 'chrome cdm windows 984',
'security_level': 3,
'session_id_type': 'chrome',
'private_key_available': True,
'vmp': True,
'send_key_control_nonce': False
}
device_chromecdm_1022 = {
'name': 'chromecdm_1022',
'description': 'chrome cdm windows 1022',
'security_level': 3,
'session_id_type': 'chrome',
'private_key_available': True,
'vmp': True,
'send_key_control_nonce': False
}
devices_available = [device_nexus6_lvl1, device_chromecdm_903, device_chromecdm_984, device_chromecdm_1022]
from __main__ import dirPath
#main_folder = os.path.dirname(__file__)
main_folder = os.path.join(dirPath, 'pywidevine', 'cdm')
FILES_FOLDER = 'devices'
class DeviceConfig:
def __init__(self, device):
self.device_name = device['name']
self.description = device['description']
self.security_level = device['security_level']
self.session_id_type = device['session_id_type']
self.private_key_available = device['private_key_available']
self.vmp = device['vmp']
self.send_key_control_nonce = device['send_key_control_nonce']
if 'keybox_filename' in device:
self.keybox_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], device['keybox_filename'])
else:
self.keybox_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], 'keybox')
if 'device_cert_filename' in device:
self.device_cert_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], device['device_cert_filename'])
else:
self.device_cert_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], 'device_cert')
if 'device_private_key_filename' in device:
self.device_private_key_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], device['device_private_key_filename'])
else:
self.device_private_key_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], 'device_private_key')
if 'device_client_id_blob_filename' in device:
self.device_client_id_blob_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], device['device_client_id_blob_filename'])
else:
self.device_client_id_blob_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], 'device_client_id_blob')
if 'device_vmp_blob_filename' in device:
self.device_vmp_blob_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], device['device_vmp_blob_filename'])
else:
self.device_vmp_blob_filename = os.path.join(main_folder, FILES_FOLDER, device['name'], 'device_vmp_blob')
def __repr__(self):
return "DeviceConfig(name={}, description={}, security_level={}, session_id_type={}, private_key_available={}, vmp={})".format(self.device_name, self.description, self.security_level, self.session_id_type, self.private_key_available, self.vmp)

View File

Binary file not shown.

View File

View File

@ -0,0 +1,466 @@
syntax = "proto2";
// from x86 (partial), most of it from the ARM version:
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
DEVICE_CERTIFICATE = 1;
REMOTE_ATTESTATION_CERTIFICATE = 2;
}
message NameValue {
required string Name = 1;
required string Value = 2;
}
message ClientCapabilities {
enum HdcpVersion {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
HDCP_V2_1 = 3;
HDCP_V2_2 = 4;
}
optional uint32 ClientToken = 1;
optional uint32 SessionToken = 2;
optional uint32 VideoResolutionConstraints = 3;
optional HdcpVersion MaxHdcpVersion = 4;
optional uint32 OemCryptoApiVersion = 5;
}
required TokenType Type = 1;
//optional bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one:
optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob
repeated NameValue ClientInfo = 3;
optional bytes ProviderClientToken = 4;
optional uint32 LicenseCounter = 5;
optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
optional FileHashes _FileHashes = 7; // vmp blob goes here
}
message DeviceCertificate {
enum CertificateType {
ROOT = 0;
INTERMEDIATE = 1;
USER_DEVICE = 2;
SERVICE = 3;
}
required CertificateType Type = 1; // the compiled code reused this as ProvisionedDeviceInfo.WvSecurityLevel, however that is incorrect (compiler aliased it as they're both identical as a structure)
optional bytes SerialNumber = 2;
optional uint32 CreationTimeSeconds = 3;
optional bytes PublicKey = 4;
optional uint32 SystemId = 5;
optional uint32 TestDeviceDeprecated = 6; // is it bool or int?
optional bytes ServiceId = 7; // service URL for service certificates
}
// missing some references,
message DeviceCertificateStatus {
enum CertificateStatus {
VALID = 0;
REVOKED = 1;
}
optional bytes SerialNumber = 1;
optional CertificateStatus Status = 2;
optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
}
message DeviceCertificateStatusList {
optional uint32 CreationTimeSeconds = 1;
repeated DeviceCertificateStatus CertificateStatus = 2;
}
message EncryptedClientIdentification {
required string ServiceId = 1;
optional bytes ServiceCertificateSerialNumber = 2;
required bytes EncryptedClientId = 3;
required bytes EncryptedClientIdIv = 4;
required bytes EncryptedPrivacyKey = 5;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
enum LicenseType {
ZERO = 0;
DEFAULT = 1; // 1 is STREAMING/temporary license; on recent versions may go up to 3 (latest x86); it might be persist/don't persist type, unconfirmed
OFFLINE = 2;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
// this is just a guess because these globals got lost, but really, do we need more?
enum ProtocolVersion {
CURRENT = 21; // don't have symbols for this
}
message LicenseIdentification {
optional bytes RequestId = 1;
optional bytes SessionId = 2;
optional bytes PurchaseId = 3;
optional LicenseType Type = 4;
optional uint32 Version = 5;
optional bytes ProviderSessionToken = 6;
}
message License {
message Policy {
optional bool CanPlay = 1; // changed from uint32 to bool
optional bool CanPersist = 2;
optional bool CanRenew = 3;
optional uint32 RentalDurationSeconds = 4;
optional uint32 PlaybackDurationSeconds = 5;
optional uint32 LicenseDurationSeconds = 6;
optional uint32 RenewalRecoveryDurationSeconds = 7;
optional string RenewalServerUrl = 8;
optional uint32 RenewalDelaySeconds = 9;
optional uint32 RenewalRetryIntervalSeconds = 10;
optional bool RenewWithUsage = 11; // was uint32
}
message KeyContainer {
enum KeyType {
SIGNING = 1;
CONTENT = 2;
KEY_CONTROL = 3;
OPERATOR_SESSION = 4;
}
enum SecurityLevel {
SW_SECURE_CRYPTO = 1;
SW_SECURE_DECODE = 2;
HW_SECURE_CRYPTO = 3;
HW_SECURE_DECODE = 4;
HW_SECURE_ALL = 5;
}
message OutputProtection {
enum CGMS {
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
CGMS_NONE = 0x2A; // PC default!
}
optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
optional CGMS CgmsFlags = 2;
}
message KeyControl {
required bytes KeyControlBlock = 1; // what is this?
required bytes Iv = 2;
}
message OperatorSessionKeyPermissions {
optional uint32 AllowEncrypt = 1;
optional uint32 AllowDecrypt = 2;
optional uint32 AllowSign = 3;
optional uint32 AllowSignatureVerify = 4;
}
message VideoResolutionConstraint {
optional uint32 MinResolutionPixels = 1;
optional uint32 MaxResolutionPixels = 2;
optional OutputProtection RequiredProtection = 3;
}
optional bytes Id = 1;
optional bytes Iv = 2;
optional bytes Key = 3;
optional KeyType Type = 4;
optional SecurityLevel Level = 5;
optional OutputProtection RequiredProtection = 6;
optional OutputProtection RequestedProtection = 7;
optional KeyControl _KeyControl = 8; // duped names, etc
optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
}
optional LicenseIdentification Id = 1;
optional Policy _Policy = 2; // duped names, etc
repeated KeyContainer Key = 3;
optional uint32 LicenseStartTime = 4;
optional uint32 RemoteAttestationVerified = 5; // bool?
optional bytes ProviderClientToken = 6;
// there might be more, check with newer versions (I see field 7-8 in a lic)
// this appeared in latest x86:
optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
}
message LicenseError {
enum Error {
INVALID_DEVICE_CERTIFICATE = 1;
REVOKED_DEVICE_CERTIFICATE = 2;
SERVICE_UNAVAILABLE = 3;
}
//LicenseRequest.RequestType ErrorCode; // clang mismatch
optional Error ErrorCode = 1;
}
message LicenseRequest {
message ContentIdentification {
message CENC {
//optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with:
optional WidevineCencHeader Pssh = 1;
optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!)
optional bytes RequestId = 3;
}
message WebM {
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
optional LicenseType LicenseType = 2;
optional bytes RequestId = 3;
}
message ExistingLicense {
optional LicenseIdentification LicenseId = 1;
optional uint32 SecondsSinceStarted = 2;
optional uint32 SecondsSinceLastPlayed = 3;
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
}
optional CENC CencId = 1;
optional WebM WebmId = 2;
optional ExistingLicense License = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification ClientId = 1;
optional ContentIdentification ContentId = 2;
optional RequestType Type = 3;
optional uint32 RequestTime = 4;
optional bytes KeyControlNonceDeprecated = 5;
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
optional uint32 KeyControlNonce = 7;
optional EncryptedClientIdentification EncryptedClientId = 8;
}
// raw pssh hack
message LicenseRequestRaw {
message ContentIdentification {
message CENC {
optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with:
//optional WidevineCencHeader Pssh = 1;
optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!)
optional bytes RequestId = 3;
}
message WebM {
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
optional LicenseType LicenseType = 2;
optional bytes RequestId = 3;
}
message ExistingLicense {
optional LicenseIdentification LicenseId = 1;
optional uint32 SecondsSinceStarted = 2;
optional uint32 SecondsSinceLastPlayed = 3;
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
}
optional CENC CencId = 1;
optional WebM WebmId = 2;
optional ExistingLicense License = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification ClientId = 1;
optional ContentIdentification ContentId = 2;
optional RequestType Type = 3;
optional uint32 RequestTime = 4;
optional bytes KeyControlNonceDeprecated = 5;
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
optional uint32 KeyControlNonce = 7;
optional EncryptedClientIdentification EncryptedClientId = 8;
}
message ProvisionedDeviceInfo {
enum WvSecurityLevel {
LEVEL_UNSPECIFIED = 0;
LEVEL_1 = 1;
LEVEL_2 = 2;
LEVEL_3 = 3;
}
optional uint32 SystemId = 1;
optional string Soc = 2;
optional string Manufacturer = 3;
optional string Model = 4;
optional string DeviceType = 5;
optional uint32 ModelYear = 6;
optional WvSecurityLevel SecurityLevel = 7;
optional uint32 TestDevice = 8; // bool?
}
// todo: fill
message ProvisioningOptions {
}
// todo: fill
message ProvisioningRequest {
}
// todo: fill
message ProvisioningResponse {
}
message RemoteAttestation {
optional EncryptedClientIdentification Certificate = 1;
optional string Salt = 2;
optional string Signature = 3;
}
// todo: fill
message SessionInit {
}
// todo: fill
message SessionState {
}
// todo: fill
message SignedCertificateStatusList {
}
message SignedDeviceCertificate {
//optional bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is:
optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
optional bytes Signature = 2;
optional SignedDeviceCertificate Signer = 3;
}
// todo: fill
message SignedProvisioningMessage {
}
// the root of all messages, from either server or client
message SignedMessage {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
// This message is copied from google's docs, not reversed:
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
optional Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
optional string provider = 3;
// A content identifier, specified by content provider.
optional bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
optional string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
optional string policy = 6;
// Crypto period index, for media using key rotation.
optional uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
optional bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
optional uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
optional uint32 crypto_period_seconds = 10;
}
// remove these when using it outside of protoc:
// from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically
message SignedLicenseRequest {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
// hack
message SignedLicenseRequestRaw {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional LicenseRequestRaw Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
message SignedLicense {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
message SignedServiceCertificate {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional SignedDeviceCertificate Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
//vmp support
message FileHashes {
message Signature {
optional string filename = 1;
optional bool test_signing = 2; //0 - release, 1 - testing
optional bytes SHA512Hash = 3;
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
optional bytes signature = 5;
}
optional bytes signer = 1;
repeated Signature signatures = 2;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,389 @@
// beware proto3 won't show missing fields it seems, need to change to "proto2" and add "optional" before every field, and remove all the dummy enum members I added:
syntax = "proto3";
// from x86 (partial), most of it from the ARM version:
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
DEVICE_CERTIFICATE = 1;
REMOTE_ATTESTATION_CERTIFICATE = 2;
}
message NameValue {
string Name = 1;
string Value = 2;
}
message ClientCapabilities {
enum HdcpVersion {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
HDCP_V2_1 = 3;
HDCP_V2_2 = 4;
}
uint32 ClientToken = 1;
uint32 SessionToken = 2;
uint32 VideoResolutionConstraints = 3;
HdcpVersion MaxHdcpVersion = 4;
uint32 OemCryptoApiVersion = 5;
}
TokenType Type = 1;
//bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one:
SignedDeviceCertificate Token = 2;
repeated NameValue ClientInfo = 3;
bytes ProviderClientToken = 4;
uint32 LicenseCounter = 5;
ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
}
message DeviceCertificate {
enum CertificateType {
ROOT = 0;
INTERMEDIATE = 1;
USER_DEVICE = 2;
SERVICE = 3;
}
//ProvisionedDeviceInfo.WvSecurityLevel Type = 1; // is this how one is supposed to call it? (it's an enum) there might be a bug here, with CertificateType getting confused with WvSecurityLevel, for now renaming it (verify against other binaries)
CertificateType Type = 1;
bytes SerialNumber = 2;
uint32 CreationTimeSeconds = 3;
bytes PublicKey = 4;
uint32 SystemId = 5;
uint32 TestDeviceDeprecated = 6; // is it bool or int?
bytes ServiceId = 7; // service URL for service certificates
}
// missing some references,
message DeviceCertificateStatus {
enum CertificateStatus {
VALID = 0;
REVOKED = 1;
}
bytes SerialNumber = 1;
CertificateStatus Status = 2;
ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
}
message DeviceCertificateStatusList {
uint32 CreationTimeSeconds = 1;
repeated DeviceCertificateStatus CertificateStatus = 2;
}
message EncryptedClientIdentification {
string ServiceId = 1;
bytes ServiceCertificateSerialNumber = 2;
bytes EncryptedClientId = 3;
bytes EncryptedClientIdIv = 4;
bytes EncryptedPrivacyKey = 5;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
enum LicenseType {
ZERO = 0;
DEFAULT = 1; // do not know what this is either, but should be 1; on recent versions may go up to 3 (latest x86)
}
// todo: fill (for this top-level type, it might be impossible/difficult)
// this is just a guess because these globals got lost, but really, do we need more?
enum ProtocolVersion {
DUMMY = 0;
CURRENT = 21; // don't have symbols for this
}
message LicenseIdentification {
bytes RequestId = 1;
bytes SessionId = 2;
bytes PurchaseId = 3;
LicenseType Type = 4;
uint32 Version = 5;
bytes ProviderSessionToken = 6;
}
message License {
message Policy {
uint32 CanPlay = 1;
uint32 CanPersist = 2;
uint32 CanRenew = 3;
uint32 RentalDurationSeconds = 4;
uint32 PlaybackDurationSeconds = 5;
uint32 LicenseDurationSeconds = 6;
uint32 RenewalRecoveryDurationSeconds = 7;
string RenewalServerUrl = 8;
uint32 RenewalDelaySeconds = 9;
uint32 RenewalRetryIntervalSeconds = 10;
uint32 RenewWithUsage = 11;
uint32 UnknownPolicy12 = 12;
}
message KeyContainer {
enum KeyType {
_NOKEYTYPE = 0; // dummy, added to satisfy proto3, not present in original
SIGNING = 1;
CONTENT = 2;
KEY_CONTROL = 3;
OPERATOR_SESSION = 4;
}
enum SecurityLevel {
_NOSECLEVEL = 0; // dummy, added to satisfy proto3, not present in original
SW_SECURE_CRYPTO = 1;
SW_SECURE_DECODE = 2;
HW_SECURE_CRYPTO = 3;
HW_SECURE_DECODE = 4;
HW_SECURE_ALL = 5;
}
message OutputProtection {
enum CGMS {
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
CGMS_NONE = 0x2A; // PC default!
}
ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
CGMS CgmsFlags = 2;
}
message KeyControl {
bytes KeyControlBlock = 1; // what is this?
bytes Iv = 2;
}
message OperatorSessionKeyPermissions {
uint32 AllowEncrypt = 1;
uint32 AllowDecrypt = 2;
uint32 AllowSign = 3;
uint32 AllowSignatureVerify = 4;
}
message VideoResolutionConstraint {
uint32 MinResolutionPixels = 1;
uint32 MaxResolutionPixels = 2;
OutputProtection RequiredProtection = 3;
}
bytes Id = 1;
bytes Iv = 2;
bytes Key = 3;
KeyType Type = 4;
SecurityLevel Level = 5;
OutputProtection RequiredProtection = 6;
OutputProtection RequestedProtection = 7;
KeyControl _KeyControl = 8; // duped names, etc
OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
}
LicenseIdentification Id = 1;
Policy _Policy = 2; // duped names, etc
repeated KeyContainer Key = 3;
uint32 LicenseStartTime = 4;
uint32 RemoteAttestationVerified = 5; // bool?
bytes ProviderClientToken = 6;
// there might be more, check with newer versions (I see field 7-8 in a lic)
// this appeared in latest x86:
uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
bytes UnknownHdcpDataField = 8;
}
message LicenseError {
enum Error {
DUMMY_NO_ERROR = 0; // dummy, added to satisfy proto3
INVALID_DEVICE_CERTIFICATE = 1;
REVOKED_DEVICE_CERTIFICATE = 2;
SERVICE_UNAVAILABLE = 3;
}
//LicenseRequest.RequestType ErrorCode; // clang mismatch
Error ErrorCode = 1;
}
message LicenseRequest {
message ContentIdentification {
message CENC {
// bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with:
WidevineCencHeader Pssh = 1;
LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1
bytes RequestId = 3;
}
message WebM {
bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
LicenseType LicenseType = 2;
bytes RequestId = 3;
}
message ExistingLicense {
LicenseIdentification LicenseId = 1;
uint32 SecondsSinceStarted = 2;
uint32 SecondsSinceLastPlayed = 3;
bytes SessionUsageTableEntry = 4;
}
CENC CencId = 1;
WebM WebmId = 2;
ExistingLicense License = 3;
}
enum RequestType {
DUMMY_REQ_TYPE = 0; // dummy, added to satisfy proto3
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
ClientIdentification ClientId = 1;
ContentIdentification ContentId = 2;
RequestType Type = 3;
uint32 RequestTime = 4;
bytes KeyControlNonceDeprecated = 5;
ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
uint32 KeyControlNonce = 7;
EncryptedClientIdentification EncryptedClientId = 8;
}
message ProvisionedDeviceInfo {
enum WvSecurityLevel {
LEVEL_UNSPECIFIED = 0;
LEVEL_1 = 1;
LEVEL_2 = 2;
LEVEL_3 = 3;
}
uint32 SystemId = 1;
string Soc = 2;
string Manufacturer = 3;
string Model = 4;
string DeviceType = 5;
uint32 ModelYear = 6;
WvSecurityLevel SecurityLevel = 7;
uint32 TestDevice = 8; // bool?
}
// todo: fill
message ProvisioningOptions {
}
// todo: fill
message ProvisioningRequest {
}
// todo: fill
message ProvisioningResponse {
}
message RemoteAttestation {
EncryptedClientIdentification Certificate = 1;
string Salt = 2;
string Signature = 3;
}
// todo: fill
message SessionInit {
}
// todo: fill
message SessionState {
}
// todo: fill
message SignedCertificateStatusList {
}
message SignedDeviceCertificate {
//bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is:
DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
bytes Signature = 2;
SignedDeviceCertificate Signer = 3;
}
// todo: fill
message SignedProvisioningMessage {
}
// the root of all messages, from either server or client
message SignedMessage {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
// This message is copied from google's docs, not reversed:
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
string provider = 3;
// A content identifier, specified by content provider.
bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
string policy = 6;
// Crypto period index, for media using key rotation.
uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
uint32 crypto_period_seconds = 10;
}
// from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically
message SignedLicenseRequest {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
message SignedLicense {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}

File diff suppressed because one or more lines are too long

15
pywidevine/cdm/key.py Normal file
View File

@ -0,0 +1,15 @@
import binascii
class Key:
def __init__(self, kid, type, key, permissions=[]):
self.kid = kid
self.type = type
self.key = key
self.permissions = permissions
def __repr__(self):
if self.type == "OPERATOR_SESSION":
return "key(kid={}, type={}, key={}, permissions={})".format(binascii.hexlify(self.kid), self.type, binascii.hexlify(self.key), self.permissions)
else:
return "key(kid={}, type={}, key={})".format(binascii.hexlify(self.kid), self.type, binascii.hexlify(self.key))

18
pywidevine/cdm/session.py Normal file
View File

@ -0,0 +1,18 @@
class Session:
def __init__(self, session_id, init_data, device_config, offline):
self.session_id = session_id
self.init_data = init_data
self.offline = offline
self.device_config = device_config
self.device_key = None
self.session_key = None
self.derived_keys = {
'enc': None,
'auth_1': None,
'auth_2': None
}
self.license_request = None
self.license = None
self.service_certificate = None
self.privacy_mode = False
self.keys = []

102
pywidevine/cdm/vmp.py Normal file
View File

@ -0,0 +1,102 @@
try:
from google.protobuf.internal.decoder import _DecodeVarint as _di # this was tested to work with protobuf 3, but it's an internal API (any varint decoder might work)
except ImportError:
# this is generic and does not depend on pb internals, however it will decode "larger" possible numbers than pb decoder which has them fixed
def LEB128_decode(buffer, pos, limit = 64):
result = 0
shift = 0
while True:
b = buffer[pos]
pos += 1
result |= ((b & 0x7F) << shift)
if not (b & 0x80):
return (result, pos)
shift += 7
if shift > limit:
raise Exception("integer too large, shift: {}".format(shift))
_di = LEB128_decode
class FromFileMixin:
@classmethod
def from_file(cls, filename):
"""Load given a filename"""
with open(filename,"rb") as f:
return cls(f.read())
# the signatures use a format internally similar to
# protobuf's encoding, but without wire types
class VariableReader(FromFileMixin):
"""Protobuf-like encoding reader"""
def __init__(self, buf):
self.buf = buf
self.pos = 0
self.size = len(buf)
def read_int(self):
"""Read a variable length integer"""
# _DecodeVarint will take care of out of range errors
(val, nextpos) = _di(self.buf, self.pos)
self.pos = nextpos
return val
def read_bytes_raw(self, size):
"""Read size bytes"""
b = self.buf[self.pos:self.pos+size]
self.pos += size
return b
def read_bytes(self):
"""Read a bytes object"""
size = self.read_int()
return self.read_bytes_raw(size)
def is_end(self):
return (self.size == self.pos)
class TaggedReader(VariableReader):
"""Tagged reader, needed for implementing a WideVine signature reader"""
def read_tag(self):
"""Read a tagged buffer"""
return (self.read_int(), self.read_bytes())
def read_all_tags(self, max_tag=3):
tags = {}
while (not self.is_end()):
(tag, bytes) = self.read_tag()
if (tag > max_tag):
raise IndexError("tag out of bound: got {}, max {}".format(tag, max_tag))
tags[tag] = bytes
return tags
class WideVineSignatureReader(FromFileMixin):
"""Parses a widevine .sig signature file."""
SIGNER_TAG = 1
SIGNATURE_TAG = 2
ISMAINEXE_TAG = 3
def __init__(self, buf):
reader = TaggedReader(buf)
self.version = reader.read_int()
if (self.version != 0):
raise Exception("Unsupported signature format version {}".format(self.version))
self.tags = reader.read_all_tags()
self.signer = self.tags[self.SIGNER_TAG]
self.signature = self.tags[self.SIGNATURE_TAG]
extra = self.tags[self.ISMAINEXE_TAG]
if (len(extra) != 1 or (extra[0] > 1)):
raise Exception("Unexpected 'ismainexe' field value (not '\\x00' or '\\x01'), please check: {0}".format(extra))
self.mainexe = bool(extra[0])
@classmethod
def get_tags(cls, filename):
"""Return a dictionary of each tag in the signature file"""
return cls.from_file(filename).tags

View File

@ -0,0 +1,44 @@
import re
from unidecode import unidecode
def get_release_tag(default_filename, vcodec, video_height, acodec, channels, bitrate, module, tag, isDual):
video_codec = ''
if 'avc' in vcodec:
video_codec = 'x264'
if 'hvc' in vcodec:
video_codec = 'x265'
elif 'dvh' in vcodec:
video_codec = 'HDR'
if isDual==True:
video_codec = video_codec + '.DUAL'
audio_codec = ''
if 'mp4a' in acodec:
audio_codec = 'AAC'
if acodec == 'ac-3':
audio_codec = 'DD'
if acodec == 'ec-3':
audio_codec = 'DDP'
elif acodec == 'ec-3' and bitrate > 700000:
audio_codec = 'Atmos'
audio_channels = ''
if channels == '2':
audio_channels = '2.0'
elif channels == '6':
audio_channels = '5.1'
audio_format = audio_codec + audio_channels
#if isDual==True:
# audio_format = audio_codec + '.DUAL'
default_filename = default_filename.replace('&', '.and.')
default_filename = re.sub(r'[]!"#$%\'()*+,:;<=>?@\\^_`{|}~[-]', '', default_filename)
default_filename = default_filename.replace(' ', '.')
default_filename = re.sub(r'\.{2,}', '.', default_filename)
default_filename = unidecode(default_filename)
output_name = '{}.{}p.{}.WEB-DL.{}.{}-{}'.format(default_filename, video_height, str(module), audio_format, video_codec, tag)
return output_name

View File

@ -0,0 +1,35 @@
from shutil import which
from os.path import dirname, realpath, join
from os import pathsep, environ
SCRIPT_PATH = dirname(realpath('paramountplus'))
BINARIES_FOLDER = join(SCRIPT_PATH, 'binaries')
MP4DECRYPT_BINARY = 'mp4decrypt'
MEDIAINFO_BINARY = 'mediainfo'
MP4DUMP_BINARY = 'mp4dump'
MKVMERGE_BINARY = 'mkvmerge'
FFMPEG_BINARY = 'ffmpeg'
FFMPEG_BINARY = 'ffmpeg'
ARIA2C_BINARY = 'aria2c'
SUBTITLE_EDIT_BINARY = 'subtitleedit'
# Add binaries folder to PATH as the first item
environ['PATH'] = pathsep.join([BINARIES_FOLDER, environ['PATH']])
MP4DECRYPT = which(MP4DECRYPT_BINARY)
MEDIAINFO = which(MEDIAINFO_BINARY)
MP4DUMP = which(MP4DUMP_BINARY)
MKVMERGE = which(MKVMERGE_BINARY)
FFMPEG = which(FFMPEG_BINARY)
ARIA2C = which(ARIA2C_BINARY)
SUBTITLE_EDIT = which(SUBTITLE_EDIT_BINARY)
class WvDownloaderConfig(object):
def __init__(self, xml, base_url, output_file, track_id, format_id):
self.xml = xml
self.base_url = base_url
self.output_file = output_file
self.track_id = track_id
self.format_id = format_id

View File

@ -0,0 +1,116 @@
import requests, pathlib
import math, subprocess
import os, sys, shutil
class WvDownloader(object):
def __init__(self, config):
self.xml = config.xml
self.output_file = config.output_file
self.config = config
def download_track(self, aria2c_infile, file_name):
aria2c_opts = [
'aria2c',
'--enable-color=false',
'--allow-overwrite=true',
'--summary-interval=0',
'--download-result=hide',
'--async-dns=false',
'--check-certificate=false',
'--auto-file-renaming=false',
'--file-allocation=none',
'--console-log-level=warn',
'-x16', '-s16', '-j16',
'-i', aria2c_infile]
subprocess.run(aria2c_opts)
source_files = pathlib.Path(temp_folder).rglob(r'./*.mp4')
with open(file_name, mode='wb') as (destination):
for file in source_files:
with open(file, mode='rb') as (source):
shutil.copyfileobj(source, destination)
if os.path.exists(temp_folder):
shutil.rmtree(temp_folder)
os.remove(aria2c_infile)
print('\nDone!')
def process_url_templace(self, template, representation_id, bandwidth, time, number):
if representation_id is not None: result = template.replace('$RepresentationID$', representation_id)
if number is not None:
nstart = result.find('$Number')
if nstart >= 0:
nend = result.find('$', nstart+1)
if nend >= 0:
var = result[nstart+1 : nend]
if 'Number%' in var:
value = var[6:] % (int(number))
else:
value = number
result = result.replace('$'+var+'$', value)
if bandwidth is not None: result = result.replace('$Bandwidth$', bandwidth)
if time is not None: result = result.replace('$Time$', time)
result = result.replace('$$', '$').replace('../', '')
return result
def generate_segments(self):
segment_template = self.get_segment_template()
return self.get_segments(segment_template)
def get_segments(self, segment_template):
urls = []
urls.append(self.config.base_url + segment_template['@initialization'].replace('$RepresentationID$', self.config.format_id))
current_number = 1
for seg in self.force_segmentimeline(segment_template):
if '@t' in seg:
current_time = seg['@t']
for i in range(int(seg.get('@r', 0)) + 1):
urls.append(self.config.base_url + self.process_url_templace(segment_template['@media'],
representation_id=self.config.format_id,
bandwidth=None, time=str(current_time), number=str(current_number)))
current_number += 1
current_time += seg['@d']
return urls
def force_segmentimeline(self, segment_timeline):
if isinstance(segment_timeline['SegmentTimeline']['S'], list):
x16 = segment_timeline['SegmentTimeline']['S']
else:
x16 = [segment_timeline['SegmentTimeline']['S']]
return x16
def force_instance(self, x):
if isinstance(x['Representation'], list):
X = x['Representation']
else:
X = [x['Representation']]
return X
def get_segment_template(self):
x = [item for (i, item) in enumerate(self.xml['MPD']['Period']['AdaptationSet']) if self.config.track_id == item["@id"]][0]
segment_level = [item['SegmentTemplate'] for (i, item) in enumerate(self.force_instance(x)) if self.config.format_id == item["@id"]][0]
return segment_level
def run(self):
urls = self.generate_segments()
print('\n' + self.output_file)
global temp_folder
aria2c_infile = 'aria2c_infile.txt'
if os.path.isfile(aria2c_infile):
os.remove(aria2c_infile)
temp_folder = self.output_file.replace('.mp4', '')
if os.path.exists(temp_folder):
shutil.rmtree(temp_folder)
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
if len(urls) > 1:
num_segments = int(math.log10(len(urls))) + 1
with open(aria2c_infile, 'a', encoding='utf8') as (file):
for (i, url) in enumerate(urls):
file.write(f'{url}\n')
file.write(f'\tout={temp_folder}.{i:0{num_segments}d}.mp4\n')
file.write(f'\tdir={temp_folder}\n')
file.flush()
self.download_track(aria2c_infile, self.output_file)
print('Done!')

View File

@ -0,0 +1,15 @@
config = {
'proxies': {
'none': None
},
}
class ProxyConfig(object):
def __init__(self, proxies):
self.config = config
self.config['proxies'] = proxies
def get_proxy(self, proxy):
return self.config['proxies'].get(proxy)

View File

@ -0,0 +1,53 @@
import logging, subprocess, re, base64
from pywidevine.cdm import cdm, deviceconfig
class WvDecrypt(object):
WV_SYSTEM_ID = [
237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]
def __init__(self, init_data_b64, cert_data_b64):
self.init_data_b64 = init_data_b64
self.cert_data_b64 = cert_data_b64
self.cdm = cdm.Cdm()
def check_pssh(pssh_b64):
pssh = base64.b64decode(pssh_b64)
if not pssh[12:28] == bytes(self.WV_SYSTEM_ID):
new_pssh = bytearray([0, 0, 0])
new_pssh.append(32 + len(pssh))
new_pssh[4:] = bytearray(b'pssh')
new_pssh[8:] = [0, 0, 0, 0]
new_pssh[13:] = self.WV_SYSTEM_ID
new_pssh[29:] = [0, 0, 0, 0]
new_pssh[31] = len(pssh)
new_pssh[32:] = pssh
return base64.b64encode(new_pssh)
else:
return pssh_b64
self.session = self.cdm.open_session(check_pssh(self.init_data_b64), deviceconfig.DeviceConfig(deviceconfig.device_android_generic))
if self.cert_data_b64:
self.cdm.set_service_certificate(self.session, self.cert_data_b64)
def log_message(self, msg):
return ('{}').format(msg)
def start_process(self):
keyswvdecrypt = []
try:
for key in self.cdm.get_keys(self.session):
if key.type == 'CONTENT':
keyswvdecrypt.append(self.log_message('{}:{}'.format(key.kid.hex(),key.key.hex())))
except Exception:
return (
False, keyswvdecrypt)
return (
True, keyswvdecrypt)
def get_challenge(self):
return self.cdm.get_license_request(self.session)
def update_license(self, license_b64):
self.cdm.provide_license(self.session, license_b64)
return True

View File

@ -0,0 +1,59 @@
# uncompyle6 version 3.7.3
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: pywidevine\decrypt\wvdecryptcustom.py
import logging, subprocess, re, base64
from pywidevine.cdm import cdm, deviceconfig
class WvDecrypt(object):
WV_SYSTEM_ID = [
237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]
def __init__(self, init_data_b64, cert_data_b64, device):
self.init_data_b64 = init_data_b64
self.cert_data_b64 = cert_data_b64
self.device = device
self.cdm = cdm.Cdm()
def check_pssh(pssh_b64):
pssh = base64.b64decode(pssh_b64)
if not pssh[12:28] == bytes(self.WV_SYSTEM_ID):
new_pssh = bytearray([0, 0, 0])
new_pssh.append(32 + len(pssh))
new_pssh[4:] = bytearray(b'pssh')
new_pssh[8:] = [0, 0, 0, 0]
new_pssh[13:] = self.WV_SYSTEM_ID
new_pssh[29:] = [0, 0, 0, 0]
new_pssh[31] = len(pssh)
new_pssh[32:] = pssh
return base64.b64encode(new_pssh)
else:
return pssh_b64
self.session = self.cdm.open_session(check_pssh(self.init_data_b64), deviceconfig.DeviceConfig(self.device))
if self.cert_data_b64:
self.cdm.set_service_certificate(self.session, self.cert_data_b64)
def log_message(self, msg):
return '{}'.format(msg)
def start_process(self):
keyswvdecrypt = []
try:
for key in self.cdm.get_keys(self.session):
if key.type == 'CONTENT':
keyswvdecrypt.append(self.log_message('{}:{}'.format(key.kid.hex(), key.key.hex())))
except Exception:
return (
False, keyswvdecrypt)
else:
return (
True, keyswvdecrypt)
def get_challenge(self):
return self.cdm.get_license_request(self.session)
def update_license(self, license_b64):
self.cdm.provide_license(self.session, license_b64)
return True

Binary file not shown.

271
pywidevine/muxer/muxer.py Normal file
View File

@ -0,0 +1,271 @@
# -*- coding: utf-8 -*-
import os
import subprocess
class Muxer(object):
def __init__(self, CurrentName, SeasonFolder, CurrentHeigh, Type, mkvmergeexe):
self.CurrentName = CurrentName
self.SeasonFolder = SeasonFolder
self.CurrentHeigh = CurrentHeigh
self.Type = Type
self.mkvmergeexe = mkvmergeexe
def mkvmerge_muxer(self, lang):
VideoInputNoExist = False
if os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].h264'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].h264'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].mkv'
if os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p].h264'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p].h264'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].mp4'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].mp4'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].h265'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].h265'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].mp4'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].mp4'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].vp9'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].vp9'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [VP9].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].mp4'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].mp4'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].h265'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].h265'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].mp4'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].mp4'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].h264'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].h264'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [AVC HIGH].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].mp4'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].mp4'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p] [CBR].mkv'
elif os.path.isfile(self.CurrentName + ' [' + self.CurrentHeigh + 'p].mp4'):
VideoInputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p].mp4'
if self.Type == "show":
VideoOutputName = os.path.join(self.SeasonFolder, self.CurrentName + ' [' + self.CurrentHeigh + 'p].mkv')
else:
VideoOutputName = self.CurrentName + ' [' + self.CurrentHeigh + 'p].mkv'
else:
VideoInputNoExist = True
if VideoInputNoExist == False:
AudioExtensionsList=[
".ac3",
".mka",
".eac3",
".m4a",
".dts",
".mp3",
".aac"
]
SubsExtensionsList= [
".srt",
".ass",
]
if lang == "English":
language_tag = "English"
if language_tag == "English":
subs_forced = 'Forced'
subs_sdh = 'SDH'
#["en", "en", "eng", "English", "yes", "yes"]
#[audio_language, subs_language, language_id, language_name, audio_default, subs_default]
LanguageList = [
["es-la", "es-la", "spa", "Spanish", "yes", "no"],
["es", "es", "spa", "Castilian", "no", "no"],
["en", "en", "eng", "English", "no", "no"],
["pt-BR", "pt-BR", "por", "Brazilian Portuguese", "no", "no"],
["cat", "cat", "cat", "Catalan", "no", "no"],
["eu", "eu", "baq", "Basque", "no", "no"],
["fr", "fr", "fre", "French", "no", "no"],
["fr-bg", "fr-bg", "fre", "French (Belgium)", "no", "no"],
["fr-lu", "fr-lu", "fre", "French (Luxembourg)", "no", "no"],
["fr-ca", "fr-ca", "fre", "French (Canada)", "no", "no"],
["de", "de", "ger", "German", "no", "no"],
["it", "it", "ita", "Italian", "no", "no"],
["pl", "pl", "pol", "Polish", "no", "no"],
["tr", "tr", "tur", "Turkish", "no", "no"],
["hy", "hy", "arm", "Armenian", "no", "no"],
["sv", "sv", "swe", "Swedish", "no", "no"],
["da", "da", "dan", "Danish", "no", "no"],
["fi", "fi", "fin", "Finnish", "no", "no"],
["nl", "nl", "dut", "Dutch", "no", "no"],
["nl-be", "nl-be", "dut", "Flemish", "no", "no"],
["no", "no", "nor", "Norwegian", "no", "no"],
["lv", "lv", "lav", "Latvian", "no", "no"],
["is", "is", "ice", "Icelandic", "no", "no"],
["ru", "ru", "rus", "Russian", "no", "no"],
["uk", "uk", "ukr", "Ukrainian", "no", "no"],
["hu", "hu", "hun", "Hungarian", "no", "no"],
["bg", "bg", "bul", "Bulgarian", "no", "no"],
["hr", "hr", "hrv", "Croatian", "no", "no"],
["lt", "lt", "lit", "Lithuanian", "no", "no"],
["et", "et", "est", "Estonian", "no", "no"],
["el", "el", "gre", "Greek", "no", "no"],
["he", "he", "heb", "Hebrew", "no", "no"],
["ar", "ar", "ara", "Arabic", "no", "no"],
["fa", "fa", "per", "Persian", "no", "no"],
["ro", "ro", "rum", "Romanian", "no", "no"],
["sr", "sr", "srp", "Serbian", "no", "no"],
["cs", "cs", "cze", "Czech", "no", "no"],
["sk", "sk", "slo", "Slovak", "no", "no"],
["sl", "sl", "slv", "Slovenian", "no", "no"],
["sq", "sq", "alb", "Albanian", "no", "no"],
["bs", "bs", "bos", "Bosnian", "no", "no"],
["mk", "mk", "mac", "Macedonian", "no", "no"],
["hi", "hi", "hin", "Hindi", "no", "no"],
["bn", "bn", "ben", "Bengali", "no", "no"],
["ur", "ur", "urd", "Urdu", "no", "no"],
["pa", "pa", "pan", "Punjabi", "no", "no"],
["ta", "ta", "tam", "Tamil", "no", "no"],
["te", "te", "tel", "Telugu", "no", "no"],
["mr", "mr", "mar", "Marathi", "no", "no"],
["kn", "kn", "kan", "Kannada (India)", "no", "no"],
["gu", "gu", "guj", "Gujarati", "no", "no"],
["ml", "ml", "mal", "Malayalam", "no", "no"],
["si", "si", "sin", "Sinhala", "no", "no"],
["as", "as", "asm", "Assamese", "no", "no"],
["mni", "mni", "mni", "Manipuri", "no", "no"],
["tl", "tl", "tgl", "Tagalog", "no", "no"],
["id", "id", "ind", "Indonesian", "no", "no"],
["ms", "ms", "may", "Malay", "no", "no"],
["fil", "fil", "fil", "Filipino", "no", "no"],
["vi", "vi", "vie", "Vietnamese", "no", "no"],
["th", "th", "tha", "Thai", "no", "no"],
["km", "km", "khm", "Khmer", "no", "no"],
["ko", "ko", "kor", "Korean", "no", "no"],
["zh", "zh", "chi", "Mandarin", "no", "no"],
["yue", "yue", "chi", "Cantonese", "no", "no"],
["zh-hans", "zh-hans", "chi", "Chinese (Simplified)", "no", "no"],
["zh-hant", "zh-hant", "chi", "Chinese (Traditional)", "no", "no"],
["zh-hk", "zh-hk", "chi", "Chinese (Simplified)", "no", "no"],
["zh-tw", "zh-tw", "chi", "Chinese (Traditional)", "no", "no"],
["zh-sg", "zh-sg", "chi", "Chinese (Singapore)", "no", "no"],
["ja", "ja", "jpn", "Japanese", "no", "no"],
["tlh", "tlh", "tlh", "Klingon", "no", "no"],
["zxx", "zxx", "zxx", "No Dialogue", "no", "no"]
]
ALLAUDIOS = []
default_active_audio = False
for audio_language, subs_language, language_id, language_name, audio_default, subs_default in LanguageList:
for AudioExtension in AudioExtensionsList:
if os.path.isfile(self.CurrentName + ' (' + audio_language + ')' + AudioExtension):
if default_active_audio == True: audio_default = "no"
ALLAUDIOS = ALLAUDIOS + ['--language', '0:' + audio_language, '--track-name', '0:' + language_name, '--default-track', '0:' + audio_default, '(', self.CurrentName + ' (' + audio_language + ')' + AudioExtension, ')']
if audio_default == "yes": default_active_audio = True
for audio_language, subs_language, language_id, language_name, audio_default, subs_default in LanguageList:
for AudioExtension in AudioExtensionsList:
if os.path.isfile(self.CurrentName + ' (' + audio_language + '-ad)' + AudioExtension):
if default_active_audio == True: audio_default = "no"
ALLAUDIOS = ALLAUDIOS + ['--language', '0:' + audio_language, '--track-name', '0:' + language_name + ' (Audio Description)', '--default-track', '0:no', '(', self.CurrentName + ' (' + audio_language + '-ad)' + AudioExtension, ')']
if audio_default == "yes": default_active_audio = True
OnlyOneLanguage = False
if len(ALLAUDIOS) == 9:
OnlyOneLanguage = True
elif len(ALLAUDIOS) == 18:
if ALLAUDIOS[1] == ALLAUDIOS[10]:
if '-ad' in ALLAUDIOS[7] or '-ad' in ALLAUDIOS[16]:
OnlyOneLanguage = True
else:
OnlyOneLanguage = False
ALLSUBS = []
default_active_subs = False
for audio_language, subs_language, language_id, language_name, audio_default, subs_default in LanguageList:
for SubsExtension in SubsExtensionsList:
if os.path.isfile(self.CurrentName + ' (' + subs_language + '-forced)' + SubsExtension):
if subs_default == "yes": default_active_subs = True
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + subs_forced, '--forced-track', '0:yes', '--default-track', '0:' + subs_default, '--compression', '0:none', '(', self.CurrentName + ' (' + subs_language + '-forced)' + SubsExtension, ')']
if OnlyOneLanguage == True:
if default_active_subs == True: subs_default = "no"
if os.path.isfile(self.CurrentName + ' (' + subs_language + ')' + SubsExtension):
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--forced-track', '0:no', '--default-track', '0:' + subs_default, '--compression', '0:none', '(', self.CurrentName + ' (' + subs_language + ')' + SubsExtension, ')']
else:
if os.path.isfile(self.CurrentName + ' (' + subs_language + ')' + SubsExtension):
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--forced-track', '0:no', '--default-track', '0:no', '--compression', '0:none', '(', self.CurrentName + ' (' + subs_language + ')' + SubsExtension, ')']
if os.path.isfile(self.CurrentName + ' (' + subs_language + '-sdh)' + SubsExtension):
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + subs_sdh, '--forced-track', '0:no', '--default-track', '0:no', '--compression', '0:none', '(', self.CurrentName + ' (' + subs_language + '-sdh)' + SubsExtension, ')']
#(Chapters)
if os.path.isfile(self.CurrentName+' Chapters.txt'):
CHAPTERS=['--chapter-charset', 'UTF-8', '--chapters', self.CurrentName + ' Chapters.txt']
else:
CHAPTERS=[]
mkvmerge_command_video = [self.mkvmergeexe,
'-q',
'--output',
VideoOutputName,
'--language',
'0:und',
'--default-track',
'0:yes',
'(',
VideoInputName,
')']
mkvmerge_command = mkvmerge_command_video + ALLAUDIOS + ALLSUBS + CHAPTERS
mkvmerge_process = subprocess.run(mkvmerge_command)

16
requirements.txt Normal file
View File

@ -0,0 +1,16 @@
pymediainfo==5.1.0
future==0.18.2
xmltodict==0.12.0
m3u8==0.9.0
requests==2.25.1
asgiref==3.4.1
titlecase==2.2.0
graphql-core==2.3.2
streamlink==2.2.0
Unidecode==1.2.0
tqdm==4.61.1
isodate==0.6.0
graphql-relay==2.0.1
prettytable==2.1.0
urllib3==1.26.5
PyJWT==2.1.0