From 42976540525802621bd52c5919b88431395a8f06 Mon Sep 17 00:00:00 2001
From: widevinedump <96489252+widevinedump@users.noreply.github.com>
Date: Sat, 25 Dec 2021 10:43:24 +0530
Subject: [PATCH] New
---
DOWNLOAD.cmd | 2 +
KEYS/PARAMOUNTPLUS.txt | 1 +
README.md | 40 +-
bad37.py | 128 +
cookies/cookies_pmnp.txt | 0
paramountplus.py | 910 +++++
pywidevine/cdm/__init__.py | 0
pywidevine/cdm/cdm.py | 380 ++
pywidevine/cdm/cdm_debug.py | 324 ++
pywidevine/cdm/cdmapi.pyd | Bin 0 -> 12146688 bytes
pywidevine/cdm/deviceconfig.py | 54 +
pywidevine/cdm/deviceconfig.py.example | 88 +
pywidevine/cdm/devices/.gitkeep | 0
.../chromecdm_2209/device_client_id_blob | Bin 0 -> 1424 bytes
.../devices/chromecdm_2209/device_vmp_blob | Bin 0 -> 2201 bytes
pywidevine/cdm/formats/__init__.py | 0
.../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 172 bytes
.../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 187 bytes
.../__pycache__/wv_proto2_pb2.cpython-37.pyc | Bin 0 -> 44608 bytes
.../__pycache__/wv_proto2_pb2.cpython-39.pyc | Bin 0 -> 45481 bytes
pywidevine/cdm/formats/wv_proto2.proto | 466 +++
pywidevine/cdm/formats/wv_proto2_pb2.py | 3324 +++++++++++++++++
pywidevine/cdm/formats/wv_proto3.proto | 389 ++
pywidevine/cdm/formats/wv_proto3_pb2.py | 2686 +++++++++++++
pywidevine/cdm/key.py | 15 +
pywidevine/cdm/session.py | 18 +
pywidevine/cdm/vmp.py | 102 +
.../__pycache__/proxy_config.cpython-39.pyc | Bin 0 -> 695 bytes
pywidevine/clients/dictionary.py | 44 +
.../__pycache__/config.cpython-37.pyc | Bin 0 -> 1239 bytes
.../__pycache__/config.cpython-39.pyc | Bin 0 -> 1264 bytes
.../__pycache__/downloader.cpython-37.pyc | Bin 0 -> 4516 bytes
.../__pycache__/downloader.cpython-39.pyc | Bin 0 -> 4585 bytes
pywidevine/clients/paramountplus/config.py | 35 +
.../clients/paramountplus/downloader.py | 116 +
pywidevine/clients/proxy_config.py | 15 +
pywidevine/decrypt/wvdecrypt.py | 53 +
pywidevine/decrypt/wvdecryptcustom.py | 59 +
.../muxer/__pycache__/muxer.cpython-39.pyc | Bin 0 -> 7810 bytes
pywidevine/muxer/muxer.py | 271 ++
requirements.txt | 16 +
41 files changed, 9534 insertions(+), 2 deletions(-)
create mode 100644 DOWNLOAD.cmd
create mode 100644 KEYS/PARAMOUNTPLUS.txt
create mode 100644 bad37.py
create mode 100644 cookies/cookies_pmnp.txt
create mode 100644 paramountplus.py
create mode 100644 pywidevine/cdm/__init__.py
create mode 100644 pywidevine/cdm/cdm.py
create mode 100644 pywidevine/cdm/cdm_debug.py
create mode 100644 pywidevine/cdm/cdmapi.pyd
create mode 100644 pywidevine/cdm/deviceconfig.py
create mode 100644 pywidevine/cdm/deviceconfig.py.example
create mode 100644 pywidevine/cdm/devices/.gitkeep
create mode 100644 pywidevine/cdm/devices/chromecdm_2209/device_client_id_blob
create mode 100644 pywidevine/cdm/devices/chromecdm_2209/device_vmp_blob
create mode 100644 pywidevine/cdm/formats/__init__.py
create mode 100644 pywidevine/cdm/formats/__pycache__/__init__.cpython-37.pyc
create mode 100644 pywidevine/cdm/formats/__pycache__/__init__.cpython-39.pyc
create mode 100644 pywidevine/cdm/formats/__pycache__/wv_proto2_pb2.cpython-37.pyc
create mode 100644 pywidevine/cdm/formats/__pycache__/wv_proto2_pb2.cpython-39.pyc
create mode 100644 pywidevine/cdm/formats/wv_proto2.proto
create mode 100644 pywidevine/cdm/formats/wv_proto2_pb2.py
create mode 100644 pywidevine/cdm/formats/wv_proto3.proto
create mode 100644 pywidevine/cdm/formats/wv_proto3_pb2.py
create mode 100644 pywidevine/cdm/key.py
create mode 100644 pywidevine/cdm/session.py
create mode 100644 pywidevine/cdm/vmp.py
create mode 100644 pywidevine/clients/__pycache__/proxy_config.cpython-39.pyc
create mode 100644 pywidevine/clients/dictionary.py
create mode 100644 pywidevine/clients/paramountplus/__pycache__/config.cpython-37.pyc
create mode 100644 pywidevine/clients/paramountplus/__pycache__/config.cpython-39.pyc
create mode 100644 pywidevine/clients/paramountplus/__pycache__/downloader.cpython-37.pyc
create mode 100644 pywidevine/clients/paramountplus/__pycache__/downloader.cpython-39.pyc
create mode 100644 pywidevine/clients/paramountplus/config.py
create mode 100644 pywidevine/clients/paramountplus/downloader.py
create mode 100644 pywidevine/clients/proxy_config.py
create mode 100644 pywidevine/decrypt/wvdecrypt.py
create mode 100644 pywidevine/decrypt/wvdecryptcustom.py
create mode 100644 pywidevine/muxer/__pycache__/muxer.cpython-39.pyc
create mode 100644 pywidevine/muxer/muxer.py
create mode 100644 requirements.txt
diff --git a/DOWNLOAD.cmd b/DOWNLOAD.cmd
new file mode 100644
index 0000000..c444fec
--- /dev/null
+++ b/DOWNLOAD.cmd
@@ -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
\ No newline at end of file
diff --git a/KEYS/PARAMOUNTPLUS.txt b/KEYS/PARAMOUNTPLUS.txt
new file mode 100644
index 0000000..4cb5550
--- /dev/null
+++ b/KEYS/PARAMOUNTPLUS.txt
@@ -0,0 +1 @@
+##### One KEY per line. #####
diff --git a/README.md b/README.md
index f6ec4d6..c31c6f8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,38 @@
-# Paramount Plus 4k Downloader
- Tool To Get Downloads up to 4k from Paramount+
+
+
Paramount 4K Downloader
+
+
+ Tool To Get Downloads up to 4k from Paramount+ :smile:
+
+
+
+
+
+
+ Hello Fellow < Developers/ >!
+
+
+
+
+
+ Hi! My name is WVDUMP. I am Leaking the scripts to punish few idiots :smile:
+
+
+ About Me
+
+
+
+- 🔠I’m currently working on Java scripts
+
+- 🌱 I’m currently learning Python
+
+- 👯 Sharing is caring
+
+
+- âš¡ CDM IS NOT INCLUDED BUY it from wvfuck@cyberfiends.net âš¡
+
+
+
+
+
+
diff --git a/bad37.py b/bad37.py
new file mode 100644
index 0000000..e188658
--- /dev/null
+++ b/bad37.py
@@ -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)
\ No newline at end of file
diff --git a/cookies/cookies_pmnp.txt b/cookies/cookies_pmnp.txt
new file mode 100644
index 0000000..e69de29
diff --git a/paramountplus.py b/paramountplus.py
new file mode 100644
index 0000000..bb8b430
--- /dev/null
+++ b/paramountplus.py
@@ -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>)(<)(?:[A-Za-z0-9_ -=]*)(>)')
+ pattern2 = re.compile('(?!<\\/i>|<\\/b>|<\\/u>)(<\\/)(?:[A-Za-z0-9_ -=]*)(>)')
+ X = X.replace('', '').replace('{\\an1}', '').replace('{\\an2}', '').replace('{\\an3}', '').replace('{\\an4}', '').replace('{\\an5}', '').replace('{\\an6}', '').replace('{\\an7}', '').replace('{\\an8}', '').replace('{\\an9}', '').replace('', '')
+ 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('()(?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('()(?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()
diff --git a/pywidevine/cdm/__init__.py b/pywidevine/cdm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pywidevine/cdm/cdm.py b/pywidevine/cdm/cdm.py
new file mode 100644
index 0000000..6b92e6f
--- /dev/null
+++ b/pywidevine/cdm/cdm.py
@@ -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
diff --git a/pywidevine/cdm/cdm_debug.py b/pywidevine/cdm/cdm_debug.py
new file mode 100644
index 0000000..bc67ca0
--- /dev/null
+++ b/pywidevine/cdm/cdm_debug.py
@@ -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
diff --git a/pywidevine/cdm/cdmapi.pyd b/pywidevine/cdm/cdmapi.pyd
new file mode 100644
index 0000000000000000000000000000000000000000..1a6ce7397fbd007b07b445566567ee6232cda8c1
GIT binary patch
literal 12146688
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 @iz2JfFIX_o7l^Pv)vaoZ)tv7yPQI5$lxmC-EZ
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 |