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, []
|
||||
Reference in New Issue
Block a user