Main
This commit is contained in:
BIN
pywidevine/clients/blim/__pycache__/client.cpython-37.pyc
Normal file
BIN
pywidevine/clients/blim/__pycache__/client.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/blim/__pycache__/config.cpython-37.pyc
Normal file
BIN
pywidevine/clients/blim/__pycache__/config.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/blim/__pycache__/downloader.cpython-37.pyc
Normal file
BIN
pywidevine/clients/blim/__pycache__/downloader.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/blim/__pycache__/downloader_pr.cpython-37.pyc
Normal file
BIN
pywidevine/clients/blim/__pycache__/downloader_pr.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/blim/__pycache__/downloader_wv.cpython-37.pyc
Normal file
BIN
pywidevine/clients/blim/__pycache__/downloader_wv.cpython-37.pyc
Normal file
Binary file not shown.
Binary file not shown.
27
pywidevine/clients/blim/client.py
Normal file
27
pywidevine/clients/blim/client.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import json, sys, time
|
||||
import pywidevine.clients.blim.config as blim_cfg
|
||||
from os.path import join
|
||||
|
||||
BLIMLOGINDATA_FILE = join(blim_cfg.COOKIES_FOLDER, 'blim_login_data.json')
|
||||
|
||||
login_cfg = {
|
||||
'email': 'teste@blim.com',
|
||||
'password': 'teste1234'
|
||||
}
|
||||
|
||||
def login(SESSION, save_login=False):
|
||||
post_data = {"email": login_cfg['email'], "password": login_cfg['password'], "remember": True, "clientId":5}
|
||||
login_resp = SESSION.post(url=blim_cfg.ENDPOINTS['login'], json=post_data)
|
||||
if login_resp.json()['data'] == []:
|
||||
print(login_resp.json()['messages'][0]['value'])
|
||||
sys.exit(1)
|
||||
|
||||
costumer_key = login_resp.json()['data']['sessionId']
|
||||
access_key_secret = login_resp.json()['data']['accessToken']
|
||||
login_data = {'COSTUMER_KEY': costumer_key, 'SECRET_KEY': access_key_secret}
|
||||
if save_login:
|
||||
with open(BLIMLOGINDATA_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write(json.dumps(login_data, indent=4))
|
||||
f.close()
|
||||
|
||||
return SESSION, costumer_key, access_key_secret
|
||||
59
pywidevine/clients/blim/config.py
Normal file
59
pywidevine/clients/blim/config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from shutil import which
|
||||
from os.path import dirname, realpath, join
|
||||
from os import pathsep, environ
|
||||
|
||||
ENDPOINTS = {
|
||||
'login': 'https://api.blim.com/account/login',
|
||||
'seasons': 'https://api.blim.com/asset/',
|
||||
'content': 'https://api.blim.com/play/resume/',
|
||||
'config': 'https://www.blim.com/secure/play/resume/configuration?config_token=portal-config'
|
||||
}
|
||||
|
||||
protection_keys = {
|
||||
'094af042a17556c5b28a176deffdd4a7:14319c175eb145071fe189d2b1da8634',
|
||||
'4ae10c2357e250e088bb8a5ab044dd50:e7f47e2b948e9222cf4d24b51881ec04',
|
||||
'b6e16839eebd4ff6ab768d482d8d2b6a:ad6c675e0810741538f7f2f0b4099d9e'
|
||||
}
|
||||
|
||||
init_files = {
|
||||
'1080p': 'https://cdn.discordapp.com/attachments/686581369249333291/857062526856200252/video_init_1920x1080.bin',
|
||||
'480p': 'https://cdn.discordapp.com/attachments/686581369249333291/857062525421092944/video_640x480.bin',
|
||||
'audio': 'https://cdn.discordapp.com/attachments/686581369249333291/857104327742193735/audio_init.bin'
|
||||
}
|
||||
|
||||
SCRIPT_PATH = dirname(realpath('blimtv'))
|
||||
|
||||
BINARIES_FOLDER = join(SCRIPT_PATH, 'binaries')
|
||||
COOKIES_FOLDER = join(SCRIPT_PATH, 'cookies')
|
||||
|
||||
MP4DECRYPT_BINARY = 'mp4decrypt'
|
||||
MP4DUMP_BINARY = 'mp4dump'
|
||||
MKVMERGE_BINARY = 'mkvmerge'
|
||||
FFMPEG_BINARY = 'ffmpeg'
|
||||
ARIA2C_BINARY = 'aria2c'
|
||||
|
||||
# Add binaries folder to PATH as the first item
|
||||
environ['PATH'] = pathsep.join([BINARIES_FOLDER, environ['PATH']])
|
||||
|
||||
MP4DECRYPT = which(MP4DECRYPT_BINARY)
|
||||
MP4DUMP = which(MP4DUMP_BINARY)
|
||||
MKVMERGE = which(MKVMERGE_BINARY)
|
||||
FFMPEG = which(FFMPEG_BINARY)
|
||||
ARIA2C = which(ARIA2C_BINARY)
|
||||
|
||||
class PrDownloaderConfig(object):
|
||||
def __init__(self, ism, base_url, output_file, bitrate, init_url, file_type):
|
||||
self.ism = ism
|
||||
self.base_url = base_url
|
||||
self.output_file = output_file
|
||||
self.bitrate = bitrate
|
||||
self.init_url = init_url
|
||||
self.file_type = file_type
|
||||
|
||||
class WvDownloaderConfig(object):
|
||||
def __init__(self, mpd, base_url, output_file, format_id, file_type):
|
||||
self.mpd = mpd
|
||||
self.base_url = base_url
|
||||
self.output_file = output_file
|
||||
self.format_id = format_id
|
||||
self.file_type = file_type
|
||||
117
pywidevine/clients/blim/downloader_pr.py
Normal file
117
pywidevine/clients/blim/downloader_pr.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import threading, isodate
|
||||
import requests
|
||||
import math
|
||||
import urllib.parse
|
||||
|
||||
from requests.sessions import session
|
||||
from tqdm import tqdm
|
||||
from queue import Queue
|
||||
|
||||
dlthreads = 24
|
||||
|
||||
class PrDownloader(object):
|
||||
def __init__(self, config):
|
||||
self.ism = config.ism
|
||||
self.output_file = config.output_file
|
||||
self.bitrate = config.bitrate
|
||||
self.base_url = config.base_url
|
||||
self.init_url = config.init_url
|
||||
self.config = config
|
||||
|
||||
def process_url_templace(self, template, representation_id, bandwidth, time, number):
|
||||
if representation_id is not None: result = template.replace('$RepresentationID$', representation_id)
|
||||
if number is not None:
|
||||
nstart = result.find('$Number')
|
||||
if nstart >= 0:
|
||||
nend = result.find('$', nstart+1)
|
||||
if nend >= 0:
|
||||
var = result[nstart+1 : nend]
|
||||
if 'Number%' in var:
|
||||
value = var[6:] % (int(number))
|
||||
else:
|
||||
value = number
|
||||
result = result.replace('$'+var+'$', value)
|
||||
if bandwidth is not None: result = result.replace('$Bandwidth$', bandwidth)
|
||||
if time is not None: result = result.replace('$Time$', time)
|
||||
result = result.replace('$$', '$').replace('../', '')
|
||||
return result
|
||||
|
||||
def generate_segments(self):
|
||||
quality_level = self.get_quality_level()
|
||||
return self.get_segments(quality_level)
|
||||
|
||||
def get_segments(self, stream_index):
|
||||
urls = []
|
||||
urls.append(self.init_url)
|
||||
t = 0
|
||||
for seg in stream_index["c"]:
|
||||
if '@t' in seg:
|
||||
t = seg['@t']
|
||||
for i in range(int(seg.get('@r', 0)) + 1):
|
||||
path = stream_index['@Url'].format(**{
|
||||
'bitrate': self.bitrate,
|
||||
'start time': t})
|
||||
url = urllib.parse.urljoin(self.base_url, path)
|
||||
urls.append(url)
|
||||
t += int(seg['@d'])
|
||||
return urls
|
||||
|
||||
def get_quality_level(self):
|
||||
X = [item for (i, item) in enumerate(self.ism['SmoothStreamingMedia']['StreamIndex']) if self.config.file_type in item.get('@Type')][0]
|
||||
return X
|
||||
|
||||
def run(self):
|
||||
urls = self.generate_segments()
|
||||
work_q = Queue()
|
||||
result_q = Queue()
|
||||
|
||||
print('\n' + self.output_file)
|
||||
pool = [WorkerThread(work_q=work_q, result_q=result_q) for i in range(dlthreads)]
|
||||
for thread in pool:
|
||||
thread.start()
|
||||
|
||||
work_count = 0
|
||||
for seg_url in urls:
|
||||
work_q.put((work_count, seg_url))
|
||||
work_count += 1
|
||||
results = []
|
||||
|
||||
for _ in tqdm(range(work_count)):
|
||||
results.append(result_q.get())
|
||||
outfile = open(self.output_file , 'wb+')
|
||||
sortedr = sorted(results, key=lambda v: v[0])
|
||||
for r in sortedr:
|
||||
outfile.write(r[1])
|
||||
outfile.close()
|
||||
del results
|
||||
print('Done!')
|
||||
|
||||
class Downloader:
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
|
||||
def DownloadSegment(self, url):
|
||||
resp = self.session.get(url, stream=True)
|
||||
resp.raw.decode_content = True
|
||||
data = resp.raw.read()
|
||||
return data
|
||||
|
||||
class WorkerThread(threading.Thread):
|
||||
def __init__(self, work_q, result_q):
|
||||
super(WorkerThread, self).__init__()
|
||||
self.work_q = work_q
|
||||
self.result_q = result_q
|
||||
self.stoprequest = threading.Event()
|
||||
self.downloader = Downloader()
|
||||
|
||||
def run(self):
|
||||
while not self.stoprequest.isSet():
|
||||
try:
|
||||
(seq, url) = self.work_q.get(True, 0.05)
|
||||
self.result_q.put((seq, self.downloader.DownloadSegment(url)))
|
||||
except:
|
||||
continue
|
||||
|
||||
def join(self, timeout=None):
|
||||
self.stoprequest.set()
|
||||
super(WorkerThread, self).join(timeout)
|
||||
155
pywidevine/clients/blim/downloader_wv.py
Normal file
155
pywidevine/clients/blim/downloader_wv.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import threading, isodate
|
||||
import requests
|
||||
import math
|
||||
|
||||
from requests.sessions import session
|
||||
from tqdm import tqdm
|
||||
from queue import Queue
|
||||
|
||||
dlthreads = 24
|
||||
|
||||
class WvDownloader(object):
|
||||
def __init__(self, config):
|
||||
self.mpd = config.mpd
|
||||
self.output_file = config.output_file
|
||||
self.mimetype = config.file_type
|
||||
self.formatId = config.format_id
|
||||
self.config = config
|
||||
|
||||
def process_url_templace(self, template, representation_id, bandwidth, time, number):
|
||||
if representation_id is not None: result = template.replace('$RepresentationID$', representation_id)
|
||||
if number is not None:
|
||||
nstart = result.find('$Number')
|
||||
if nstart >= 0:
|
||||
nend = result.find('$', nstart+1)
|
||||
if nend >= 0:
|
||||
var = result[nstart+1 : nend]
|
||||
if 'Number%' in var:
|
||||
value = var[6:] % (int(number))
|
||||
else:
|
||||
value = number
|
||||
result = result.replace('$'+var+'$', value)
|
||||
if bandwidth is not None: result = result.replace('$Bandwidth$', bandwidth)
|
||||
if time is not None: result = result.replace('$Time$', time)
|
||||
result = result.replace('$$', '$').replace('../', '')
|
||||
return result
|
||||
|
||||
def generate_segments(self):
|
||||
segment_template = self.get_segment_template()
|
||||
return self.get_segments(segment_template)
|
||||
|
||||
def get_segments(self, segment_template):
|
||||
urls = []
|
||||
urls.append(self.config.base_url + segment_template['@initialization'].replace('$RepresentationID$', self.config.format_id))
|
||||
print(urls)
|
||||
try:
|
||||
current_number = int(segment_template.get("@startNumber", 0))
|
||||
period_duration = self.get_duration()
|
||||
segment_duration = int(segment_template["@duration"]) / int(segment_template["@timescale"])
|
||||
total_segments = math.ceil(period_duration / segment_duration)
|
||||
for _ in range(current_number, current_number + total_segments):
|
||||
urls.append(self.config.base_url + self.process_url_templace(segment_template['@media'],
|
||||
representation_id=self.config.format_id,
|
||||
bandwidth=None, time="0", number=str(current_number)))
|
||||
current_number += 1
|
||||
except KeyError:
|
||||
current_number = 0
|
||||
current_time = 0
|
||||
for seg in segment_template["SegmentTimeline"]["S"]:
|
||||
if '@t' in seg:
|
||||
current_time = seg['@t']
|
||||
for i in range(int(seg.get('@r', 0)) + 1):
|
||||
urls.append(self.config.base_url + self.process_url_templace(segment_template['@media'],
|
||||
representation_id=self.config.format_id,
|
||||
bandwidth=None, time=str(current_time), number=str(current_number)))
|
||||
current_number += 1
|
||||
current_time += seg['@d']
|
||||
return urls
|
||||
|
||||
def get_duration(self):
|
||||
media_duration = self.mpd["MPD"]["@mediaPresentationDuration"]
|
||||
return isodate.parse_duration(media_duration).total_seconds()
|
||||
|
||||
def get_segment_template(self):
|
||||
tracks = self.mpd['MPD']['Period']['AdaptationSet']
|
||||
|
||||
segment_template = []
|
||||
if self.mimetype == "video/mp4":
|
||||
for video_track in tracks:
|
||||
if video_track["@mimeType"] == self.mimetype:
|
||||
for v in video_track["Representation"]:
|
||||
segment_template = v["SegmentTemplate"]
|
||||
|
||||
if self.mimetype == "audio/mp4":
|
||||
for audio_track in tracks:
|
||||
if audio_track["@mimeType"] == self.mimetype:
|
||||
try:
|
||||
segment_template = audio_track["SegmentTemplate"]
|
||||
except (KeyError, TypeError):
|
||||
for a in self.list_representation(audio_track):
|
||||
segment_template = a["SegmentTemplate"]
|
||||
|
||||
return segment_template
|
||||
|
||||
def list_representation(self, x):
|
||||
if isinstance(x['Representation'], list):
|
||||
X = x['Representation']
|
||||
else:
|
||||
X = [x['Representation']]
|
||||
return X
|
||||
|
||||
def run(self):
|
||||
urls = self.generate_segments()
|
||||
work_q = Queue()
|
||||
result_q = Queue()
|
||||
|
||||
print('\n' + self.output_file)
|
||||
pool = [WorkerThread(work_q=work_q, result_q=result_q) for i in range(dlthreads)]
|
||||
for thread in pool:
|
||||
thread.start()
|
||||
|
||||
work_count = 0
|
||||
for seg_url in urls:
|
||||
work_q.put((work_count, seg_url))
|
||||
work_count += 1
|
||||
results = []
|
||||
|
||||
for _ in tqdm(range(work_count)):
|
||||
results.append(result_q.get())
|
||||
outfile = open(self.output_file , 'wb+')
|
||||
sortedr = sorted(results, key=lambda v: v[0])
|
||||
for r in sortedr:
|
||||
outfile.write(r[1])
|
||||
outfile.close()
|
||||
del results
|
||||
print('Done!')
|
||||
|
||||
class Downloader:
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
|
||||
def DownloadSegment(self, url):
|
||||
resp = self.session.get(url, stream=True)
|
||||
resp.raw.decode_content = True
|
||||
data = resp.raw.read()
|
||||
return data
|
||||
|
||||
class WorkerThread(threading.Thread):
|
||||
def __init__(self, work_q, result_q):
|
||||
super(WorkerThread, self).__init__()
|
||||
self.work_q = work_q
|
||||
self.result_q = result_q
|
||||
self.stoprequest = threading.Event()
|
||||
self.downloader = Downloader()
|
||||
|
||||
def run(self):
|
||||
while not self.stoprequest.isSet():
|
||||
try:
|
||||
(seq, url) = self.work_q.get(True, 0.05)
|
||||
self.result_q.put((seq, self.downloader.DownloadSegment(url)))
|
||||
except:
|
||||
continue
|
||||
|
||||
def join(self, timeout=None):
|
||||
self.stoprequest.set()
|
||||
super(WorkerThread, self).join(timeout)
|
||||
104
pywidevine/clients/blim/manifest_parse.py
Normal file
104
pywidevine/clients/blim/manifest_parse.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import isodate
|
||||
|
||||
def get_mpd_list(mpd):
|
||||
def get_height(width, height):
|
||||
if width == '1920':
|
||||
return '1080'
|
||||
elif width in ('1280', '1248'):
|
||||
return '720'
|
||||
else:
|
||||
return height
|
||||
|
||||
length = isodate.parse_duration(mpd['MPD']['@mediaPresentationDuration']).total_seconds()
|
||||
period = mpd['MPD']['Period']
|
||||
base_url = period['BaseURL']
|
||||
tracks = period['AdaptationSet']
|
||||
|
||||
video_list = []
|
||||
for video_tracks in tracks:
|
||||
if video_tracks['@mimeType'] == 'video/mp4':
|
||||
for x in video_tracks['Representation']:
|
||||
try:
|
||||
codecs = x['@codecs']
|
||||
except KeyError:
|
||||
codecs = video_tracks['@codecs']
|
||||
|
||||
videoDict = {
|
||||
'Height':get_height(x['@width'], x['@height']),
|
||||
'Width':x['@width'],
|
||||
'Bandwidth':x['@bandwidth'],
|
||||
'ID':x['@id'],
|
||||
'Codec':codecs}
|
||||
video_list.append(videoDict)
|
||||
|
||||
def list_representation(x):
|
||||
if isinstance(x['Representation'], list):
|
||||
X = x['Representation']
|
||||
else:
|
||||
X = [x['Representation']]
|
||||
return X
|
||||
|
||||
def replace_code_lang(x):
|
||||
X = x.replace('es', 'es-la').replace('en', 'es-la')
|
||||
return X
|
||||
|
||||
audio_list = []
|
||||
for audio_tracks in tracks:
|
||||
if audio_tracks['@mimeType'] == 'audio/mp4':
|
||||
for x in list_representation(audio_tracks):
|
||||
try:
|
||||
codecs = x['@codecs']
|
||||
except KeyError:
|
||||
codecs = audio_tracks['@codecs']
|
||||
audio_dict = {
|
||||
'Bandwidth':x['@bandwidth'],
|
||||
'ID':x['@id'],
|
||||
'Language':audio_tracks["@lang"],
|
||||
'Codec':codecs}
|
||||
audio_list.append(audio_dict)
|
||||
|
||||
subs_list = []
|
||||
for subs_tracks in tracks:
|
||||
if subs_tracks['@mimeType'] == 'text/vtt':
|
||||
for x in list_representation(subs_tracks):
|
||||
subs_dict = {
|
||||
'ID':x['@id'],
|
||||
'Language':replace_code_lang(subs_tracks["@lang"]),
|
||||
'Codec':subs_tracks['@mimeType'],
|
||||
'File_URL':base_url + x['BaseURL'].replace('../', '')}
|
||||
subs_list.append(subs_dict)
|
||||
|
||||
return length, video_list, audio_list, subs_list
|
||||
|
||||
def get_ism_list(ism):
|
||||
length = float(ism['SmoothStreamingMedia']['@Duration'][:-7])
|
||||
tracks = ism['SmoothStreamingMedia']["StreamIndex"]
|
||||
|
||||
video_list = []
|
||||
for video_tracks in tracks:
|
||||
if video_tracks['@Type'] == 'video':
|
||||
for x in video_tracks['QualityLevel']:
|
||||
videoDict = {
|
||||
'Height':x['@MaxHeight'],
|
||||
'Width':x['@MaxWidth'],
|
||||
'ID':'0',
|
||||
'Bandwidth':x['@Bitrate'],
|
||||
'Codec':x["@FourCC"]}
|
||||
video_list.append(videoDict)
|
||||
|
||||
def replace_code_lang(x):
|
||||
X = x.replace('255', 'es-la')
|
||||
return X
|
||||
|
||||
audio_list = []
|
||||
for audio_tracks in tracks:
|
||||
if audio_tracks['@Type'] == 'audio':
|
||||
for x in audio_tracks["QualityLevel"]:
|
||||
audio_dict = {
|
||||
'Bandwidth':x['@Bitrate'],
|
||||
'ID':'0',
|
||||
'Language':replace_code_lang(x["@AudioTag"]),
|
||||
'Codec':x["@FourCC"]}
|
||||
audio_list.append(audio_dict)
|
||||
|
||||
return length, video_list, audio_list, []
|
||||
42
pywidevine/clients/dictionary.py
Normal file
42
pywidevine/clients/dictionary.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import re
|
||||
from unidecode import unidecode
|
||||
|
||||
def get_release_tag(default_filename, vcodec, video_height, acodec, channels, bitrate, module, tag, isDual):
|
||||
video_codec = ''
|
||||
|
||||
if 'avc' in vcodec:
|
||||
video_codec = 'H.264'
|
||||
if 'hvc' in vcodec:
|
||||
video_codec = 'H.265'
|
||||
elif 'dvh' in vcodec:
|
||||
video_codec = 'HDR'
|
||||
|
||||
if isDual==False:
|
||||
audio_codec = ''
|
||||
if 'mp4a' in acodec:
|
||||
audio_codec = 'AAC'
|
||||
if acodec == 'ac-3':
|
||||
audio_codec = 'DD'
|
||||
if acodec == 'ec-3':
|
||||
audio_codec = 'DDP'
|
||||
elif acodec == 'ec-3' and bitrate > 700000:
|
||||
audio_codec = 'Atmos'
|
||||
|
||||
audio_channels = ''
|
||||
if channels == '2':
|
||||
audio_channels = '2.0'
|
||||
elif channels == '6':
|
||||
audio_channels = '5.1'
|
||||
audio_format = audio_codec + audio_channels
|
||||
else:
|
||||
audio_format = 'DUAL'
|
||||
|
||||
|
||||
default_filename = default_filename.replace('&', '.and.')
|
||||
default_filename = re.sub(r'[]!"#$%\'()*+,:;<=>?@\\^_`{|}~[-]', '', default_filename)
|
||||
default_filename = default_filename.replace(' ', '.')
|
||||
default_filename = re.sub(r'\.{2,}', '.', default_filename)
|
||||
default_filename = unidecode(default_filename)
|
||||
|
||||
output_name = '{}.{}p.{}.WEB-DL.{}.{}-{}'.format(default_filename, video_height, str(module), audio_format, video_codec, tag)
|
||||
return output_name
|
||||
BIN
pywidevine/clients/hbomax/__pycache__/authHelper.cpython-37.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/authHelper.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/hbomax/__pycache__/authHelper.cpython-38.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/authHelper.cpython-38.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/hbomax/__pycache__/client.cpython-37.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/client.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/hbomax/__pycache__/client.cpython-38.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/client.cpython-38.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/hbomax/__pycache__/client_.cpython-37.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/client_.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/hbomax/__pycache__/config.cpython-37.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/config.cpython-37.pyc
Normal file
Binary file not shown.
BIN
pywidevine/clients/hbomax/__pycache__/constants.cpython-37.pyc
Normal file
BIN
pywidevine/clients/hbomax/__pycache__/constants.cpython-37.pyc
Normal file
Binary file not shown.
106
pywidevine/clients/hbomax/client.py
Normal file
106
pywidevine/clients/hbomax/client.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import base64, time, requests, os, json
|
||||
import pywidevine.clients.hbomax.config as hmaxcfg
|
||||
from os.path import join
|
||||
|
||||
SESSION = requests.Session()
|
||||
HMAXTOKEN_FILE = join(hmaxcfg.COOKIES_FOLDER, 'hmax_login_data.json')
|
||||
|
||||
|
||||
login_config = {
|
||||
'username': 'rivas909@me.com',
|
||||
'password': 'NoCambieselPass.12345'
|
||||
}
|
||||
|
||||
def login(SESSION, login_endpoint, content_url, save_login=True):
|
||||
def get_free_token(token_url):
|
||||
token_data = hmaxcfg.get_token_info()
|
||||
free_token = requests.post(url=token_url, headers=token_data['headers'], json=token_data['data'])
|
||||
if int(free_token.status_code) != 200:
|
||||
print(free_token.json()['message'])
|
||||
exit(1)
|
||||
return free_token.json()['access_token']
|
||||
free_access_tk = get_free_token(login_endpoint)
|
||||
auth_data = hmaxcfg.get_auth_token_info(login_config)
|
||||
headers = auth_data['headers']
|
||||
headers['authorization'] = "Bearer {}".format(free_access_tk)
|
||||
auth_rep = SESSION.post(url=login_endpoint, headers=headers, json=auth_data['data'])
|
||||
if int(auth_rep.status_code) != 200:
|
||||
print(auth_rep.json()['message'])
|
||||
exit(1)
|
||||
|
||||
access_token_js = auth_rep.json()
|
||||
|
||||
login_grant_access = [
|
||||
{
|
||||
"id": "urn:hbo:privacy-settings:mined",
|
||||
"id": "urn:hbo:profiles:mined",
|
||||
"id": "urn:hbo:query:lastplayed",
|
||||
"id": "urn:hbo:user:me"}
|
||||
]
|
||||
user_grant_access = {
|
||||
"accept": "application/vnd.hbo.v9.full+json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"accept-language": hmaxcfg.metadata_language,
|
||||
"user-agent": hmaxcfg.UA,
|
||||
"x-hbo-client-version": "Hadron/50.40.0.111 desktop (DESKTOP)",
|
||||
"x-hbo-device-name": "desktop",
|
||||
"x-hbo-device-os-version": "undefined",
|
||||
"Authorization": f"Bearer {access_token_js['refresh_token']}"
|
||||
}
|
||||
user_grant_req = SESSION.post(content_url, json=login_grant_access, headers=user_grant_access)
|
||||
|
||||
if int(user_grant_req.status_code) != 207:
|
||||
print("failed to list profiles")
|
||||
|
||||
user_grant_js = user_grant_req.json()
|
||||
user_grant_id = ""
|
||||
|
||||
for profile in user_grant_js:
|
||||
if profile['id'] == "urn:hbo:profiles:mine":
|
||||
if len(profile['body']['profiles']) > 0:
|
||||
user_grant_id = profile['body']['profiles'][0]['profileId']
|
||||
else:
|
||||
print("no profiles found, create one on hbomax and try again")
|
||||
exit(1)
|
||||
|
||||
profile_headers = {
|
||||
"accept": "application/vnd.hbo.v9.full+json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"accept-language": hmaxcfg.metadata_language,
|
||||
"user-agent": hmaxcfg.UA,
|
||||
"x-hbo-client-version": "Hadron/50.40.0.111 desktop (DESKTOP)",
|
||||
"x-hbo-device-name": "desktop",
|
||||
"x-hbo-device-os-version": "undefined",
|
||||
"referer": "https://play.hbomax.com/profileSelect",
|
||||
"Authorization": f"Bearer {free_access_tk}" #~ free token
|
||||
}
|
||||
|
||||
user_profile = {
|
||||
"grant_type": "user_refresh_profile",
|
||||
"profile_id": user_grant_id,
|
||||
"refresh_token": f"{access_token_js['refresh_token']}",
|
||||
}
|
||||
|
||||
user_profile_req = SESSION.post(login_endpoint, json=user_profile, headers=profile_headers)
|
||||
|
||||
if int(user_profile_req.status_code) != 200:
|
||||
error_msg = "failed to obatin the final token"
|
||||
print(error_msg)
|
||||
|
||||
user_profile_js = user_profile_req.json()
|
||||
|
||||
refresh_token = user_profile_js['refresh_token']
|
||||
|
||||
login_data = {'ACCESS_TOKEN': refresh_token, 'EXPIRATION_TIME': int(time.time())}
|
||||
if save_login:
|
||||
with open(HMAXTOKEN_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write(json.dumps(login_data, indent=4))
|
||||
f.close()
|
||||
return auth_rep.json()['access_token']
|
||||
|
||||
|
||||
def get_video_payload(urn):
|
||||
headers = hmaxcfg.generate_payload()
|
||||
payload = []
|
||||
payload.append({"id":urn, "headers": headers['headers']})
|
||||
return payload
|
||||
125
pywidevine/clients/hbomax/config.py
Normal file
125
pywidevine/clients/hbomax/config.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import uuid, sys
|
||||
import configparser
|
||||
|
||||
from shutil import which
|
||||
from os.path import dirname, realpath, join
|
||||
from os import pathsep, environ
|
||||
|
||||
def generate_device():
|
||||
return str(uuid.uuid4())
|
||||
_uuid = generate_device() #traceid
|
||||
|
||||
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'
|
||||
config = {}
|
||||
|
||||
config['la'] = {
|
||||
'tokens': 'https://gateway-latam.api.hbo.com/auth/tokens',
|
||||
'content': 'https://comet-latam.api.hbo.com/content',
|
||||
'license_wv': 'https://comet-latam.api.hbo.com/drm/license/widevine?keygen=playready&drmKeyVersion=2'
|
||||
}
|
||||
|
||||
config['us'] = {
|
||||
'tokens': 'https://gateway.api.hbo.com/auth/tokens',
|
||||
'content': 'https://comet.api.hbo.com/content',
|
||||
'license_wv': 'https://comet.api.hbo.com/drm/license/widevine?keygen=playready&drmKeyVersion=2'
|
||||
}
|
||||
|
||||
metadata_language = 'en-US'
|
||||
|
||||
UA = 'Mozilla/5.0 (Linux; Android 7.1.1; SHIELD Android TV Build/LMY47D) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/84.0.4147.135 Safari/537.36'
|
||||
|
||||
login_headers = {
|
||||
"accept": "application/vnd.hbo.v9.full+json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"accept-language": metadata_language,
|
||||
"user-agent": UA,
|
||||
"x-hbo-client-version": "Hadron/50.40.0.111 desktop (DESKTOP)",
|
||||
"x-hbo-device-name": "desktop",
|
||||
"x-hbo-device-os-version": "undefined",
|
||||
}
|
||||
|
||||
login_json = {
|
||||
"client_id": '24fa5e36-3dc4-4ed0-b3f1-29909271b63d',
|
||||
"client_secret": '24fa5e36-3dc4-4ed0-b3f1-29909271b63d',
|
||||
"scope":"browse video_playback_free",
|
||||
"grant_type":"client_credentials",
|
||||
"deviceSerialNumber": 'b394a2da-b3a7-429d-8f70-5c4eae50a678',
|
||||
"clientDeviceData":{
|
||||
"paymentProviderCode":"apple"
|
||||
}
|
||||
}
|
||||
|
||||
payload = {
|
||||
'x-hbo-device-model':user_agent,
|
||||
'x-hbo-video-features':'server-stitched-playlist,mlp',
|
||||
'x-hbo-session-id':_uuid,
|
||||
'x-hbo-video-player-version':'QUANTUM_BROWSER/50.30.0.249',
|
||||
'x-hbo-device-code-override':'ANDROIDTV',
|
||||
'x-hbo-video-mlp':True,
|
||||
}
|
||||
|
||||
SCRIPT_PATH = dirname(realpath('hbomax'))
|
||||
|
||||
BINARIES_FOLDER = join(SCRIPT_PATH, 'binaries')
|
||||
COOKIES_FOLDER = join(SCRIPT_PATH, 'cookies')
|
||||
|
||||
MP4DECRYPT_BINARY = 'mp4decrypt'
|
||||
MEDIAINFO_BINARY = 'mediainfo'
|
||||
MP4DUMP_BINARY = 'mp4dump'
|
||||
MKVMERGE_BINARY = 'mkvmerge'
|
||||
FFMPEG_BINARY = 'ffmpeg'
|
||||
FFMPEG_BINARY = 'ffmpeg'
|
||||
ARIA2C_BINARY = 'aria2c'
|
||||
SUBTITLE_EDIT_BINARY = 'subtitleedit'
|
||||
|
||||
# Add binaries folder to PATH as the first item
|
||||
environ['PATH'] = pathsep.join([BINARIES_FOLDER, environ['PATH']])
|
||||
|
||||
MP4DECRYPT = which(MP4DECRYPT_BINARY)
|
||||
MEDIAINFO = which(MEDIAINFO_BINARY)
|
||||
MP4DUMP = which(MP4DUMP_BINARY)
|
||||
MKVMERGE = which(MKVMERGE_BINARY)
|
||||
FFMPEG = which(FFMPEG_BINARY)
|
||||
ARIA2C = which(ARIA2C_BINARY)
|
||||
SUBTITLE_EDIT = which(SUBTITLE_EDIT_BINARY)
|
||||
|
||||
def get_token_info():
|
||||
return {'headers': login_headers, 'data': login_json}
|
||||
|
||||
def get_user_headers():
|
||||
headers = {
|
||||
'origin': 'https://play.hbomax.com',
|
||||
'referer': 'https://play.hbomax.com/',
|
||||
'x-b3-traceid': f'{_uuid}-{_uuid}',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36',
|
||||
'accept': 'application/vnd.hbo.v9.full+json',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'x-hbo-client-version': 'Hadron/50.50.0.85 desktop (DESKTOP)',
|
||||
'x-hbo-device-name': 'desktop',
|
||||
'x-hbo-device-os-version': 'undefined'}
|
||||
return {'headers': headers}
|
||||
|
||||
def get_auth_token_info(cfg):
|
||||
data = {
|
||||
"grant_type": "user_name_password",
|
||||
"scope": "browse video_playback device elevated_account_management",
|
||||
"username": cfg['username'],
|
||||
"password": cfg['password'],
|
||||
}
|
||||
return {'headers': login_headers, 'data': data, 'device_id': _uuid}
|
||||
|
||||
def generate_payload():
|
||||
return {"headers": payload}
|
||||
|
||||
class HMAXRegion(object):
|
||||
def configHBOMaxLatam():
|
||||
tokens = config['la']['tokens']
|
||||
content = config['la']['content']
|
||||
license_wv = config['la']['license_wv']
|
||||
return tokens, content, license_wv
|
||||
|
||||
def configHBOMaxUS():
|
||||
tokens = config['us']['tokens']
|
||||
content = config['us']['content']
|
||||
license_wv = config['us']['license_wv']
|
||||
return tokens, content, license_wv
|
||||
Binary file not shown.
Binary file not shown.
30
pywidevine/clients/paramountplus/config.py
Normal file
30
pywidevine/clients/paramountplus/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from shutil import which
|
||||
from os.path import dirname, realpath, join
|
||||
from os import pathsep, environ
|
||||
|
||||
SCRIPT_PATH = dirname(realpath('paramountplus'))
|
||||
|
||||
BINARIES_FOLDER = join(SCRIPT_PATH, 'binaries')
|
||||
|
||||
MP4DECRYPT_BINARY = 'mp4decrypt'
|
||||
MP4DUMP_BINARY = 'mp4dump'
|
||||
MKVMERGE_BINARY = 'mkvmerge'
|
||||
FFMPEG_BINARY = 'ffmpeg'
|
||||
ARIA2C_BINARY = 'aria2c'
|
||||
|
||||
# Add binaries folder to PATH as the first item
|
||||
environ['PATH'] = pathsep.join([BINARIES_FOLDER, environ['PATH']])
|
||||
|
||||
MP4DECRYPT = which(MP4DECRYPT_BINARY)
|
||||
MP4DUMP = which(MP4DUMP_BINARY)
|
||||
MKVMERGE = which(MKVMERGE_BINARY)
|
||||
FFMPEG = which(FFMPEG_BINARY)
|
||||
ARIA2C = which(ARIA2C_BINARY)
|
||||
|
||||
class WvDownloaderConfig(object):
|
||||
def __init__(self, xml, base_url, output_file, track_id, format_id):
|
||||
self.xml = xml
|
||||
self.base_url = base_url
|
||||
self.output_file = output_file
|
||||
self.track_id = track_id
|
||||
self.format_id = format_id
|
||||
9
pywidevine/clients/paramountplus/config_orignal.py
Normal file
9
pywidevine/clients/paramountplus/config_orignal.py
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
class WvDownloaderConfig(object):
|
||||
def __init__(self, xml, base_url, output_file, track_id, format_id, file_type):
|
||||
self.xml = xml
|
||||
self.base_url = base_url
|
||||
self.output_file = output_file
|
||||
self.track_id = track_id
|
||||
self.format_id = format_id
|
||||
self.file_type = file_type
|
||||
116
pywidevine/clients/paramountplus/downloader.py
Normal file
116
pywidevine/clients/paramountplus/downloader.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import requests, pathlib
|
||||
import math, subprocess
|
||||
import os, sys, shutil
|
||||
|
||||
class WvDownloader(object):
|
||||
def __init__(self, config):
|
||||
self.xml = config.xml
|
||||
self.output_file = config.output_file
|
||||
self.config = config
|
||||
|
||||
def download_track(self, aria2c_infile, file_name):
|
||||
aria2c_opts = [
|
||||
'aria2c',
|
||||
'--enable-color=false',
|
||||
'--allow-overwrite=true',
|
||||
'--summary-interval=0',
|
||||
'--download-result=hide',
|
||||
'--async-dns=false',
|
||||
'--check-certificate=false',
|
||||
'--auto-file-renaming=false',
|
||||
'--file-allocation=none',
|
||||
'--console-log-level=warn',
|
||||
'-x16', '-s16', '-j16',
|
||||
'-i', aria2c_infile]
|
||||
subprocess.run(aria2c_opts, check=True)
|
||||
|
||||
source_files = pathlib.Path(temp_folder).rglob(r'./*.mp4')
|
||||
with open(file_name, mode='wb') as (destination):
|
||||
for file in source_files:
|
||||
with open(file, mode='rb') as (source):
|
||||
shutil.copyfileobj(source, destination)
|
||||
if os.path.exists(temp_folder):
|
||||
shutil.rmtree(temp_folder)
|
||||
os.remove(aria2c_infile)
|
||||
print('\nDone!')
|
||||
|
||||
def process_url_templace(self, template, representation_id, bandwidth, time, number):
|
||||
if representation_id is not None: result = template.replace('$RepresentationID$', representation_id)
|
||||
if number is not None:
|
||||
nstart = result.find('$Number')
|
||||
if nstart >= 0:
|
||||
nend = result.find('$', nstart+1)
|
||||
if nend >= 0:
|
||||
var = result[nstart+1 : nend]
|
||||
if 'Number%' in var:
|
||||
value = var[6:] % (int(number))
|
||||
else:
|
||||
value = number
|
||||
result = result.replace('$'+var+'$', value)
|
||||
if bandwidth is not None: result = result.replace('$Bandwidth$', bandwidth)
|
||||
if time is not None: result = result.replace('$Time$', time)
|
||||
result = result.replace('$$', '$').replace('../', '')
|
||||
return result
|
||||
|
||||
def generate_segments(self):
|
||||
segment_template = self.get_segment_template()
|
||||
return self.get_segments(segment_template)
|
||||
|
||||
def get_segments(self, segment_template):
|
||||
urls = []
|
||||
urls.append(self.config.base_url + segment_template['@initialization'].replace('$RepresentationID$', self.config.format_id))
|
||||
current_number = 1
|
||||
for seg in self.force_segmentimeline(segment_template):
|
||||
if '@t' in seg:
|
||||
current_time = seg['@t']
|
||||
for i in range(int(seg.get('@r', 0)) + 1):
|
||||
urls.append(self.config.base_url + self.process_url_templace(segment_template['@media'],
|
||||
representation_id=self.config.format_id,
|
||||
bandwidth=None, time=str(current_time), number=str(current_number)))
|
||||
current_number += 1
|
||||
current_time += seg['@d']
|
||||
return urls
|
||||
|
||||
def force_segmentimeline(self, segment_timeline):
|
||||
if isinstance(segment_timeline['SegmentTimeline']['S'], list):
|
||||
x16 = segment_timeline['SegmentTimeline']['S']
|
||||
else:
|
||||
x16 = [segment_timeline['SegmentTimeline']['S']]
|
||||
return x16
|
||||
|
||||
def force_instance(self, x):
|
||||
if isinstance(x['Representation'], list):
|
||||
X = x['Representation']
|
||||
else:
|
||||
X = [x['Representation']]
|
||||
return X
|
||||
|
||||
def get_segment_template(self):
|
||||
x = [item for (i, item) in enumerate(self.xml['MPD']['Period']['AdaptationSet']) if self.config.track_id == item["@id"]][0]
|
||||
segment_level = [item['SegmentTemplate'] for (i, item) in enumerate(self.force_instance(x)) if self.config.format_id == item["@id"]][0]
|
||||
return segment_level
|
||||
|
||||
def run(self):
|
||||
urls = self.generate_segments()
|
||||
|
||||
print('\n' + self.output_file)
|
||||
global temp_folder
|
||||
aria2c_infile = 'aria2c_infile.txt'
|
||||
if os.path.isfile(aria2c_infile):
|
||||
os.remove(aria2c_infile)
|
||||
temp_folder = self.output_file.replace('.mp4', '')
|
||||
if os.path.exists(temp_folder):
|
||||
shutil.rmtree(temp_folder)
|
||||
if not os.path.exists(temp_folder):
|
||||
os.makedirs(temp_folder)
|
||||
|
||||
if len(urls) > 1:
|
||||
num_segments = int(math.log10(len(urls))) + 1
|
||||
with open(aria2c_infile, 'a', encoding='utf8') as (file):
|
||||
for (i, url) in enumerate(urls):
|
||||
file.write(f'{url}\n')
|
||||
file.write(f'\tout={temp_folder}.{i:0{num_segments}d}.mp4\n')
|
||||
file.write(f'\tdir={temp_folder}\n')
|
||||
file.flush()
|
||||
self.download_track(aria2c_infile, self.output_file)
|
||||
print('Done!')
|
||||
110
pywidevine/clients/paramountplus/downloader_oringal.py
Normal file
110
pywidevine/clients/paramountplus/downloader_oringal.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import requests, pathlib
|
||||
import math, subprocess
|
||||
import os, sys, shutil
|
||||
|
||||
class WvDownloader(object):
|
||||
def __init__(self, config):
|
||||
self.xml = config.xml
|
||||
self.output_file = config.output_file
|
||||
self.config = config
|
||||
|
||||
def download_track(self, aria2c_infile, file_name):
|
||||
aria2c_opts = [
|
||||
'aria2c',
|
||||
'--allow-overwrite=true',
|
||||
'--download-result=hide',
|
||||
'--console-log-level=warn',
|
||||
'-x16', '-s16', '-j16',
|
||||
'-i', aria2c_infile]
|
||||
subprocess.run(aria2c_opts, check=True)
|
||||
|
||||
source_files = pathlib.Path(temp_folder).rglob(r'./*.mp4')
|
||||
with open(file_name, mode='wb') as (destination):
|
||||
for file in source_files:
|
||||
with open(file, mode='rb') as (source):
|
||||
shutil.copyfileobj(source, destination)
|
||||
if os.path.exists(temp_folder):
|
||||
shutil.rmtree(temp_folder)
|
||||
os.remove(aria2c_infile)
|
||||
print('\nDone!')
|
||||
|
||||
def process_url_templace(self, template, representation_id, bandwidth, time, number):
|
||||
if representation_id is not None: result = template.replace('$RepresentationID$', representation_id)
|
||||
if number is not None:
|
||||
nstart = result.find('$Number')
|
||||
if nstart >= 0:
|
||||
nend = result.find('$', nstart+1)
|
||||
if nend >= 0:
|
||||
var = result[nstart+1 : nend]
|
||||
if 'Number%' in var:
|
||||
value = var[6:] % (int(number))
|
||||
else:
|
||||
value = number
|
||||
result = result.replace('$'+var+'$', value)
|
||||
if bandwidth is not None: result = result.replace('$Bandwidth$', bandwidth)
|
||||
if time is not None: result = result.replace('$Time$', time)
|
||||
result = result.replace('$$', '$').replace('../', '')
|
||||
return result
|
||||
|
||||
def generate_segments(self):
|
||||
segment_template = self.get_segment_template()
|
||||
return self.get_segments(segment_template)
|
||||
|
||||
def get_segments(self, segment_template):
|
||||
urls = []
|
||||
urls.append(self.config.base_url + segment_template['@initialization'].replace('$RepresentationID$', self.config.format_id))
|
||||
current_number = 1
|
||||
for seg in self.force_segmentimeline(segment_template):
|
||||
if '@t' in seg:
|
||||
current_time = seg['@t']
|
||||
for i in range(int(seg.get('@r', 0)) + 1):
|
||||
urls.append(self.config.base_url + self.process_url_templace(segment_template['@media'],
|
||||
representation_id=self.config.format_id,
|
||||
bandwidth=None, time=str(current_time), number=str(current_number)))
|
||||
current_number += 1
|
||||
current_time += seg['@d']
|
||||
return urls
|
||||
|
||||
def force_segmentimeline(self, segment_timeline):
|
||||
if isinstance(segment_timeline['SegmentTimeline']['S'], list):
|
||||
x16 = segment_timeline['SegmentTimeline']['S']
|
||||
else:
|
||||
x16 = [segment_timeline['SegmentTimeline']['S']]
|
||||
return x16
|
||||
|
||||
def force_instance(self, x):
|
||||
if isinstance(x['Representation'], list):
|
||||
X = x['Representation']
|
||||
else:
|
||||
X = [x['Representation']]
|
||||
return X
|
||||
|
||||
def get_segment_template(self):
|
||||
x = [item for (i, item) in enumerate(self.xml['MPD']['Period']['AdaptationSet']) if self.config.track_id in item["@id"]][0]
|
||||
segment_level = [item['SegmentTemplate'] for (i, item) in enumerate(self.force_instance(x)) if self.config.format_id in item["@id"]][0]
|
||||
return segment_level
|
||||
|
||||
def run(self):
|
||||
urls = self.generate_segments()
|
||||
|
||||
print('\n' + self.output_file)
|
||||
global temp_folder
|
||||
aria2c_infile = 'aria2c_infile.txt'
|
||||
if os.path.isfile(aria2c_infile):
|
||||
os.remove(aria2c_infile)
|
||||
temp_folder = self.output_file.replace('.mp4', '')
|
||||
if os.path.exists(temp_folder):
|
||||
shutil.rmtree(temp_folder)
|
||||
if not os.path.exists(temp_folder):
|
||||
os.makedirs(temp_folder)
|
||||
|
||||
if len(urls) > 1:
|
||||
num_segments = int(math.log10(len(urls))) + 1
|
||||
with open(aria2c_infile, 'a', encoding='utf8') as (file):
|
||||
for (i, url) in enumerate(urls):
|
||||
file.write(f'{url}\n')
|
||||
file.write(f'\tout={temp_folder}.{i:0{num_segments}d}.mp4\n')
|
||||
file.write(f'\tdir={temp_folder}\n')
|
||||
file.flush()
|
||||
self.download_track(aria2c_infile, self.output_file)
|
||||
print('Done!')
|
||||
BIN
pywidevine/clients/paramountplus/paramountplus.rar
Normal file
BIN
pywidevine/clients/paramountplus/paramountplus.rar
Normal file
Binary file not shown.
15
pywidevine/clients/proxy_config.py
Normal file
15
pywidevine/clients/proxy_config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
config = {
|
||||
'proxies': {
|
||||
'none': None
|
||||
},
|
||||
}
|
||||
|
||||
class ProxyConfig(object):
|
||||
def __init__(self, proxies):
|
||||
self.config = config
|
||||
self.config['proxies'] = proxies
|
||||
|
||||
def get_proxy(self, proxy):
|
||||
return self.config['proxies'].get(proxy)
|
||||
|
||||
Reference in New Issue
Block a user