Main
This commit is contained in:
949
hbomax.py
Normal file
949
hbomax.py
Normal file
@@ -0,0 +1,949 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Module: HBO Max Downloader
|
||||
# Created on: 04-11-2020
|
||||
# Version: 3.5
|
||||
|
||||
import sys, os
|
||||
import subprocess, re, base64, requests
|
||||
import xmltodict, isodate
|
||||
import time, glob, uuid, ffmpy, json
|
||||
import shutil, urllib.parse
|
||||
|
||||
from unidecode import unidecode
|
||||
|
||||
import pywidevine.clients.hbomax.config as HMAXConfig
|
||||
import pywidevine.clients.hbomax.client as HMAXClient
|
||||
|
||||
from pywidevine.clients.hbomax.config import HMAXRegion
|
||||
from pywidevine.clients.proxy_config import ProxyConfig
|
||||
from pywidevine.muxer.muxer import Muxer
|
||||
from os.path import join, isfile
|
||||
|
||||
currentFile = 'hbomax'
|
||||
realPath = os.path.realpath(currentFile)
|
||||
dirPath = os.path.dirname(realPath)
|
||||
SESSION = requests.session()
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
|
||||
|
||||
def main(args):
|
||||
|
||||
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/HBOMAX.txt'
|
||||
try:
|
||||
keys_file_hbomax = open(keys_file, 'r', encoding='utf8')
|
||||
keys_file_txt = keys_file_hbomax.readlines()
|
||||
except Exception:
|
||||
with open(keys_file, 'a', encoding='utf8') as (file):
|
||||
file.write('##### One KEY per line. #####\n')
|
||||
keys_file_hbomax = open(keys_file, 'r', encoding='utf8')
|
||||
keys_file_txt = keys_file_hbomax.readlines()
|
||||
|
||||
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 downloadFile(aria2c_infile):
|
||||
aria2c_opts = [
|
||||
HMAXConfig.ARIA2C,
|
||||
'--allow-overwrite=true',
|
||||
'--download-result=hide',
|
||||
'--console-log-level=warn',
|
||||
'--enable-color=false',
|
||||
'-x16', '-s16', '-j16',
|
||||
'-i', aria2c_infile]
|
||||
subprocess.run(aria2c_opts, check=True)
|
||||
|
||||
def downloadFile2(link, file_name):
|
||||
with open(file_name, 'wb') as (f):
|
||||
print(file_name)
|
||||
response = SESSION.get(link, stream=True)
|
||||
total_length = response.headers.get('content-length')
|
||||
if total_length is None:
|
||||
f.write(response.content)
|
||||
else:
|
||||
dl = 0
|
||||
total_length = int(total_length)
|
||||
for data in response.iter_content(chunk_size=4096):
|
||||
dl += len(data)
|
||||
f.write(data)
|
||||
|
||||
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 getKeyId(name):
|
||||
mp4dump = subprocess.Popen([HMAXConfig.MP4DUMP, name], stdout=subprocess.PIPE)
|
||||
mp4dump = str(mp4dump.stdout.read())
|
||||
A=find_str(mp4dump, "default_KID")
|
||||
KEY_ID_ORI=mp4dump[A:A+63].replace("default_KID = ", "").replace("[", "").replace("]", "").replace(" ", "")
|
||||
if KEY_ID_ORI == "":
|
||||
KEY_ID_ORI = "nothing"
|
||||
return KEY_ID_ORI
|
||||
|
||||
def mediainfo_(file):
|
||||
mediainfo_output = subprocess.Popen([HMAXConfig.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('\\', '').replace('/', ' & ')
|
||||
return unidecode(x)
|
||||
|
||||
def ReplaceCodeLanguages(X):
|
||||
X = X.lower()
|
||||
X = X.replace('_subtitle_dialog_0', '').replace('_narrative_dialog_0', '').replace('_caption_dialog_0', '').replace('_dialog_0', '').replace('_descriptive_0', '_descriptive').replace('_descriptive', '_descriptive').replace('_sdh', '-sdh').replace('es-es', 'es').replace('SPA', 'es').replace('en-es', 'es').replace('kn-in', 'kn').replace('gu-in', 'gu').replace('ja-jp', 'ja').replace('mni-in', 'mni').replace('si-in', 'si').replace('as-in', 'as').replace('ml-in', 'ml').replace('sv-se', 'sv').replace('hy-hy', 'hy').replace('sv-sv', 'sv').replace('da-da', 'da').replace('fi-fi', 'fi').replace('nb-nb', 'nb').replace('is-is', 'is').replace('uk-uk', 'uk').replace('hu-hu', 'hu').replace('bg-bg', 'bg').replace('hr-hr', 'hr').replace('lt-lt', 'lt').replace('et-et', 'et').replace('el-el', 'el').replace('he-he', 'he').replace('ar-ar', 'ar').replace('fa-fa', 'fa').replace('ENG', 'en').replace('ro-ro', 'ro').replace('sr-sr', 'sr').replace('cs-cs', 'cs').replace('sk-sk', 'sk').replace('mk-mk', 'mk').replace('hi-hi', 'hi').replace('bn-bn', 'bn').replace('ur-ur', 'ur').replace('pa-pa', 'pa').replace('ta-ta', 'ta').replace('te-te', 'te').replace('mr-mr', 'mr').replace('kn-kn', 'kn').replace('gu-gu', 'gu').replace('ml-ml', 'ml').replace('si-si', 'si').replace('as-as', 'as').replace('mni-mni', 'mni').replace('tl-tl', 'tl').replace('id-id', 'id').replace('ms-ms', 'ms').replace('vi-vi', 'vi').replace('th-th', 'th').replace('km-km', 'km').replace('ko-ko', 'ko').replace('zh-zh', 'zh').replace('ja-ja', 'ja').replace('ru-ru', 'ru').replace('tr-tr', 'tr').replace('it-it', 'it').replace('es-mx', 'es-la').replace('ar-sa', 'ar').replace('zh-cn', 'zh').replace('nl-nl', 'nl').replace('pl-pl', 'pl').replace('pt-pt', 'pt').replace('hi-in', 'hi').replace('mr-in', 'mr').replace('bn-in', 'bn').replace('te-in', 'te').replace('POR', 'pt').replace('cmn-hans', 'zh-hans').replace('cmn-hant', 'zh-hant').replace('ko-kr', 'ko').replace('en-au', 'en').replace('es-419', 'es-la').replace('es-us', 'es-la').replace('en-us', 'en').replace('en-gb', 'en').replace('fr-fr', 'fr').replace('de-de', 'de').replace('las-419', 'es-la').replace('ar-ae', 'ar').replace('da-dk', 'da').replace('yue-hant', 'yue').replace('bn-in', 'bn').replace('ur-in', 'ur').replace('ta-in', 'ta').replace('sl-si', 'sl').replace('cs-cz', 'cs').replace('hi-jp', 'hi').replace('-001', '').replace('en-US', 'en').replace('deu', 'de').replace('eng', 'en').replace('ca-es', 'cat').replace('fil-ph', 'fil').replace('en-ca', 'en').replace('eu-es', 'eu').replace('ar-eg', 'ar').replace('he-il', 'he').replace('el-gr', 'he').replace('nb-no', 'nb').replace('es-ar', 'es-la').replace('en-ph', 'en').replace('sq-al', 'sq').replace('bs-ba', 'bs')
|
||||
return X
|
||||
|
||||
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'
|
||||
|
||||
global auth_url, content_url, license_wv
|
||||
|
||||
if args.region == "la":
|
||||
auth_url, content_url, license_wv = HMAXRegion.configHBOMaxLatam()
|
||||
|
||||
if args.region == "us":
|
||||
auth_url, content_url, license_wv = HMAXRegion.configHBOMaxUS()
|
||||
|
||||
def get_authorization_header(TOKEN):
|
||||
headers = HMAXConfig.get_user_headers()['headers']
|
||||
|
||||
headers = {
|
||||
"accept": "application/vnd.hbo.v9.full+json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"accept-language": str(args.titlelang),
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
"user-agent": HMAXConfig.UA,
|
||||
"x-hbo-client-version": "Hadron/50.40.0.111 desktop (DESKTOP)",
|
||||
"x-hbo-device-name": "desktop",
|
||||
"x-hbo-device-os-version": "undefined"
|
||||
}
|
||||
return headers
|
||||
|
||||
os.makedirs(HMAXConfig.COOKIES_FOLDER, exist_ok=True)
|
||||
HMAXTOKEN_FILE = join(HMAXConfig.COOKIES_FOLDER, 'hmax_login_data.json')
|
||||
if not isfile(HMAXTOKEN_FILE):
|
||||
access_token = HMAXClient.login(SESSION, auth_url, content_url)
|
||||
|
||||
def refresh_token():
|
||||
content = None
|
||||
TOKEN = False
|
||||
with open(HMAXTOKEN_FILE,'rb') as f:
|
||||
content = f.read().decode('utf-8')
|
||||
jso = json.loads(content)
|
||||
token_exp = int(time.time()) - jso["EXPIRATION_TIME"]
|
||||
if int(token_exp/60) > 15:
|
||||
TOKEN = False
|
||||
elif int(token_exp/60) < 15:
|
||||
TOKEN = True
|
||||
|
||||
if TOKEN:
|
||||
access_token = jso['ACCESS_TOKEN']
|
||||
|
||||
if not TOKEN:
|
||||
access_token = HMAXClient.login(SESSION, auth_url, content_url)
|
||||
return get_authorization_header(access_token)
|
||||
|
||||
def mpd_parse(mpd_url):
|
||||
if args.atmos:
|
||||
mpd_url = mpd_url.replace('_noatmos', '')
|
||||
base_url = mpd_url.rsplit('/', 1)[0] + '/'
|
||||
r = SESSION.get(url=mpd_url)
|
||||
xml = xmltodict.parse(r.text, force_list={
|
||||
'Period', 'AdaptationSet', 'ContentProtection'
|
||||
})
|
||||
mpd = json.loads(json.dumps(xml))
|
||||
period = mpd['MPD']['Period']
|
||||
tracks = []
|
||||
for pb in period:
|
||||
tracks = tracks + pb['AdaptationSet']
|
||||
|
||||
def get_height(width, height):
|
||||
if width == '1920':
|
||||
return '1080'
|
||||
elif width in ('1280', '1248'):
|
||||
return '720'
|
||||
else:
|
||||
return height
|
||||
|
||||
def force_instance(x):
|
||||
if isinstance(x['Representation'], list):
|
||||
X = x['Representation']
|
||||
else:
|
||||
X = [x['Representation']]
|
||||
return X
|
||||
|
||||
def get_pssh(track):
|
||||
pssh = ''
|
||||
for t in track.get('ContentProtection', {}):
|
||||
if (t['@schemeIdUri'].lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
|
||||
and t.get('pssh', {}).get('#text')):
|
||||
pssh = t.get('pssh', {}).get('#text')
|
||||
return pssh
|
||||
|
||||
video_list = []
|
||||
for video_tracks in tracks:
|
||||
if video_tracks['@contentType'] == 'video':
|
||||
for x in video_tracks['Representation']:
|
||||
videoDict = {
|
||||
'Height':get_height(x['@width'], x['@height']),
|
||||
'Width':x['@width'],
|
||||
'Bandwidth':x['@bandwidth'],
|
||||
'ID':x['@id'],
|
||||
'Codec':x['@codecs'],
|
||||
'File_URL':x['BaseURL']}
|
||||
video_list.append(videoDict)
|
||||
video_list = sorted(video_list, key=(lambda k: int(k['Bandwidth'])))
|
||||
|
||||
if args.videocodec:
|
||||
if args.videocodec == 'h264':
|
||||
codec_s = 'avc1'
|
||||
if args.videocodec == 'hevc':
|
||||
codec_s = 'hvc1'
|
||||
if args.videocodec == 'hdr':
|
||||
codec_s = 'dvh1'
|
||||
|
||||
video_list_tmp = []
|
||||
for x in video_list:
|
||||
if codec_s in x['Codec']:
|
||||
video_list_tmp.append(x)
|
||||
video_list = video_list_tmp
|
||||
|
||||
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':
|
||||
isAD = False
|
||||
pssh = get_pssh(audio_tracks)
|
||||
try:
|
||||
if audio_tracks['Role']['@value']:
|
||||
isAD = True
|
||||
except KeyError:
|
||||
isAD = False
|
||||
|
||||
if isAD:
|
||||
lang_id = ReplaceCodeLanguages(audio_tracks["@lang"]) + '-ad'
|
||||
else:
|
||||
lang_id = ReplaceCodeLanguages(audio_tracks["@lang"])
|
||||
|
||||
for x in force_instance(audio_tracks):
|
||||
audio_dict = {
|
||||
'Bandwidth':x['@bandwidth'],
|
||||
'ID':x['@id'],
|
||||
'Language':lang_id,
|
||||
'Codec':x['@codecs'],
|
||||
'Channels':x['AudioChannelConfiguration']['@value'],
|
||||
'File_URL':x['BaseURL'],
|
||||
'isAD':isAD}
|
||||
audio_list.append(audio_dict)
|
||||
|
||||
audio_list = sorted(audio_list, key=(lambda k: (int(k['Bandwidth']), 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
|
||||
|
||||
if args.desc_audio:
|
||||
c = 0
|
||||
while c != len(audio_list):
|
||||
if not audio_list[c]['isAD']:
|
||||
audio_list.remove(audio_list[c])
|
||||
else:
|
||||
c += 1
|
||||
else:
|
||||
c = 0
|
||||
while c != len(audio_list):
|
||||
if audio_list[c]['isAD']:
|
||||
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'],
|
||||
'Channels': x['Channels'],
|
||||
'File_URL':x['File_URL'],
|
||||
'isAD':x['isAD']
|
||||
}
|
||||
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
|
||||
|
||||
return (video_list, audio_list, pssh, base_url)
|
||||
|
||||
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
|
||||
|
||||
def get_season(series_id):
|
||||
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)]
|
||||
|
||||
season_req = SESSION.post(url=content_url, headers=refresh_token(), json=[{"id":series_id}], proxies=proxy_cfg.get_proxy('meta')).json()[0]['body']
|
||||
try:
|
||||
if seasons == 'all':
|
||||
seasons = [num for num, season in enumerate(season_req['references']['seasons'], start=1)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for season_num in seasons:
|
||||
if args.all_season:
|
||||
episode_list = season_req['references']['episodes']
|
||||
else:
|
||||
try:
|
||||
season_id = season_req['references']['seasons'][int(season_num)-1]
|
||||
episode_req = SESSION.post(url=content_url, headers=refresh_token(), json=[{"id":season_id}], proxies=proxy_cfg.get_proxy('meta')).json()[0]['body']
|
||||
episode_list = episode_req['references']['episodes']
|
||||
except KeyError:
|
||||
episode_list = season_req['references']['episodes']
|
||||
|
||||
episodes_list_new = []
|
||||
for num, ep in enumerate(episode_list, start=1):
|
||||
episodes_list_new.insert(num - 0, {
|
||||
'id': ep,
|
||||
'episode_num': num})
|
||||
episode_list = sorted(episodes_list_new, key=lambda x: x['episode_num'])
|
||||
|
||||
if args.episodeStart:
|
||||
eps = get_episodes(args.episodeStart, len(episode_list))
|
||||
episode_list = [x for x in episode_list if x['episode_num'] in eps]
|
||||
|
||||
for episode in episode_list:
|
||||
get_metadata(content_id=episode['id'])
|
||||
|
||||
def get_video_id(content_id):
|
||||
video_id = 'preview'
|
||||
while 'preview' in video_id:
|
||||
video_resp = SESSION.post(url=content_url, headers=refresh_token(), json=HMAXClient.get_video_payload(content_id)).json()
|
||||
if video_resp[0]["statusCode"] > 200:
|
||||
print(video_resp[0]['body']['message'])
|
||||
exit(1)
|
||||
video_id = [item['body']['references']['video'] for (i, item) in enumerate(video_resp) if 'video' in item['body']['references']][0]
|
||||
mpd_url, length, subs_list, chapters = get_infos(video_id)
|
||||
return video_resp[0]['body'], mpd_url, length, subs_list, chapters
|
||||
|
||||
def get_infos(video_id):
|
||||
video_json = SESSION.post(url=content_url, headers=refresh_token(), json=HMAXClient.get_video_payload(video_id)).json()[0]['body']
|
||||
try:
|
||||
mpd_url = video_json['fallbackManifest']
|
||||
except KeyError:
|
||||
mpd_url = video_json['manifest']
|
||||
for x in video_json['videos']:
|
||||
if x['type'] == 'urn:video:main':
|
||||
length = float(x['duration'])
|
||||
return mpd_url, length, get_subtitles(video_json), get_chapters(video_json)
|
||||
|
||||
def get_chapters(video_json):
|
||||
chapters = []
|
||||
for x in video_json['videos']:
|
||||
if 'annotations' in x:
|
||||
for (i, chapter) in enumerate(x['annotations']):
|
||||
secs, ms = divmod(chapter['start'], 1)
|
||||
mins, secs = divmod(secs, 60)
|
||||
hours, mins = divmod(mins, 60)
|
||||
ms = ms * 10000;
|
||||
chapter_time = '%02d:%02d:%02d.%04d' % (hours, mins, secs, ms)
|
||||
chapters.append({'TEXT':chapter['secondaryType'], 'TIME': chapter_time})
|
||||
return chapters
|
||||
|
||||
def get_subtitles(video_json):
|
||||
subs_list = []
|
||||
for x in video_json['videos']:
|
||||
if x['type'] == 'urn:video:main':
|
||||
if 'textTracks' in x:
|
||||
for sub in x['textTracks']:
|
||||
|
||||
isCC = False
|
||||
if 'ClosedCaptions' in sub["type"]:
|
||||
isCC = True
|
||||
isNormal = False
|
||||
if isCC:
|
||||
lang_id = ReplaceCodeLanguages(sub['language']) + '-sdh'
|
||||
trackType = 'SDH'
|
||||
else:
|
||||
lang_id = ReplaceCodeLanguages(sub['language'])
|
||||
isNormal = True
|
||||
trackType = 'NORMAL'
|
||||
isForced = False
|
||||
if sub["type"] == "Forced":
|
||||
isForced = True
|
||||
isNormal = False
|
||||
trackType = 'FORCED'
|
||||
lang_id = ReplaceCodeLanguages(sub['language']) + '-forced'
|
||||
subsDict = {
|
||||
'Language':lang_id,
|
||||
'URL':sub['url'],
|
||||
'isCC':isCC,
|
||||
'isForced':isForced,
|
||||
'isNormal':isNormal,
|
||||
'Type':trackType}
|
||||
subs_list.append(subsDict)
|
||||
|
||||
subs_list_new = []
|
||||
subs_for_list_new = []
|
||||
for subs in subs_list:
|
||||
isForced = subs['isForced']
|
||||
if isForced:
|
||||
subs_for_list_new.append(subs)
|
||||
else:
|
||||
subs_list_new.append(subs)
|
||||
|
||||
subs_for_list = []
|
||||
for subs in subs_for_list_new:
|
||||
lang = subs['Language']
|
||||
if args.forcedlang:
|
||||
if lang in args.forcedlang:
|
||||
subs_for_list.append(subs)
|
||||
else:
|
||||
subs_for_list.append(subs)
|
||||
|
||||
subs_list = []
|
||||
for subs in subs_list_new:
|
||||
lang = subs['Language']
|
||||
if args.sublang:
|
||||
if lang in args.sublang:
|
||||
subs_list.append(subs)
|
||||
else:
|
||||
subs_list.append(subs)
|
||||
|
||||
subs_list_new = []
|
||||
subs_list_new = subs_list + subs_for_list
|
||||
subs_list = subs_list_new
|
||||
|
||||
return subs_list
|
||||
|
||||
def get_metadata(content_id):
|
||||
meta_resp, mpd_url, length, subs_list, chapters = get_video_id(content_id)
|
||||
|
||||
if 'feature' in args.url_season:
|
||||
hbomaxType = "movie"
|
||||
releaseYear = meta_resp['releaseYear']
|
||||
seriesTitles = meta_resp['titles']['full']
|
||||
episodeTitle = meta_resp['titles']['full']
|
||||
|
||||
if 'numberInSeries' in meta_resp:
|
||||
hbomaxType = "show"
|
||||
numberInSeries = meta_resp['numberInSeries']
|
||||
seriesTitles = meta_resp['seriesTitles']['full']
|
||||
episodeTitle = meta_resp['titles']['full']
|
||||
|
||||
if 'numberInSeason' in meta_resp:
|
||||
hbomaxType = "show"
|
||||
seriesTitles = meta_resp['seriesTitles']['full']
|
||||
seasonNumber = meta_resp['seasonNumber']
|
||||
episodeNumber = meta_resp['numberInSeason']
|
||||
episodeTitle = meta_resp['titles']['full']
|
||||
|
||||
if hbomaxType=="movie":
|
||||
seriesName = f'{replace_words(episodeTitle)} ({releaseYear})'
|
||||
folderName = None
|
||||
|
||||
if hbomaxType=="show":
|
||||
try:
|
||||
seriesName = f'{replace_words(seriesTitles)} S{seasonNumber:02}E{episodeNumber:02} - {replace_words(episodeTitle)}'
|
||||
folderName = f'{replace_words(seriesTitles)} S{seasonNumber:02}'
|
||||
except UnboundLocalError:
|
||||
seriesName = f'{replace_words(seriesTitles)} E{numberInSeries:02} - {replace_words(episodeTitle)}'
|
||||
folderName = f'{replace_words(seriesTitles)}'
|
||||
|
||||
start_process(seriesName, folderName, subs_list, mpd_url, length, chapters, hbomaxType)
|
||||
|
||||
def start_process(seriesName, folderName, subs_list, mpd_url, length, chapters, hbomaxType):
|
||||
video_list, audio_list, pssh, base_url = mpd_parse(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'])
|
||||
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 = str(dict(z)['Language'])
|
||||
sub_profile = str(dict(z)['Type'])
|
||||
print('SUBTITLE - Profile: '+ sub_profile +' - Language: ' + sub_lang)
|
||||
print()
|
||||
print('Name: ' + seriesName + '\n')
|
||||
|
||||
if args.license:
|
||||
format_mpd = ""
|
||||
if 'hvc1' in video_codec:
|
||||
format_mpd = "HEVC KEYS"
|
||||
keys_all = get_keys(pssh)
|
||||
with open(keys_file, 'a', encoding='utf8') as (file):
|
||||
file.write(seriesName + format_mpd + '\n')
|
||||
for key in keys_all:
|
||||
with open(keys_file, 'a', encoding='utf8') as (file):
|
||||
file.write(key + '\n')
|
||||
print('\n' + seriesName + ' ' + format_mpd + '\n' + key)
|
||||
|
||||
else:
|
||||
'''
|
||||
if args.tag:
|
||||
from pywidevine.clients.dictionary import get_release_tag
|
||||
for x in audio_list:
|
||||
isDual = False
|
||||
audio_total = len(audio_list)
|
||||
if audio_total > 1:
|
||||
isDual = True
|
||||
seriesName = get_release_tag(seriesName, video_codec, video_height, x['Codec'], x['Channels'], x['Bandwidth'], 'HMAX', str(args.tag), isDual)
|
||||
'''
|
||||
|
||||
if hbomaxType == 'show':
|
||||
CurrentName = seriesName
|
||||
CurrentHeigh = str(video_height)
|
||||
if 'hvc1' in video_codec:
|
||||
VideoOutputName = folderdownloader + '\\' + str(folderName) + str(CurrentName) + ' [' + str(CurrentHeigh) + 'p] [HEVC].mkv'
|
||||
if 'dvh1' in video_codec:
|
||||
VideoOutputName = folderdownloader + '\\' + str(folderName) + str(CurrentName) + ' [' + str(CurrentHeigh) + 'p] [HDR].mkv'
|
||||
else:
|
||||
VideoOutputName = folderdownloader + '\\' + str(folderName) + str(CurrentName) + ' [' + str(CurrentHeigh) + 'p].mkv'
|
||||
|
||||
else:
|
||||
CurrentName = seriesName
|
||||
CurrentHeigh = str(video_height)
|
||||
if 'hvc1' in video_codec:
|
||||
VideoOutputName = str(CurrentName) + ' [' + str(CurrentHeigh) + 'p] [HEVC].mkv'
|
||||
if 'dvh1' in video_codec:
|
||||
VideoOutputName = str(CurrentName) + ' [' + str(CurrentHeigh) + 'p] [HDR].mkv'
|
||||
else:
|
||||
VideoOutputName = str(CurrentName) + ' [' + str(CurrentHeigh) + 'p].mkv'
|
||||
|
||||
if not args.novideo or (not args.noaudio):
|
||||
print("Getting KEYS...")
|
||||
keys_all = get_keys(pssh)
|
||||
if not keys_all:
|
||||
print('License request failed, using keys from txt')
|
||||
keys_all = keys_file_txt
|
||||
if args.licenses_as_json:
|
||||
with open(keys_file, "a", encoding="utf8") as file:
|
||||
file.write(seriesName + "\n")
|
||||
for key in keys_all:
|
||||
with open(keys_file, "a", encoding="utf8") as file:
|
||||
file.write(key + "\n")
|
||||
print("Done!\n")
|
||||
|
||||
if not os.path.isfile(VideoOutputName):
|
||||
|
||||
print('Downloading video & audio')
|
||||
aria2c_input = ''
|
||||
if not args.novideo:
|
||||
if 'hvc1' in video_codec:
|
||||
inputVideo = seriesName + ' [' + str(CurrentHeigh) + 'p] [HEVC].mp4'
|
||||
if 'dvh1' in video_codec:
|
||||
inputVideo = seriesName + ' [' + str(CurrentHeigh) + 'p] [HDR].mp4'
|
||||
else:
|
||||
inputVideo = seriesName + ' [' + str(CurrentHeigh) + 'p].mp4'
|
||||
if os.path.isfile(inputVideo) and not os.path.isfile(inputVideo + '.aria2'):
|
||||
print('\n' + inputVideo + '\nFile has already been successfully downloaded previously.\n')
|
||||
else:
|
||||
url = urllib.parse.urljoin(base_url, video_list[(-1)]['File_URL'])
|
||||
aria2c_input += f'{url}\n'
|
||||
aria2c_input += f'\tdir={folderdownloader}\n'
|
||||
aria2c_input += f'\tout={inputVideo}\n'
|
||||
|
||||
#downloadFile(base_url + video_list[(-1)]['File_URL'], inputVideo)
|
||||
|
||||
if not args.noaudio:
|
||||
for x in audio_list:
|
||||
langAbbrev = x['Language']
|
||||
inputAudio = seriesName + ' ' + '(' + langAbbrev + ')' + '.mp4'
|
||||
inputAudio_ac3 = seriesName + ' ' + '(' + langAbbrev + ')' + '.ac3'
|
||||
inputAudio_eac3 = seriesName + ' ' + '(' + langAbbrev + ')' + '.eac3'
|
||||
inputAudio_m4a = seriesName + ' ' + '(' + langAbbrev + ')' + '.m4a'
|
||||
if os.path.isfile(inputAudio) and not os.path.isfile(inputAudio + '.aria2') or os.path.isfile(inputAudio_ac3) or os.path.isfile(inputAudio_m4a) or os.path.isfile(inputAudio_eac3):
|
||||
print('\n' + inputAudio + '\nFile has already been successfully downloaded previously.\n')
|
||||
else:
|
||||
url = urllib.parse.urljoin(base_url, x['File_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 = [
|
||||
HMAXConfig.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 = str(dict(z)['Language'])
|
||||
inputSubtitle = seriesName + " " + "(" + langAbbrev + ")"
|
||||
if os.path.isfile(inputSubtitle + ".xml") or os.path.isfile(inputSubtitle + ".srt"):
|
||||
print("\n" + inputSubtitle + "\nFile has already been successfully downloaded previously.\n")
|
||||
else:
|
||||
downloadFile2(str(dict(z)['URL']), inputSubtitle + ".xml")
|
||||
SubtitleEdit_process = subprocess.Popen([HMAXConfig.SUBTITLE_EDIT, "/convert", inputSubtitle + ".xml", "srt", "/fixcommonerrors", "/encoding:utf-8", "/RemoveLineBreaks"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).wait()
|
||||
for f in glob.glob(inputSubtitle + ".xml"):
|
||||
os.remove(f)
|
||||
print("Done!\n")
|
||||
else:
|
||||
print ("\nNo subtitles available.")
|
||||
|
||||
if not args.nochpaters:
|
||||
if chapters != []:
|
||||
print('\nGenerating chapters file...')
|
||||
if os.path.isfile(seriesName + ' Chapters.txt'):
|
||||
print(seriesName + " Chapters.txt" + " has already been successfully downloaded previously.")
|
||||
else:
|
||||
counter = 1
|
||||
with open(seriesName + ' Chapters.txt', 'a', encoding='utf-8') as f:
|
||||
for x in chapters:
|
||||
f.write("CHAPTER" + f'{counter:02}' + "=" + x["TIME"] + "\n" + "CHAPTER" + f'{counter:02}' + "NAME=" + x["TEXT"] + "\n")
|
||||
counter = counter + 1
|
||||
print('Done!\n')
|
||||
else:
|
||||
print("\nNo chapters available.")
|
||||
|
||||
#~NOTE: aqui faz de tudo! Extrai as keys, faz decrypt e muxa os arquivos
|
||||
|
||||
CorrectDecryptVideo = False
|
||||
if not args.novideo:
|
||||
if 'hvc1' in video_codec:
|
||||
inputVideo = seriesName + ' [' + str(CurrentHeigh) + 'p] [HEVC].mp4'
|
||||
if 'dvh1' in video_codec:
|
||||
inputVideo = seriesName + ' [' + str(CurrentHeigh) + 'p] [HDR].mp4'
|
||||
else:
|
||||
inputVideo = seriesName + ' [' + 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 = seriesName + ' ' + '(' + 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...')
|
||||
|
||||
if hbomaxType=="show":
|
||||
MKV_Muxer=Muxer(CurrentName=CurrentName,
|
||||
SeasonFolder=folderName,
|
||||
CurrentHeigh=CurrentHeigh,
|
||||
Type=hbomaxType,
|
||||
mkvmergeexe=HMAXConfig.MKVMERGE)
|
||||
|
||||
else:
|
||||
MKV_Muxer=Muxer(CurrentName=CurrentName,
|
||||
SeasonFolder=None,
|
||||
CurrentHeigh=CurrentHeigh,
|
||||
Type=hbomaxType,
|
||||
mkvmergeexe=HMAXConfig.MKVMERGE)
|
||||
|
||||
MKV_Muxer.mkvmerge_muxer(lang="English")
|
||||
|
||||
if args.tag:
|
||||
if 'hvc1' in video_codec:
|
||||
inputName = CurrentName + ' [' + CurrentHeigh + 'p] [HEVC].mkv'
|
||||
if 'dvh1' in video_codec:
|
||||
inputName = seriesName + ' [' + str(CurrentHeigh) + 'p] [HDR].mkv'
|
||||
else:
|
||||
inputName = CurrentName + ' [' + CurrentHeigh + 'p].mkv'
|
||||
|
||||
release_group(base_filename=inputName,
|
||||
default_filename=CurrentName,
|
||||
folder_name=folderName,
|
||||
type=hbomaxType,
|
||||
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("File '" + str(VideoOutputName) + "' already exists.")
|
||||
|
||||
def title_parse(x):
|
||||
m = re.match(r'https?://(play\.hbomax\.com/|(?:www\.)hbomax\.com/)(?:page|feature|series|episode)/(urn?:hbo?:(?:feature|series|page|episode):.+?$)', x)
|
||||
if m:
|
||||
if 'type' in m[2] and 'series' in m[2]:
|
||||
m = 'urn:hbo:series:{}'.format(m[2].split(':')[-3])
|
||||
elif 'type' in m[2] and 'feature' in m[2]:
|
||||
m = 'urn:hbo:feature:{}'.format(m[2].split(':')[-3])
|
||||
elif 'type' in m[2] and 'episode' in m[2]:
|
||||
m = 'urn:hbo:episode:{}'.format(m[2].split(':')[-3])
|
||||
else:
|
||||
m = m[2]
|
||||
return m
|
||||
|
||||
from pywidevine.decrypt.wvdecryptcustom import WvDecrypt
|
||||
from pywidevine.cdm import cdm, deviceconfig
|
||||
|
||||
def get_keys(pssh):
|
||||
device = deviceconfig.device_android_generic
|
||||
wvdecrypt = WvDecrypt(init_data_b64=bytes(pssh.encode()), cert_data_b64=None, device=device)
|
||||
|
||||
license_req = SESSION.post(url=license_wv, headers=refresh_token(), data=wvdecrypt.get_challenge()).content
|
||||
license_b64 = base64.b64encode(license_req)
|
||||
|
||||
wvdecrypt.update_license(license_b64)
|
||||
status, keys = wvdecrypt.start_process()
|
||||
return keys
|
||||
|
||||
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)
|
||||
|
||||
output_name = '{}.{}p.HMAX.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))
|
||||
|
||||
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([HMAXConfig.MP4DECRYPT, '--show-progress', '--key', key, inputAudio, outputAudioTemp])
|
||||
stdoutdata, stderrdata = wvdecrypt_process.communicate()
|
||||
wvdecrypt_process.wait()
|
||||
time.sleep(0.05)
|
||||
os.remove(inputAudio)
|
||||
print('\nDemuxing audio...')
|
||||
mediainfo = mediainfo_(outputAudioTemp)
|
||||
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'
|
||||
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=HMAXConfig.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([HMAXConfig.MP4DECRYPT, '--show-progress', '--key', key, inputVideo, outputVideoTemp])
|
||||
stdoutdata, stderrdata = wvdecrypt_process.communicate()
|
||||
wvdecrypt_process.wait()
|
||||
print('\nRemuxing video...')
|
||||
ff = ffmpy.FFmpeg(executable=HMAXConfig.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
|
||||
|
||||
global content_id
|
||||
content_id = title_parse(args.url_season)
|
||||
|
||||
if 'series' in args.url_season:
|
||||
if not args.season:
|
||||
args.season = 'all'
|
||||
get_season(content_id)
|
||||
elif 'feature' or 'episode':
|
||||
get_metadata(content_id=content_id)
|
||||
Reference in New Issue
Block a user