This commit is contained in:
widevinedump
2021-12-25 14:41:36 +05:30
parent 9692d1d509
commit ec8ddd995a
68 changed files with 13111 additions and 2 deletions

View 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

View 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

View 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)

View 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)

View 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, []