mirror of
https://github.com/widevinedump/DISNEY-4K-SCRIPT.git
synced 2024-03-24 20:20:46 +01:00
New
This commit is contained in:
parent
0bfa3f62e9
commit
4966f5355a
162 changed files with 250891 additions and 0 deletions
185
pydisney/disneyplus_api.py
Normal file
185
pydisney/disneyplus_api.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import uuid, requests, json
|
||||
|
||||
|
||||
class DSNP(object):
|
||||
|
||||
def __init__(self, DsnyID, Token, Type, Season=False, ishdr=False, isuhd=False, ishevc=False):
|
||||
|
||||
self.uhd = isuhd
|
||||
self.hdr = ishdr
|
||||
self.hevc = ishevc
|
||||
|
||||
self.isAmovie = True
|
||||
self.DsnyID = DsnyID
|
||||
self.contentTransactionId = uuid.uuid4()
|
||||
self.Token = Token
|
||||
|
||||
if Type == 'show':
|
||||
self.isAmovie = False
|
||||
self.Season = Season
|
||||
|
||||
self.api = {
|
||||
'DmcSeriesBundle': 'https://disney.content.edge.bamgrid.com/svc/content/DmcSeriesBundle/version/5.1/region/US/audience/false/maturity/1850/language/en/encodedSeriesId/{video_id}',
|
||||
|
||||
'DmcEpisodes': 'https://disney.content.edge.bamgrid.com/svc/content/DmcEpisodes/version/5.1/region/US/audience/false/maturity/1850/language/en/seasonId/{season_id}/pageSize/30/page/1',
|
||||
|
||||
'DmcVideo': 'https://disney.content.edge.bamgrid.com/svc/content/DmcVideoBundle/version/5.1/region/US/audience/false/maturity/1850/language/en/encodedFamilyId/{family_id}',
|
||||
|
||||
'LicenseServer': 'https://edge.bamgrid.com/widevine/v1/obtain-license',
|
||||
'manifest': 'https://us.edge.bamgrid.com/media/{mediaid}/scenarios/{scenarios}'
|
||||
}
|
||||
self.scenarios = {
|
||||
"default": "restricted-drm-ctr-sw",
|
||||
"default_hevc": "handset-drm-ctr-h265",
|
||||
"SD": "handset-drm-ctr",
|
||||
"HD": "tv-drm-ctr",
|
||||
"atmos": "tv-drm-ctr-h265-hdr10-atmos",
|
||||
"uhd_sdr": "tv-drm-ctr-h265-atmos",
|
||||
"uhd_hdr": "tv-drm-ctr-h265-hdr10-atmos",
|
||||
"uhd_dv": "tv-drm-ctr-h265-dovi-atmos",
|
||||
}
|
||||
|
||||
def load_info_m3u8(self, mediaId, mediaFormat, quality, isAtmos=False):
|
||||
|
||||
headers = {
|
||||
"accept": "application/vnd.media-service+json; version=2",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"x-bamsdk-platform": "windows",
|
||||
"x-bamsdk-version": '3.10',
|
||||
"Origin": 'https://www.disneyplus.com',
|
||||
"authorization": self.Token
|
||||
}
|
||||
if isAtmos:
|
||||
atmosurl = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['uhd_hdr'])
|
||||
resp = requests.get(atmosurl, headers=headers)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print('M3U8 - Error:' + str(resp.text))
|
||||
return False
|
||||
|
||||
data = json.loads(resp.text)
|
||||
atmos_url = data['stream']['complete']
|
||||
|
||||
if mediaFormat == 'SD':
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['SD'])
|
||||
|
||||
else:
|
||||
if self.uhd:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['uhd_sdr'])
|
||||
elif self.hdr:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['uhd_hdr'])
|
||||
|
||||
elif self.hevc:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['uhd_sdr'])
|
||||
|
||||
elif int(quality) == 720:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['default'])
|
||||
|
||||
elif int(quality) < 720:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['SD'])
|
||||
|
||||
elif int(quality) == 720 and self.hevc:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['default_hevc'])
|
||||
|
||||
else:
|
||||
url = self.api['manifest'].format(mediaid=mediaId, scenarios=self.scenarios['HD'])
|
||||
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print('M3U8 - Error:' + str(resp.text))
|
||||
return False
|
||||
|
||||
data = json.loads(resp.text)
|
||||
m3u8_url = data['stream']['complete']
|
||||
|
||||
if isAtmos:
|
||||
return m3u8_url, atmos_url
|
||||
|
||||
return m3u8_url, None
|
||||
|
||||
def load_playlist(self):
|
||||
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"x-bamsdk-platform": "windows",
|
||||
"x-bamsdk-version": '3.10',
|
||||
"authorization": f'Bearer {self.Token}'
|
||||
}
|
||||
|
||||
|
||||
if self.isAmovie:
|
||||
url = self.api['DmcVideo'].format(family_id=self.DsnyID)
|
||||
else:
|
||||
url = self.api['DmcSeriesBundle'].format(video_id=self.DsnyID)
|
||||
|
||||
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print('DATA - Error:' + str(resp.text))
|
||||
return False
|
||||
|
||||
data = resp.json()
|
||||
|
||||
if self.isAmovie:
|
||||
data_info = data['data']['DmcVideoBundle']['video']
|
||||
title = data_info['text']['title']['full']['program']['default']['content']
|
||||
description = data_info['text']['description']['medium']['program']['default']['content']
|
||||
js_data = {
|
||||
'Title': title,
|
||||
'Year': data_info['releases'][0]['releaseYear'],
|
||||
'Description': description,
|
||||
'mediaFormat': data_info['mediaMetadata']['format'],
|
||||
'id': {
|
||||
'contentId': data_info['contentId'],
|
||||
'mediaId': data_info['mediaMetadata']['mediaId']
|
||||
}
|
||||
}
|
||||
return js_data
|
||||
else:
|
||||
EpisodeList = []
|
||||
data_info = data['data']['DmcSeriesBundle']
|
||||
|
||||
SeasonTitle = data_info['series']['text']['title']['full']['series']['default']['content']
|
||||
for season in data_info['seasons']['seasons']:
|
||||
if int(season['seasonSequenceNumber']) == int(self.Season):
|
||||
SeascontentId = season['seasonId']
|
||||
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"x-bamsdk-platform": "windows",
|
||||
"x-bamsdk-version": '3.10',
|
||||
"authorization": f'Bearer {self.Token}'
|
||||
}
|
||||
|
||||
url = self.api['DmcEpisodes'].format(season_id=SeascontentId)
|
||||
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print('DATA - Error:' + str(resp.text))
|
||||
return False
|
||||
|
||||
JS = resp.json()
|
||||
|
||||
JS = JS['data']['DmcEpisodes']['videos']
|
||||
|
||||
for eps in JS:
|
||||
EpisodesDict = {'contentId': eps['contentId'],
|
||||
'mediaId': eps['mediaMetadata']['mediaId'],
|
||||
'seasonNumber': eps['seasonSequenceNumber'],
|
||||
'episodeNumber': eps['episodeSequenceNumber'],
|
||||
'Title': SeasonTitle,
|
||||
'mediaFormat': eps['mediaMetadata']['format']}
|
||||
|
||||
EpisodeList.append(EpisodesDict)
|
||||
|
||||
return EpisodeList
|
||||
|
||||
return
|
||||
162
pydisney/disneyplus_login.py
Normal file
162
pydisney/disneyplus_login.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import re
|
||||
import requests, json
|
||||
|
||||
class LOGIN(object):
|
||||
def __init__(self, email, password, proxies=False):
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.web_page = 'https://www.disneyplus.com/login'
|
||||
self.devices_url = "https://global.edge.bamgrid.com/devices"
|
||||
self.login_url = 'https://global.edge.bamgrid.com/idp/login'
|
||||
self.token_url = "https://global.edge.bamgrid.com/token"
|
||||
self.grant_url = 'https://global.edge.bamgrid.com/accounts/grant'
|
||||
self.SESSION = requests.Session()
|
||||
if proxies:
|
||||
self.SESSION.proxies.update(proxies)
|
||||
|
||||
def clientapikey(self):
|
||||
r = self.SESSION.get(self.web_page)
|
||||
match = re.search("window.server_path = ({.*});", r.text)
|
||||
janson = json.loads(match.group(1))
|
||||
clientapikey = janson["sdk"]["clientApiKey"]
|
||||
|
||||
return clientapikey
|
||||
|
||||
def assertion(self, client_apikey):
|
||||
|
||||
postdata = {
|
||||
"applicationRuntime": "firefox",
|
||||
"attributes": {},
|
||||
"deviceFamily": "browser",
|
||||
"deviceProfile": "macosx"
|
||||
}
|
||||
|
||||
header = {"authorization": "Bearer {}".format(client_apikey), "Origin": "https://www.disneyplus.com"}
|
||||
res = self.SESSION.post(url=self.devices_url, headers=header, json=postdata)
|
||||
|
||||
assertion = res.json()["assertion"]
|
||||
|
||||
return assertion
|
||||
|
||||
def access_token(self, client_apikey, assertion_):
|
||||
|
||||
header = {"authorization": "Bearer {}".format(client_apikey), "Origin": "https://www.disneyplus.com"}
|
||||
|
||||
postdata = {
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
"latitude": "0",
|
||||
"longitude": "0",
|
||||
"platform": "browser",
|
||||
"subject_token": assertion_,
|
||||
"subject_token_type": "urn:bamtech:params:oauth:token-type:device"
|
||||
}
|
||||
|
||||
res = self.SESSION.post(url=self.token_url, headers=header, data=postdata)
|
||||
|
||||
if res.status_code == 200:
|
||||
access_token = res.json()["access_token"]
|
||||
return access_token
|
||||
|
||||
if 'unreliable-location' in str(res.text):
|
||||
print('Make sure you use NL proxy/vpn, or your proxy/vpn is blacklisted.')
|
||||
exit()
|
||||
else:
|
||||
try:
|
||||
print('Error: ' + str(res.json()["errors"]['error_description']))
|
||||
exit()
|
||||
except Exception:
|
||||
print('Error: ' + str(res.text))
|
||||
exit()
|
||||
|
||||
return None
|
||||
|
||||
def login(self, access_token):
|
||||
headers = {
|
||||
'accept': 'application/json; charset=utf-8',
|
||||
'authorization': "Bearer {}".format(access_token),
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'Origin': 'https://www.disneyplus.com',
|
||||
'Referer': 'https://www.disneyplus.com/login/password',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
|
||||
'x-bamsdk-platform': 'windows',
|
||||
'x-bamsdk-version': '3.10',
|
||||
}
|
||||
|
||||
data = {'email': self.email, 'password': self.password}
|
||||
res = self.SESSION.post(url=self.login_url, data=json.dumps(data), headers=headers)
|
||||
if res.status_code == 200:
|
||||
id_token = res.json()["id_token"]
|
||||
return id_token
|
||||
|
||||
try:
|
||||
print('Error: ' + str(res.json()["errors"]))
|
||||
exit()
|
||||
except Exception:
|
||||
print('Error: ' + str(res.text))
|
||||
exit()
|
||||
|
||||
return None
|
||||
|
||||
def grant(self, id_token, access_token):
|
||||
|
||||
headers = {
|
||||
'accept': 'application/json; charset=utf-8',
|
||||
'authorization': "Bearer {}".format(access_token),
|
||||
'content-type': 'application/json; charset=UTF-8',
|
||||
'Origin': 'https://www.disneyplus.com',
|
||||
'Referer': 'https://www.disneyplus.com/login/password',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
|
||||
'x-bamsdk-platform': 'windows',
|
||||
'x-bamsdk-version': '3.10',
|
||||
}
|
||||
|
||||
data = {'id_token': id_token}
|
||||
|
||||
res = self.SESSION.post(url=self.grant_url, data=json.dumps(data), headers=headers)
|
||||
assertion = res.json()["assertion"]
|
||||
|
||||
return assertion
|
||||
|
||||
|
||||
def FinalToken(self, subject_token, client_apikey):
|
||||
|
||||
header = {"authorization": "Bearer {}".format(client_apikey), "Origin": "https://www.disneyplus.com"}
|
||||
|
||||
postdata = {
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
"latitude": "0",
|
||||
"longitude": "0",
|
||||
"platform": "browser",
|
||||
"subject_token": subject_token,
|
||||
"subject_token_type": "urn:bamtech:params:oauth:token-type:account"
|
||||
}
|
||||
|
||||
res = self.SESSION.post(url=self.token_url, headers=header, data=postdata)
|
||||
|
||||
if res.status_code == 200:
|
||||
access_token = res.json()["access_token"]
|
||||
expires_in = res.json()["expires_in"]
|
||||
return access_token, expires_in
|
||||
|
||||
try:
|
||||
print('Error: ' + str(res.json()["errors"]))
|
||||
exit()
|
||||
except Exception:
|
||||
print('Error: ' + str(res.text))
|
||||
exit()
|
||||
|
||||
return None, None
|
||||
|
||||
def GetAuthToken(self):
|
||||
|
||||
clientapikey_ = self.clientapikey()
|
||||
assertion_ = self.assertion(clientapikey_)
|
||||
access_token_ = self.access_token(clientapikey_, assertion_)
|
||||
id_token_ = self.login(access_token_)
|
||||
user_assertion = self.grant(id_token_, access_token_)
|
||||
TOKEN, EXPIRE = self.FinalToken(user_assertion, clientapikey_)
|
||||
|
||||
return TOKEN, EXPIRE
|
||||
|
||||
251
pydisney/disneyplus_muxer.py
Normal file
251
pydisney/disneyplus_muxer.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import os, subprocess, sys, contextlib
|
||||
|
||||
class Muxer(object):
|
||||
def __init__(self, CurrentName, SeasonFolder, CurrentHeigh, Type, defaults, mkvmergeexe):
|
||||
self.CurrentName = CurrentName
|
||||
self.SeasonFolder = SeasonFolder
|
||||
self.CurrentHeigh = CurrentHeigh
|
||||
self.Type = Type
|
||||
self.defaults = defaults
|
||||
self.mkvmergeexe = mkvmergeexe
|
||||
|
||||
def mux(self, command):
|
||||
newlines = ['\n', '\r\n', '\r']
|
||||
def unbuffered(proc, stream='stdout'):
|
||||
stream = getattr(proc, stream)
|
||||
with contextlib.closing(stream):
|
||||
while True:
|
||||
out = []
|
||||
last = stream.read(1)
|
||||
# Don't loop forever
|
||||
if last == '' and proc.poll() is not None:
|
||||
break
|
||||
while last not in newlines:
|
||||
# Don't loop forever
|
||||
if last == '' and proc.poll() is not None:
|
||||
break
|
||||
out.append(last)
|
||||
last = stream.read(1)
|
||||
out = ''.join(out)
|
||||
yield out
|
||||
|
||||
proc = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
)
|
||||
for line in unbuffered(proc):
|
||||
if 'Progress:' in line:
|
||||
sys.stdout.write("\r%s" % (line.replace('Progress:', 'Progress:')))
|
||||
sys.stdout.flush()
|
||||
print('')
|
||||
|
||||
return
|
||||
|
||||
def DPMuxer(self):
|
||||
|
||||
VideoInputNoExist = False
|
||||
|
||||
if os.path.isfile('.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p].h264'):
|
||||
VideoInputName = '.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p].h264'
|
||||
if self.Type == "show":
|
||||
VideoOutputName = '.\\'+self.SeasonFolder+'\\'+self.CurrentName + '.mkv'
|
||||
else:
|
||||
VideoOutputName = '.\\'+self.CurrentName + '.mkv'
|
||||
|
||||
elif os.path.isfile('.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p].h265'):
|
||||
VideoInputName = '.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p].h265'
|
||||
if self.Type == "show":
|
||||
VideoOutputName = '.\\'+self.SeasonFolder+'\\'+self.CurrentName + '.mkv'
|
||||
else:
|
||||
VideoOutputName = '.\\'+self.CurrentName + '.mkv'
|
||||
|
||||
elif os.path.isfile('.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].h265'):
|
||||
VideoInputName = '.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HEVC].h265'
|
||||
if self.Type == "show":
|
||||
VideoOutputName = '.\\'+self.SeasonFolder+'\\'+self.CurrentName + '.mkv'
|
||||
else:
|
||||
VideoOutputName = '.\\'+self.CurrentName + '.mkv'
|
||||
|
||||
elif os.path.isfile('.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].h265'):
|
||||
VideoInputName = '.\\'+self.CurrentName + ' [' + self.CurrentHeigh + 'p] [HDR].h265'
|
||||
if self.Type == "show":
|
||||
VideoOutputName = '.\\'+self.SeasonFolder+'\\'+self.CurrentName + '.mkv'
|
||||
else:
|
||||
VideoOutputName = '.\\'+self.CurrentName + '.mkv'
|
||||
else:
|
||||
VideoInputNoExist = True
|
||||
|
||||
if VideoInputNoExist == False:
|
||||
|
||||
AudioExtensionsList=[
|
||||
".ac3",
|
||||
".eac3",
|
||||
".m4a",
|
||||
".dts",
|
||||
".mp3",
|
||||
".aac",
|
||||
]
|
||||
|
||||
SubsExtensionsList= [
|
||||
".srt",
|
||||
".ass",
|
||||
]
|
||||
|
||||
|
||||
language_tag = "English"
|
||||
|
||||
if language_tag == "English":
|
||||
subs_forced = '[Forced]'
|
||||
subs_full = ''
|
||||
subs_sdh = '[SDH]'
|
||||
|
||||
LanguageList = [
|
||||
["English", "eng", "eng", "English", "yes", "no"],
|
||||
["Afrikaans", "af", "afr", "Afrikaans", "no", "no"],
|
||||
["Arabic", "ara", "ara", "Arabic", "no", "no"],
|
||||
["Arabic (Syria)", "araSy", "ara", "Arabic Syria", "no", "no"],
|
||||
["Arabic (Egypt)", "araEG", "ara", "Arabic Egypt", "no", "no"],
|
||||
["Arabic (Kuwait)", "araKW", "ara", "Arabic Kuwait", "no", "no"],
|
||||
["Arabic (Lebanon)", "araLB", "ara", "Arabic Lebanon", "no", "no"],
|
||||
["Arabic (Algeria)", "araDZ", "ara", "Arabic Algeria", "no", "no"],
|
||||
["Arabic (Bahrain)", "araBH", "ara", "Arabic Bahrain", "no", "no"],
|
||||
["Arabic (Iraq)", "araIQ", "ara", "Arabic Iraq", "no", "no"],
|
||||
["Arabic (Jordan)", "araJO", "ara", "Arabic Jordan", "no", "no"],
|
||||
["Arabic (Libya)", "araLY", "ara", "Arabic Libya", "no", "no"],
|
||||
["Arabic (Morocco)", "araMA", "ara", "Arabic Morocco", "no", "no"],
|
||||
["Arabic (Oman)", "araOM", "ara", "Arabic Oman", "no", "no"],
|
||||
["Arabic (Saudi Arabia)", "araSA", "ara", "Arabic Saudi Arabia", "no", "no"],
|
||||
["Arabic (Tunisia)", "araTN", "ara", "Arabic Tunisia", "no", "no"],
|
||||
["Arabic (United Arab Emirates)", "araAE", "ara", "Arabic United Arab Emirates", "no", "no"],
|
||||
["Arabic (Yemen)", "araYE", "ara", "Arabic Yemen", "no", "no"],
|
||||
["Armenian", "hye", "arm", "Armenian", "no", "no"],
|
||||
["Assamese", "asm", "asm", "Assamese", "no", "no"],
|
||||
["Bangla", "ben", "ben", "Bengali", "no", "no"],
|
||||
["Basque", "eus", "baq", "Basque", "no", "no"],
|
||||
["Bulgarian", "bul", "bul", "Bulgarian", "no", "no"],
|
||||
["Cantonese", "None", "chi", "Cantonese", "no", "no"],
|
||||
["Catalan", "cat", "cat", "Catalan", "no", "no"],
|
||||
["Simplified Chinese", "zhoS", "chi", "Chinese Simplified", "no", "no"],
|
||||
["Traditional Chinese", "zhoT", "chi", "Chinese Traditional", "no", "no"],
|
||||
["Croatian", "hrv", "hrv", "Croatian", "no", "no"],
|
||||
["Czech", "ces", "cze", "Czech", "no", "no"],
|
||||
["Danish", "dan", "dan", "Danish", "no", "no"],
|
||||
["Dutch", "nld", "dut", "Dutch", "no", "no"],
|
||||
["Estonian", "est", "est", "Estonian", "no", "no"],
|
||||
["Filipino", "fil", "fil", "Filipino", "no", "no"],
|
||||
["Finnish", "fin", "fin", "Finnish", "no", "no"],
|
||||
["Flemish", "nlBE", "dut", "Flemish", "no", "no"],
|
||||
["French", "fra", "fra", "French", "no", "no"],
|
||||
["French Canadian", "caFra", "fre", "French Canadian", "no", "no"],
|
||||
["German", "deu", "ger", "German", "no", "no"],
|
||||
["Greek", "ell", "gre", "Greek", "no", "no"],
|
||||
["Gujarati", "guj", "guj", "Gujarati", "no", "no"],
|
||||
["Hebrew", "heb", "heb", "Hebrew", "no", "no"],
|
||||
["Hindi", "hin", "hin", "Hindi", "no", "no"],
|
||||
["Hungarian", "hun", "hun", "Hungarian", "no", "no"],
|
||||
["Icelandic", "isl", "ice", "Icelandic", "no", "no"],
|
||||
["Indonesian", "ind", "ind", "Indonesian", "no", "no"],
|
||||
["Italian", "ita", "ita", "Italian", "no", "no"],
|
||||
["Japanese", "jpn", "jpn", "Japanese", "no", "no"],
|
||||
["Kannada (India)", "kan", "kan", "Kannada (India)", "no", "no"],
|
||||
["Khmer", "khm", "khm", "Khmer", "no", "no"],
|
||||
["Klingon", "tlh", "tlh", "Klingon", "no", "no"],
|
||||
["Korean", "kor", "kor", "Korean", "no", "no"],
|
||||
["Lithuanian", "lit", "lit", "Lithuanian", "no", "no"],
|
||||
["Latvian", "lav", "lav", "Latvian", "no", "no"],
|
||||
["Malay", "msa", "may", "Malay", "no", "no"],
|
||||
["Malayalam", "mal", "mal", "Malayalam", "no", "no"],
|
||||
["Mandarin", "None", "chi", "Mandarin", "no", "no"],
|
||||
["Mandarin Chinese (Simplified)", "zh-Hans", "chi", "Simplified", "no", "no"],
|
||||
["Mandarin Chinese (Traditional)", "zh-Hant", "chi", "Traditional", "no", "no"],
|
||||
["Yue Chinese", "yue", "chi", "(Yue Chinese)", "no", "no"],
|
||||
["Manipuri", "mni", "mni", "Manipuri", "no", "no"],
|
||||
["Marathi", "mar", "mar", "Marathi", "no", "no"],
|
||||
["No Dialogue", "zxx", "zxx", "No Dialogue", "no", "no"],
|
||||
["Norwegian", "nor", "nor", "Norwegian", "no", "no"],
|
||||
["Persian", "fas", "per", "Persian", "no", "no"],
|
||||
["Polish", "pol", "pol", "Polish", "no", "no"],
|
||||
["Portuguese", "por", "por", "Portuguese", "no", "no"],
|
||||
["Brazilian Portuguese", "brPor", "por", "Brazilian Portuguese", "no", "no"],
|
||||
["Punjabi", "pan", "pan", "Punjabi", "no", "no"],
|
||||
["Romanian", "ron", "rum", "Romanian", "no", "no"],
|
||||
["Russian", "rus", "rus", "Russian", "no", "no"],
|
||||
["Serbian", "srp", "srp", "Serbian", "no", "no"],
|
||||
["Sinhala", "sin", "sin", "Sinhala", "no", "no"],
|
||||
["Slovak", "slk", "slo", "Slovak", "no", "no"],
|
||||
["Slovenian", "slv", "slv", "Slovenian", "no", "no"],
|
||||
["Spanish", "spa", "spa", "Spanish", "no", "no"],
|
||||
["European Spanish", "euSpa", "spa", "European Spanish", "no", "no"],
|
||||
["Swedish", "swe", "swe", "Swedish", "no", "no"],
|
||||
["Tagalog", "tgl", "tgl", "Tagalog", "no", "no"],
|
||||
["Tamil", "tam", "tam", "Tamil", "no", "no"],
|
||||
["Telugu", "tel", "tel", "Telugu", "no", "no"],
|
||||
["Thai", "tha", "tha", "Thai", "no", "no"],
|
||||
["Turkish", "tur", "tur", "Turkish", "no", "no"],
|
||||
["Ukrainian", "ukr", "ukr", "Ukrainian", "no", "no"],
|
||||
["Urdu", "urd", "urd", "Urdu", "no", "no"],
|
||||
["Vietnamese", "vie", "vie", "Vietnamese", "no", "no"], ]
|
||||
|
||||
ALLAUDIOS = []
|
||||
for audio_language, subs_language, language_id, language_name, audio_default, subs_default in LanguageList:
|
||||
for AudioExtension in AudioExtensionsList:
|
||||
if os.path.isfile('.\\' + self.CurrentName + ' ' + language_id + AudioExtension):
|
||||
if language_id == self.defaults['audio']:
|
||||
ALLAUDIOS = ALLAUDIOS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name, '--default-track', '0:yes', '(', '.\\' + self.CurrentName + ' ' + language_id + AudioExtension, ')']
|
||||
else:
|
||||
ALLAUDIOS = ALLAUDIOS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name, '--default-track', '0:no', '(', '.\\' + self.CurrentName + ' ' + language_id + AudioExtension, ')']
|
||||
|
||||
OnlyOneLanguage = False
|
||||
if len(ALLAUDIOS) == 9:
|
||||
OnlyOneLanguage = True
|
||||
|
||||
elif len(ALLAUDIOS) == 18:
|
||||
if ALLAUDIOS[1] == ALLAUDIOS[10]:
|
||||
if ' - Audio Description' in ALLAUDIOS[7] or ' - Audio Description' in ALLAUDIOS[16]:
|
||||
OnlyOneLanguage = True
|
||||
else:
|
||||
OnlyOneLanguage = False
|
||||
|
||||
ALLSUBS = []
|
||||
for audio_language, subs_language, language_id, language_name, audio_default, subs_default in LanguageList:
|
||||
if subs_language == self.defaults['sub']:
|
||||
subs_default == 'yes'
|
||||
for SubsExtension in SubsExtensionsList:
|
||||
if os.path.isfile('.\\' + self.CurrentName + ' ' + 'forced-' + subs_language + SubsExtension):
|
||||
if subs_language == self.defaults['sub']:
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_forced, '--forced-track', '0:yes', '--default-track', '0:no', '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + 'forced-' + subs_language + SubsExtension, ')']
|
||||
else:
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_forced, '--forced-track', '0:yes', '--default-track', '0:' + subs_default, '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + 'forced-' + subs_language + SubsExtension, ')']
|
||||
if OnlyOneLanguage == True:
|
||||
pass
|
||||
if os.path.isfile('.\\' + self.CurrentName + ' ' + subs_language + SubsExtension):
|
||||
if subs_language == self.defaults['sub']:
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_full, '--forced-track', '0:no', '--default-track', '0:yes', '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + subs_language + SubsExtension, ')']
|
||||
else:
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_full, '--forced-track', '0:no', '--default-track', '0:' + subs_default, '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + subs_language + SubsExtension, ')']
|
||||
elif os.path.isfile('.\\' + self.CurrentName + ' ' + subs_language + SubsExtension):
|
||||
if subs_language == self.defaults['sub']:
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_full, '--forced-track', '0:no', '--default-track', '0:yes', '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + subs_language + SubsExtension, ')']
|
||||
else:
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_full, '--forced-track', '0:no', '--default-track', '0:no', '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + subs_language + SubsExtension, ')']
|
||||
if os.path.isfile('.\\' + self.CurrentName + ' ' + 'sdh-' + subs_language + SubsExtension):
|
||||
ALLSUBS = ALLSUBS + ['--language', '0:' + language_id, '--track-name', '0:' + language_name + ' ' + subs_sdh, '--forced-track', '0:no', '--default-track', '0:no', '--compression', '0:none', '(', '.\\' + self.CurrentName + ' ' + 'sdh-' + subs_language + SubsExtension, ')']
|
||||
|
||||
#MUX
|
||||
|
||||
mkvmerge_command_video = [self.mkvmergeexe,
|
||||
'--output',
|
||||
VideoOutputName,
|
||||
'--language',
|
||||
'0:und',
|
||||
'--default-track',
|
||||
'0:yes',
|
||||
'(',
|
||||
VideoInputName,
|
||||
')']
|
||||
|
||||
mkvmerge_command = mkvmerge_command_video + ALLAUDIOS + ALLSUBS
|
||||
self.mux(mkvmerge_command)
|
||||
231
pydisney/disneyplus_parser.py
Normal file
231
pydisney/disneyplus_parser.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import os, clipboard, uuid, requests, re, sys, pycountry
|
||||
from pydisney.m3u8_formater import M3U8
|
||||
|
||||
languageCodes = {
|
||||
"zh-Hans": "zhoS",
|
||||
"zh-Hant": "zhoT",
|
||||
"pt-BR": "brPor",
|
||||
"es-ES": "euSpa",
|
||||
"en-GB": "enGB",
|
||||
"en-PH": "enPH",
|
||||
"nl-BE": "nlBE",
|
||||
"fil": "enPH",
|
||||
"yue": "zhoS",
|
||||
'fr-CA':'caFra'
|
||||
}
|
||||
|
||||
class Parser(object):
|
||||
def __init__(self, m3u8, atmos_m3u8, is2ch=False):
|
||||
self.m3u8 = m3u8
|
||||
self.base = self.m3u8.rsplit('/', 1)[0] + '/'
|
||||
|
||||
if atmos_m3u8 is not None:
|
||||
self.isAtmos = True
|
||||
self.atmos_m3u8 = atmos_m3u8
|
||||
self.atmos_base = self.atmos_m3u8.rsplit('/', 1)[0] + '/'
|
||||
else:
|
||||
self.isAtmos=False
|
||||
|
||||
self.is2ch = is2ch
|
||||
|
||||
def countrycode(self, code):
|
||||
if code == 'cmn-Hans':
|
||||
return 'Mandarin Chinese (Simplified)', 'zh-Hans'
|
||||
elif code == 'cmn-Hant':
|
||||
return 'Mandarin Chinese (Traditional)', 'zh-Hant'
|
||||
elif code == 'es-419':
|
||||
return 'Spanish', 'spa'
|
||||
elif code == 'es-ES':
|
||||
return 'European Spanish', 'euSpa'
|
||||
elif code == 'pt-BR':
|
||||
return 'Brazilian Portuguese', 'brPor'
|
||||
elif code == 'pt-PT':
|
||||
return 'Portuguese', 'por'
|
||||
elif code == 'fr-CA':
|
||||
return 'French Canadian', 'caFra'
|
||||
elif code == 'fr-FR':
|
||||
return 'French', 'fra'
|
||||
|
||||
lang_code = code[:code.index('-')] if '-' in code else code
|
||||
lang = pycountry.languages.get(alpha_2=lang_code)
|
||||
if lang is None:
|
||||
lang = pycountry.languages.get(alpha_3=lang_code)
|
||||
|
||||
try:
|
||||
languagecode = languageCodes[code]
|
||||
except KeyError:
|
||||
languagecode = lang.alpha_3
|
||||
|
||||
return lang.name, languagecode
|
||||
|
||||
def getCodec(self, codecs):
|
||||
if ishevc or ishdr or ishdrdv:
|
||||
search = 'hvc'
|
||||
else:
|
||||
search = 'avc'
|
||||
l = []
|
||||
for c in codecs.split(','):
|
||||
if search in c:
|
||||
l.append(c)
|
||||
|
||||
return l[-1]
|
||||
|
||||
def Parser(self):
|
||||
AudioCodecs = None
|
||||
AudioExtension = None
|
||||
AudioList = []
|
||||
subtitleList = []
|
||||
forcedlist = []
|
||||
videoList = []
|
||||
added = set()
|
||||
|
||||
manifest_req = requests.get(self.m3u8)
|
||||
video_manifest = M3U8(manifest_req.text)
|
||||
|
||||
if self.isAtmos:
|
||||
atmos_req = requests.get(self.atmos_m3u8)
|
||||
try:
|
||||
audio_manifest = M3U8(atmos_req.text)
|
||||
audio_base = self.atmos_base
|
||||
audio_text = atmos_req.text
|
||||
except ValueError:
|
||||
audio_manifest = video_manifest
|
||||
audio_base = self.base
|
||||
audio_text = manifest_req.text
|
||||
else:
|
||||
audio_manifest = video_manifest
|
||||
audio_base = self.base
|
||||
audio_text = manifest_req.text
|
||||
|
||||
if self.isAtmos:
|
||||
if 'atmos' in str(audio_text):
|
||||
AudioCodecs = 'atmos'
|
||||
AudioExtension = '.eac3'
|
||||
else:
|
||||
print('this item has no atmos.')
|
||||
print('trying ac3 6ch...')
|
||||
if 'eac-3' in str(audio_text):
|
||||
AudioCodecs = 'eac-3'
|
||||
AudioExtension = '.eac3'
|
||||
else:
|
||||
print('this item has no ac3 6ch, trying aac 2ch')
|
||||
if 'aac-128k' in str(audio_text):
|
||||
AudioCodecs = 'aac-128k'
|
||||
AudioExtension = '.aac'
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
if self.is2ch:
|
||||
if 'aac-128k' in str(audio_text):
|
||||
AudioCodecs = 'aac-128k'
|
||||
AudioExtension = '.aac'
|
||||
else:
|
||||
print('this item has no aac 2ch')
|
||||
sys.exit(1)
|
||||
else:
|
||||
if 'eac-3' in str(audio_text):
|
||||
AudioCodecs = 'eac-3'
|
||||
AudioExtension = '.eac3'
|
||||
else:
|
||||
print('this item has no ac3 6ch, trying aac 2ch')
|
||||
if 'aac-128k' in str(audio_text):
|
||||
AudioCodecs = 'aac-128k'
|
||||
AudioExtension = '.aac'
|
||||
else:
|
||||
print('this item has no aac 2ch')
|
||||
sys.exit(1)
|
||||
|
||||
if AudioCodecs is None or AudioExtension is None:
|
||||
print('error while search for audio codec in m3u8 streams.')
|
||||
sys.exit(1)
|
||||
|
||||
video_streams = [x for x in video_manifest.master_playlist if x['TAG'] == 'EXT-X-STREAM-INF']
|
||||
audio_streams = [x for x in audio_manifest.master_playlist if x['TAG'] == 'EXT-X-MEDIA']
|
||||
subs_streams = [x for x in video_manifest.master_playlist if x['TAG'] == 'EXT-X-MEDIA']
|
||||
|
||||
for video in video_streams:
|
||||
if not video["URI"] in added:
|
||||
bitrate = 'None'
|
||||
if re.search('([0-9]*)k_', video["URI"]):
|
||||
bitrate = str(re.search('([0-9]*)k_', video["URI"])[1])
|
||||
else:
|
||||
if re.search('([0-9]*)_complete', video["URI"]):
|
||||
bitrate = str(re.search('([0-9]*)_complete', video["URI"])[1])
|
||||
|
||||
videoList.append(
|
||||
{
|
||||
'resolution': video["RESOLUTION"],
|
||||
'codec': str(video["CODECS"]),
|
||||
'bandwidth': str(video["BANDWIDTH"]),
|
||||
'bitrate': bitrate,
|
||||
'height': video["RESOLUTION"].rsplit('x', 1)[1],
|
||||
'url': self.base+video["URI"]
|
||||
}
|
||||
)
|
||||
added.add(video["URI"])
|
||||
|
||||
for m in audio_streams:
|
||||
if m['TYPE'] == 'AUDIO' and m['GROUP-ID'] == AudioCodecs and m.get('CHARACTERISTICS') is None:
|
||||
bitrate = 'None'
|
||||
if re.search('([0-9]*)k_', m["URI"]):
|
||||
bitrate = str(re.search('([0-9]*)k_', m["URI"])[1])
|
||||
else:
|
||||
if re.search('([0-9]*)_complete', m["URI"]):
|
||||
bitrate = str(re.search('([0-9]*)_complete', m["URI"])[1])
|
||||
|
||||
bitrate = '768' if str(m['CHANNELS']) == '16/JOC' and int(bitrate) > 768 else bitrate
|
||||
language, code = self.countrycode(m['LANGUAGE'])
|
||||
|
||||
Profile = m['GROUP-ID']
|
||||
Profile = "aac" if "aac" in m['GROUP-ID'].lower() else Profile
|
||||
Profile = "eac-3" if "eac-3" in m['GROUP-ID'].lower() else Profile
|
||||
Profile = "atmos" if "joc" in m['GROUP-ID'].lower() else Profile
|
||||
|
||||
AudioList.append(
|
||||
{
|
||||
'language': str(language),
|
||||
'code': str(code),
|
||||
'bitrate': bitrate,
|
||||
'codec': Profile,
|
||||
'channels': str(m['CHANNELS'].replace('"', "").replace("/JOC", "")),
|
||||
'url': audio_base+m['URI']
|
||||
}
|
||||
)
|
||||
|
||||
for m in subs_streams:
|
||||
if m['TYPE'] == 'SUBTITLES' and m['FORCED'] == 'NO':
|
||||
language, code = self.countrycode(m['LANGUAGE'])
|
||||
subtitleList.append(
|
||||
{
|
||||
'language': str(language),
|
||||
'code': str(code),
|
||||
'url': self.base+m['URI']
|
||||
}
|
||||
)
|
||||
|
||||
if m['TYPE'] == 'SUBTITLES' and m['FORCED'] == 'NO' and m['LANGUAGE'] == 'en':
|
||||
language, code = self.countrycode(m['LANGUAGE'])
|
||||
subtitleList.append(
|
||||
{
|
||||
'language': str(language),
|
||||
'code': 'sdh-' + str(code),
|
||||
'url': self.base+m['URI']
|
||||
}
|
||||
)
|
||||
|
||||
if m['TYPE'] == 'SUBTITLES' and m['FORCED'] == 'YES':
|
||||
language, code = self.countrycode(m['LANGUAGE'])
|
||||
forcedlist.append(
|
||||
{
|
||||
'language': str(language),
|
||||
'code': str(code),
|
||||
'url': self.base+m['URI']
|
||||
}
|
||||
)
|
||||
|
||||
videoList = sorted(videoList, key=lambda k: int(k['bandwidth']))
|
||||
print(videoList)
|
||||
|
||||
return videoList, AudioList, subtitleList, forcedlist, AudioExtension
|
||||
|
||||
472
pydisney/m3u8_formater.py
Normal file
472
pydisney/m3u8_formater.py
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
# ex:ts=4:sw=4:sts=4:et
|
||||
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
import binascii
|
||||
import copy
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from svtplay_dl.error import ServiceError
|
||||
from svtplay_dl.error import UIException
|
||||
from svtplay_dl.fetcher import VideoRetriever
|
||||
from svtplay_dl.subtitle import subtitle
|
||||
from svtplay_dl.utils.http import get_full_url
|
||||
from svtplay_dl.utils.output import ETA
|
||||
from svtplay_dl.utils.output import output
|
||||
from svtplay_dl.utils.output import progress_stream
|
||||
from svtplay_dl.utils.output import progressbar
|
||||
|
||||
|
||||
class HLSException(UIException):
|
||||
def __init__(self, url, message):
|
||||
self.url = url
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class LiveHLSException(HLSException):
|
||||
def __init__(self, url):
|
||||
super().__init__(url, "This is a live HLS stream, and they are not supported.")
|
||||
|
||||
|
||||
def hlsparse(config, res, url, **kwargs):
|
||||
streams = {}
|
||||
|
||||
if not res:
|
||||
return streams
|
||||
|
||||
if res.status_code > 400:
|
||||
streams[0] = ServiceError("Can't read HLS playlist. {}".format(res.status_code))
|
||||
return streams
|
||||
m3u8 = M3U8(res.text)
|
||||
|
||||
keycookie = kwargs.pop("keycookie", None)
|
||||
authorization = kwargs.pop("authorization", None)
|
||||
httpobject = kwargs.pop("httpobject", None)
|
||||
output = kwargs.pop("output", None)
|
||||
|
||||
media = {}
|
||||
subtitles = {}
|
||||
segments = None
|
||||
|
||||
if m3u8.master_playlist:
|
||||
for i in m3u8.master_playlist:
|
||||
audio_url = None
|
||||
if i["TAG"] == "EXT-X-MEDIA":
|
||||
if "AUTOSELECT" in i and (i["AUTOSELECT"].upper() == "YES"):
|
||||
if i["TYPE"] and i["TYPE"] != "SUBTITLES":
|
||||
if "URI" in i:
|
||||
if segments is None:
|
||||
segments = True
|
||||
if i["GROUP-ID"] not in media:
|
||||
media[i["GROUP-ID"]] = []
|
||||
media[i["GROUP-ID"]].append(i["URI"])
|
||||
else:
|
||||
segments = False
|
||||
if i["TYPE"] == "SUBTITLES":
|
||||
if "URI" in i:
|
||||
if i["GROUP-ID"] not in subtitles:
|
||||
subtitles[i["GROUP-ID"]] = []
|
||||
item = [i["URI"], i["LANGUAGE"]]
|
||||
if item not in subtitles[i["GROUP-ID"]]:
|
||||
subtitles[i["GROUP-ID"]].append(item)
|
||||
continue
|
||||
elif i["TAG"] == "EXT-X-STREAM-INF":
|
||||
bit_rate = float(i["BANDWIDTH"]) / 1000
|
||||
if "AUDIO" in i and (i["AUDIO"] in media):
|
||||
audio_url = get_full_url(media[i["AUDIO"]][0], url)
|
||||
urls = get_full_url(i["URI"], url)
|
||||
else:
|
||||
continue # Needs to be changed to utilise other tags.
|
||||
streams[int(bit_rate)] = HLS(
|
||||
copy.copy(config),
|
||||
urls,
|
||||
bit_rate,
|
||||
cookies=res.cookies,
|
||||
keycookie=keycookie,
|
||||
authorization=authorization,
|
||||
audio=audio_url,
|
||||
output=output,
|
||||
segments=bool(segments),
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
if subtitles and httpobject:
|
||||
for sub in list(subtitles.keys()):
|
||||
for n in subtitles[sub]:
|
||||
m3u8s = M3U8(httpobject.request("get", get_full_url(n[0], url), cookies=res.cookies).text)
|
||||
if "cmore" in url:
|
||||
subtype = "wrstsegment" # this have been seen in tv4play
|
||||
else:
|
||||
subtype = "wrst"
|
||||
streams[int(random.randint(1, 40))] = subtitle(
|
||||
copy.copy(config),
|
||||
subtype,
|
||||
get_full_url(m3u8s.media_segment[0]["URI"], url),
|
||||
subfix=n[1],
|
||||
output=copy.copy(output),
|
||||
m3u8=m3u8s,
|
||||
)
|
||||
|
||||
elif m3u8.media_segment:
|
||||
config.set("segments", False)
|
||||
streams[0] = HLS(
|
||||
copy.copy(config), url, 0, cookies=res.cookies, keycookie=keycookie, authorization=authorization, output=output, segments=False
|
||||
)
|
||||
|
||||
else:
|
||||
streams[0] = ServiceError("Can't find HLS playlist in m3u8 file.")
|
||||
|
||||
return streams
|
||||
|
||||
|
||||
class HLS(VideoRetriever):
|
||||
@property
|
||||
def name(self):
|
||||
return "hls"
|
||||
|
||||
def download(self):
|
||||
self.output_extention = "ts"
|
||||
if self.segments:
|
||||
if self.audio:
|
||||
self._download(self.audio, file_name=(copy.copy(self.output), "audio.ts"))
|
||||
self._download(self.url, file_name=(self.output, "ts"))
|
||||
|
||||
else:
|
||||
# Ignore audio
|
||||
self.audio = None
|
||||
self._download(self.url, file_name=(self.output, "ts"))
|
||||
|
||||
def _download(self, url, file_name):
|
||||
cookies = self.kwargs.get("cookies", None)
|
||||
start_time = time.time()
|
||||
m3u8 = M3U8(self.http.request("get", url, cookies=cookies).text)
|
||||
key = None
|
||||
|
||||
def random_iv():
|
||||
return os.urandom(16)
|
||||
|
||||
file_d = output(file_name[0], self.config, file_name[1])
|
||||
if file_d is None:
|
||||
return
|
||||
|
||||
hls_time_stamp = self.kwargs.pop("hls_time_stamp", False)
|
||||
decryptor = None
|
||||
size_media = len(m3u8.media_segment)
|
||||
eta = ETA(size_media)
|
||||
total_duration = 0
|
||||
duration = 0
|
||||
max_duration = 0
|
||||
for index, i in enumerate(m3u8.media_segment):
|
||||
if "duration" in i["EXTINF"]:
|
||||
duration = i["EXTINF"]["duration"]
|
||||
max_duration = max(max_duration, duration)
|
||||
total_duration += duration
|
||||
item = get_full_url(i["URI"], url)
|
||||
|
||||
if not self.config.get("silent"):
|
||||
if self.config.get("live"):
|
||||
progressbar(size_media, index + 1, "".join(["DU: ", str(timedelta(seconds=int(total_duration)))]))
|
||||
else:
|
||||
eta.increment()
|
||||
progressbar(size_media, index + 1, "".join(["ETA: ", str(eta)]))
|
||||
|
||||
data = self.http.request("get", item, cookies=cookies)
|
||||
if data.status_code == 404:
|
||||
break
|
||||
data = data.content
|
||||
if m3u8.encrypted:
|
||||
headers = {}
|
||||
if self.keycookie:
|
||||
keycookies = self.keycookie
|
||||
else:
|
||||
keycookies = cookies
|
||||
if self.authorization:
|
||||
headers["authorization"] = self.authorization
|
||||
|
||||
# Update key/decryptor
|
||||
if "EXT-X-KEY" in i:
|
||||
keyurl = get_full_url(i["EXT-X-KEY"]["URI"], url)
|
||||
if keyurl and keyurl[:4] == "skd:":
|
||||
raise HLSException(keyurl, "Can't decrypt beacuse of DRM")
|
||||
key = self.http.request("get", keyurl, cookies=keycookies, headers=headers).content
|
||||
iv = binascii.unhexlify(i["EXT-X-KEY"]["IV"][2:].zfill(32)) if "IV" in i["EXT-X-KEY"] else random_iv()
|
||||
backend = default_backend()
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
|
||||
decryptor = cipher.decryptor()
|
||||
|
||||
if decryptor:
|
||||
data = decryptor.update(data)
|
||||
else:
|
||||
raise ValueError("No decryptor found for encrypted hls steam.")
|
||||
|
||||
file_d.write(data)
|
||||
|
||||
if self.config.get("capture_time") > 0 and total_duration >= self.config.get("capture_time") * 60:
|
||||
break
|
||||
|
||||
if (size_media == (index + 1)) and self.config.get("live"):
|
||||
sleep_int = (start_time + max_duration * 2) - time.time()
|
||||
if sleep_int > 0:
|
||||
time.sleep(sleep_int)
|
||||
|
||||
size_media_old = size_media
|
||||
while size_media_old == size_media:
|
||||
start_time = time.time()
|
||||
|
||||
if hls_time_stamp:
|
||||
end_time_stamp = (datetime.utcnow() - timedelta(minutes=1, seconds=max_duration * 2)).replace(microsecond=0)
|
||||
start_time_stamp = end_time_stamp - timedelta(minutes=1)
|
||||
|
||||
base_url = url.split(".m3u8")[0]
|
||||
url = "{}.m3u8?in={}&out={}?".format(base_url, start_time_stamp.isoformat(), end_time_stamp.isoformat())
|
||||
|
||||
new_m3u8 = M3U8(self.http.request("get", url, cookies=cookies).text)
|
||||
for n_m3u in new_m3u8.media_segment:
|
||||
if not any(d["URI"] == n_m3u["URI"] for d in m3u8.media_segment):
|
||||
m3u8.media_segment.append(n_m3u)
|
||||
|
||||
size_media = len(m3u8.media_segment)
|
||||
|
||||
if size_media_old == size_media:
|
||||
time.sleep(max_duration)
|
||||
|
||||
file_d.close()
|
||||
if not self.config.get("silent"):
|
||||
progress_stream.write("\n")
|
||||
self.finished = True
|
||||
|
||||
|
||||
class M3U8:
|
||||
# Created for hls version <=7
|
||||
# https://tools.ietf.org/html/rfc8216
|
||||
|
||||
MEDIA_SEGMENT_TAGS = ("EXTINF", "EXT-X-BYTERANGE", "EXT-X-DISCONTINUITY", "EXT-X-KEY", "EXT-X-MAP", "EXT-X-PROGRAM-DATE-TIME", "EXT-X-DATERANGE")
|
||||
MEDIA_PLAYLIST_TAGS = (
|
||||
"EXT-X-TARGETDURATION",
|
||||
"EXT-X-MEDIA-SEQUENCE",
|
||||
"EXT-X-DISCONTINUITY-SEQUENCE",
|
||||
"EXT-X-ENDLIST",
|
||||
"EXT-X-PLAYLIST-TYPE",
|
||||
"EXT-X-I-FRAMES-ONLY",
|
||||
)
|
||||
MASTER_PLAYLIST_TAGS = ("EXT-X-MEDIA", "EXT-X-STREAM-INF", "EXT-X-I-FRAME-STREAM-INF", "EXT-X-SESSION-DATA", "EXT-X-SESSION-KEY")
|
||||
MEDIA_OR_MASTER_PLAYLIST_TAGS = ("EXT-X-INDEPENDENT-SEGMENTS", "EXT-X-START")
|
||||
|
||||
TAG_TYPES = {"MEDIA_SEGMENT": 0, "MEDIA_PLAYLIST": 1, "MASTER_PLAYLIST": 2}
|
||||
|
||||
def __init__(self, data):
|
||||
|
||||
self.version = None
|
||||
|
||||
self.media_segment = []
|
||||
self.media_playlist = {}
|
||||
self.master_playlist = []
|
||||
|
||||
self.encrypted = False
|
||||
self.independent_segments = False
|
||||
|
||||
self.parse_m3u(data)
|
||||
|
||||
def __str__(self):
|
||||
return "Version: {}\nMedia Segment: {}\nMedia Playlist: {}\nMaster Playlist: {}\nEncrypted: {}\tIndependent_segments: {}".format(
|
||||
self.version, self.media_segment, self.media_playlist, self.master_playlist, self.encrypted, self.independent_segments
|
||||
)
|
||||
|
||||
def parse_m3u(self, data):
|
||||
if not data.startswith("#EXTM3U"):
|
||||
raise ValueError("Does not appear to be an 'EXTM3U' file.")
|
||||
|
||||
data = data.replace("\r\n", "\n")
|
||||
lines = data.split("\n")[1:]
|
||||
|
||||
last_tag_type = None
|
||||
tag_type = None
|
||||
|
||||
media_segment_info = {}
|
||||
|
||||
for index, l in enumerate(lines):
|
||||
if not l:
|
||||
continue
|
||||
elif l.startswith("#EXT"):
|
||||
|
||||
info = {}
|
||||
tag, attr = _get_tag_attribute(l)
|
||||
if tag == "EXT-X-VERSION":
|
||||
self.version = int(attr)
|
||||
|
||||
# 4.3.2. Media Segment Tags
|
||||
elif tag in M3U8.MEDIA_SEGMENT_TAGS:
|
||||
|
||||
tag_type = M3U8.TAG_TYPES["MEDIA_SEGMENT"]
|
||||
# 4.3.2.1. EXTINF
|
||||
if tag == "EXTINF":
|
||||
if "," in attr:
|
||||
dur, title = attr.split(",", 1)
|
||||
else:
|
||||
dur = attr
|
||||
title = None
|
||||
info["duration"] = float(dur)
|
||||
info["title"] = title
|
||||
|
||||
# 4.3.2.2. EXT-X-BYTERANGE
|
||||
elif tag == "EXT-X-BYTERANGE":
|
||||
if "@" in attr:
|
||||
n, o = attr.split("@", 1)
|
||||
info["n"], info["o"] = (int(n), int(o))
|
||||
else:
|
||||
info["n"] = int(attr)
|
||||
info["o"] = 0
|
||||
|
||||
# 4.3.2.3. EXT-X-DISCONTINUITY
|
||||
elif tag == "EXT-X-DISCONTINUITY":
|
||||
pass
|
||||
|
||||
# 4.3.2.4. EXT-X-KEY
|
||||
elif tag == "EXT-X-KEY":
|
||||
self.encrypted = True
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
# 4.3.2.5. EXT-X-MAP
|
||||
elif tag == "EXT-X-MAP":
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
# 4.3.2.6. EXT-X-PROGRAM-DATE-TIME"
|
||||
elif tag == "EXT-X-PROGRAM-DATE-TIME":
|
||||
info = attr
|
||||
|
||||
# 4.3.2.7. EXT-X-DATERANGE
|
||||
elif tag == "EXT-X-DATERANGE":
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
media_segment_info[tag] = info
|
||||
|
||||
# 4.3.3. Media Playlist Tags
|
||||
elif tag in M3U8.MEDIA_PLAYLIST_TAGS:
|
||||
|
||||
tag_type = M3U8.TAG_TYPES["MEDIA_PLAYLIST"]
|
||||
# 4.3.3.1. EXT-X-TARGETDURATION
|
||||
if tag == "EXT-X-TARGETDURATION":
|
||||
info = int(attr)
|
||||
|
||||
# 4.3.3.2. EXT-X-MEDIA-SEQUENCE
|
||||
elif tag == "EXT-X-MEDIA-SEQUENCE":
|
||||
info = int(attr)
|
||||
|
||||
# 4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE
|
||||
elif tag == "EXT-X-DISCONTINUITY-SEQUENCE":
|
||||
info = int(attr)
|
||||
|
||||
# 4.3.3.4. EXT-X-ENDLIST
|
||||
elif tag == "EXT-X-ENDLIST":
|
||||
break
|
||||
|
||||
# 4.3.3.5. EXT-X-PLAYLIST-TYPE
|
||||
elif tag == "EXT-X-PLAYLIST-TYPE":
|
||||
info = attr
|
||||
|
||||
# 4.3.3.6. EXT-X-I-FRAMES-ONLY
|
||||
elif tag == "EXT-X-I-FRAMES-ONLY":
|
||||
pass
|
||||
|
||||
self.media_playlist[tag] = info
|
||||
|
||||
# 4.3.4. Master Playlist Tags
|
||||
elif tag in M3U8.MASTER_PLAYLIST_TAGS:
|
||||
|
||||
tag_type = M3U8.TAG_TYPES["MASTER_PLAYLIST"]
|
||||
# 4.3.4.1. EXT-X-MEDIA
|
||||
if tag == "EXT-X-MEDIA":
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
# 4.3.4.2. EXT-X-STREAM-INF
|
||||
elif tag == "EXT-X-STREAM-INF":
|
||||
info = _get_tuple_attribute(attr)
|
||||
if "BANDWIDTH" not in info:
|
||||
raise ValueError("Can't find 'BANDWIDTH' in 'EXT-X-STREAM-INF'")
|
||||
info["URI"] = lines[index + 1]
|
||||
|
||||
# 4.3.4.3. EXT-X-I-FRAME-STREAM-INF
|
||||
elif tag == "EXT-X-I-FRAME-STREAM-INF":
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
# 4.3.4.4. EXT-X-SESSION-DATA
|
||||
elif tag == "EXT-X-SESSION-DATA":
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
# 4.3.4.5. EXT-X-SESSION-KEY
|
||||
elif tag == "EXT-X-SESSION-KEY":
|
||||
self.encrypted = True
|
||||
info = _get_tuple_attribute(attr)
|
||||
info["TAG"] = tag
|
||||
|
||||
self.master_playlist.append(info)
|
||||
|
||||
# 4.3.5. Media or Master Playlist Tags
|
||||
elif tag in M3U8.MEDIA_OR_MASTER_PLAYLIST_TAGS:
|
||||
|
||||
tag_type = M3U8.TAG_TYPES["MEDIA_PLAYLIST"]
|
||||
# 4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS
|
||||
if tag == "EXT-X-INDEPENDENT-SEGMENTS":
|
||||
self.independent_segments = True
|
||||
|
||||
# 4.3.5.2. EXT-X-START
|
||||
elif tag == "EXT-X-START":
|
||||
info = _get_tuple_attribute(attr)
|
||||
|
||||
self.media_playlist[tag] = info
|
||||
|
||||
# Unused tags
|
||||
else:
|
||||
pass
|
||||
# This is a comment
|
||||
elif l.startswith("#"):
|
||||
pass
|
||||
# This must be a url/uri
|
||||
else:
|
||||
tag_type = None
|
||||
|
||||
if last_tag_type is M3U8.TAG_TYPES["MEDIA_SEGMENT"]:
|
||||
media_segment_info["URI"] = l
|
||||
self.media_segment.append(media_segment_info)
|
||||
media_segment_info = {}
|
||||
|
||||
last_tag_type = tag_type
|
||||
|
||||
if self.media_segment and self.master_playlist:
|
||||
raise ValueError("This 'M3U8' file contains data for both 'Media Segment' and 'Master Playlist'. This is not allowed.")
|
||||
|
||||
|
||||
def _get_tag_attribute(line):
|
||||
line = line[1:]
|
||||
try:
|
||||
search_line = re.search(r"^([A-Z\-]*):(.*)", line)
|
||||
return search_line.group(1), search_line.group(2)
|
||||
except Exception:
|
||||
return line, None
|
||||
|
||||
|
||||
def _get_tuple_attribute(attribute):
|
||||
attr_tuple = {}
|
||||
for art_l in re.split(""",(?=(?:[^'"]|'[^']*'|"[^"]*")*$)""", attribute):
|
||||
if art_l:
|
||||
name, value = art_l.split("=", 1)
|
||||
name = name.strip()
|
||||
# Checks for attribute name
|
||||
if not re.match(r"^[A-Z0-9\-]*$", name):
|
||||
raise ValueError("Not a valid attribute name.")
|
||||
|
||||
# Remove extra quotes of string
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
attr_tuple[name] = value
|
||||
|
||||
return attr_tuple
|
||||
80
pydisney/namehelper.py
Normal file
80
pydisney/namehelper.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import logging, os, re, subprocess, sys
|
||||
from pymediainfo import MediaInfo
|
||||
|
||||
__LOGGER__ = logging.getLogger(__name__)
|
||||
|
||||
def rename(file, source, group):
|
||||
|
||||
base_name = file
|
||||
name = os.path.splitext(os.path.basename(file))[0]
|
||||
directory_name = os.path.dirname(file)
|
||||
media_info = MediaInfo.parse(file)
|
||||
|
||||
for track in media_info.tracks:
|
||||
if track.track_type == 'Video':
|
||||
if int(track.width) == 1280 or int(track.height) == 720: resolution = '720p'
|
||||
elif int(track.width) == 1920 or int(track.height) == 1080: resolution = '1080p'
|
||||
else:
|
||||
if int(track.height) > 600:
|
||||
resolution = '{}p'.format(track.height)
|
||||
else:
|
||||
resolution = None
|
||||
|
||||
if track.format == "AVC":
|
||||
if track.encoding_settings: codec = "x264"
|
||||
else:codec = "H.264"
|
||||
elif track.format == "HEVC":
|
||||
if track.encoding_settings: codec = "x265"
|
||||
else:codec = "H.265"
|
||||
if 'Main 10@L5' in track.format_profile:
|
||||
hdr = True
|
||||
else:
|
||||
hdr = None
|
||||
|
||||
|
||||
try:
|
||||
track = [track for track in media_info.tracks if track.track_type == "Audio"][0]
|
||||
except IndexError:
|
||||
track = track
|
||||
if track.track_type == 'Audio':
|
||||
|
||||
if track.format == "E-AC-3":
|
||||
audioCodec = "DDP"
|
||||
elif track.format == "AC-3":
|
||||
audioCodec = "DD"
|
||||
elif track.format == "AAC":
|
||||
audioCodec = "AAC"
|
||||
elif track.format == "DTS":
|
||||
audioCodec = "DTS"
|
||||
elif "DTS" in track.format:
|
||||
audioCodec = "DTS"
|
||||
else:
|
||||
print("No Audio Root Found: {}".format(track.format))
|
||||
audioCodec = None
|
||||
|
||||
if track.channel_s == 6:
|
||||
if "Atmos" in track.commercial_name:
|
||||
channels = '5.1.Atmos'
|
||||
else:
|
||||
channels = "5.1"
|
||||
elif track.channel_s == 2: channels = "2.0"
|
||||
elif track.channel_s == 1: channels = "1.0"
|
||||
else:
|
||||
print("No Audio Channel Found: {}".format(track.channel_s))
|
||||
channels = None
|
||||
|
||||
name = name.replace(" ", ".").replace("'", "").replace(',', '')
|
||||
if hdr is not None:
|
||||
name = '{}.{}.{}.WEB-DL.HDR.{}{}.{}-{}'.format(
|
||||
name, resolution, source, audioCodec, channels, codec, group).replace('.-.', '.')
|
||||
else:
|
||||
if resolution is None:
|
||||
name = '{}.{}.WEB-DL.{}{}.{}-{}'.format(
|
||||
name, source, audioCodec, channels, codec, group).replace('.-.', '.')
|
||||
else:
|
||||
name = '{}.{}.{}.WEB-DL.{}{}.{}-{}'.format(
|
||||
name, resolution, source, audioCodec, channels, codec, group).replace('.-.', '.')
|
||||
name = re.sub(r'(\.\.)', '.', name)
|
||||
filename = '{}.mkv'.format(os.path.join(directory_name, name))
|
||||
if os.path.exists(filename): os.remove(filename)
|
||||
os.rename(base_name, filename)
|
||||
Loading…
Add table
Add a link
Reference in a new issue