383 lines
16 KiB
Python
383 lines
16 KiB
Python
import requests
|
|
import time
|
|
import json
|
|
import base64
|
|
import m3u8
|
|
import os
|
|
from pywidevine.cdm import Cdm
|
|
from pywidevine.device import Device
|
|
from pywidevine.pssh import PSSH
|
|
from bs4 import BeautifulSoup
|
|
|
|
#Config
|
|
PATH_TO_WVD = './WVD.wvd'
|
|
|
|
def ascii_clear():
|
|
os.system('cls||clear')
|
|
print("""
|
|
^555555555557 J555555555? :GBGGGGGGGGGGGGGGGGGGGGB7 ^?PGGGGGGGGGGGGGGGGGPY7:
|
|
5@@@@@@@@@@@~ !@@@@@@@@@@7 5@@@@@@@@@@@@@@@@@@@@@@#: J&@@@@@@@@@@@@@@@@@@@@@@&?
|
|
!@@@@@@@@@@@J .B@@@@@@@@@P 7@@@@@@@@@@@@@@@@@@@@@@@7 !@@@@@@@@@@@#############&5
|
|
.G@@@@@@@@@@B. Y@@@@@@@@@&^ .YGGGGGGGGGGGGGGGGGGGGGGY .G@@@@@@@@@#~..............
|
|
?@@@@@@@@@@@~ ~@@@@@@@@@@Y ?@@@@@@@@@@7
|
|
:#@@@@@@@@@@Y .G@@@@@@@@@#: :!~~~~~~~~~~~~~~~~~~~~~!. :#@@@@@@@@@G
|
|
Y@@@@@@@@@@#: J@@@@@@@@@@? G@@@@@@@@@@@@@@@@@@@@@@B. J@@@@@@@@@&~
|
|
^&@@@@@@@@@@! ^&@@@@@@@@@B. ?@@@@@@@@@@@@@@@@@@@@@@&~ ^&@@@@@@@@@Y
|
|
P@@@@@@@@@@P P@@@@@@@@@@! :#@@@@@@@@@@@@@@@@@@@@@@J 5@@@@@@@@@#:
|
|
!@@@@@@@@@@#: ?@@@@@@@@@@P 5@@@@@@@@@@P^^^^^^^^^^^^ ~@@@@@@@@@@?
|
|
.B@@@@@@@@@@5 !&@@@@@@@@@&^ ~@@@@@@@@@@&^ G@@@@@@@@@G
|
|
?@@@@@@@@@@@&PY5B@@@@@@@@@@@Y .B@@@@@@@@@@J 7@@@@@@@@@@#5YYYYYYYYYYY55^
|
|
:#@@@@@@@@@@@@@@@@@@@@@@@@@@#: ?@@@@@@@@@@#. .B@@@@@@@@@@@@@@@@@@@@@@@@B.
|
|
:#@@@@@@@@@@@@@@@@@@@@@@@@@B~ ^&@@@@@@@@@@7 :&@@@@@@@@@@@@@@@@@@@@@@@#~
|
|
^YGB##################BG57. Y##########P ~5B###################P?:
|
|
.................... ........... ...................
|
|
|
|
Stream extractor
|
|
TAJLN 2023
|
|
""")
|
|
|
|
def do_cdm(pssh, license_url, licence_token, key_id):
|
|
pssh = PSSH(pssh)
|
|
|
|
device = Device.load(PATH_TO_WVD)
|
|
cdm = Cdm.from_device(device)
|
|
session_id = cdm.open()
|
|
challenge = cdm.get_license_challenge(session_id, pssh)
|
|
|
|
drm_info_json = {
|
|
"system":"com.widevine.alpha",
|
|
"key_ids":[key_id]
|
|
}
|
|
|
|
headers = {
|
|
'Authorization': 'Bearer ' + licence_token,
|
|
'X-DRM-INFO': base64.b64encode(json.dumps(drm_info_json).encode()).decode(),
|
|
'Content-Type': 'application/octet-stream',
|
|
'User-Agent': 'Dice Shield/12.11.0 (Linux;Android 9) ExoPlayerLib/2.18.4',
|
|
}
|
|
|
|
licence = requests.post(license_url, headers=headers, data=challenge)
|
|
licence.raise_for_status()
|
|
|
|
cdm.parse_license(session_id, licence.content)
|
|
|
|
keys = cdm.get_keys(session_id)
|
|
|
|
cdm.close(session_id)
|
|
|
|
for key in keys:
|
|
if key.type != 'SIGNING':
|
|
return f"{key.kid.hex}:{key.key.hex()}"
|
|
|
|
def find_wv_pssh_offset(raw: bytes) -> str:
|
|
offset = raw.rfind(b'pssh')
|
|
return raw[offset - 4:offset - 4 + raw[offset - 1]]
|
|
|
|
|
|
def to_pssh(content: bytes) -> str:
|
|
wv_offset = find_wv_pssh_offset(content)
|
|
return base64.b64encode(wv_offset).decode()
|
|
|
|
def current_milli_time():
|
|
return round(time.time() * 1000)
|
|
|
|
def refresh_token(token):
|
|
url = 'https://dce-frontoffice.imggaming.com/api/v2/token/refresh'
|
|
|
|
headers = {
|
|
'accept': 'application/json, text/plain, */*',
|
|
'x-api-key': '4dc1e8df-5869-41ea-95c2-6f04c67459ed',
|
|
'app': 'dice',
|
|
'accept-language': 'en-US',
|
|
'realm': 'dce.ufc',
|
|
'authorization': 'Bearer ' + token,
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': '525',
|
|
'Host': 'dce-frontoffice.imggaming.com',
|
|
'Connection': 'Keep-Alive',
|
|
'Accept-Encoding': 'gzip',
|
|
'User-Agent': 'okhttp/3.11.0',
|
|
}
|
|
|
|
post_data = {
|
|
"refreshToken": "eyJhbGciOiJSUzI1NiIsInB1ciI6IlJFRiIsInNpZyI6ImciLCJ0eXAiOiJKV1QiLCJ2IjozfQ.eyJhcCI6eyJhcHQiOiJJRCJ9LCJhcHIiOiJJRCIsImF1ZCI6ImRjZS51ZmMiLCJkZXYiOiJBTkRST0lEX1BIT05FIiwiZW52IjoicHJvZCIsImV4cCI6MTY5OTMyNDQ1OCwiZ3VlIjpmYWxzZSwiaWF0IjoxNjkwNjg0NDU4LCJpc3MiOiJkY2UtaWQiLCJzdWIiOiJhWHZLclp8MjQ2YWRiNGEtOGI4YS00NWYwLWI2NGYtYjg1ODgxODUzYjNmIn0.DHydVhuWBNdIvfubRG9XujU8IDycfGo3GRdsVpyNArR-2t-WzKKrbD4K-EtYWB6kSAMGtEnfZ8-uYUSxTmBIM0vK3MV8_le5zd5jLCzMoWM89CkwaGWHLkjSMnlrxYagzPeWhLtYZplg7rxygYCh6uX7VG76WVAohmUqaLOZPro"
|
|
}
|
|
|
|
post_data = json.dumps(post_data)
|
|
|
|
response = requests.post(url, headers=headers, data=post_data)
|
|
if response.status_code == 201:
|
|
print('Token refresh successful')
|
|
data = json.loads(response.content)
|
|
return data['authorisationToken']
|
|
else:
|
|
print('Token refresh failed')
|
|
print(response.content)
|
|
quit()
|
|
|
|
def choose_event(token):
|
|
url = 'https://dce-frontoffice.imggaming.com/api/v2/event/live'
|
|
|
|
headers = {
|
|
'accept': 'application/json, text/plain, */*',
|
|
'content-type': 'application/json',
|
|
'x-api-key': '4dc1e8df-5869-41ea-95c2-6f04c67459ed',
|
|
'app': 'dice',
|
|
'accept-language': 'en-US',
|
|
'realm': 'dce.ufc',
|
|
'authorization': 'Bearer ' + token,
|
|
'Host': 'dce-frontoffice.imggaming.com',
|
|
'Connection': 'Keep-Alive',
|
|
'Accept-Encoding': 'gzip',
|
|
'User-Agent': 'okhttp/3.11.0',
|
|
}
|
|
|
|
response = requests.get(url, headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
print('Event list request successful')
|
|
|
|
data = json.loads(response.content)
|
|
|
|
events = data['events']
|
|
|
|
ascii_clear()
|
|
|
|
print('Found ' + str(len(events)) + ' currently active events:')
|
|
|
|
i = 1
|
|
for e in events:
|
|
print(str(i) + '. ' + e['title'])
|
|
i+=1
|
|
|
|
choose = int(input("\nChoose event (To search again type 0): "))
|
|
|
|
if choose == 0 or choose > len(events):
|
|
return choose_event(token)
|
|
else:
|
|
ascii_clear()
|
|
|
|
event_id = str(events[choose-1]['id'])
|
|
event_name = events[choose-1]['title']
|
|
|
|
if 'androidIAPCodes' in json.dumps(events[choose-1]):
|
|
androidIAPCodes = events[choose-1]['availablePurchases'][0]['androidIAPCodes'][0]
|
|
else:
|
|
androidIAPCodes = None
|
|
|
|
return [event_id, event_name, androidIAPCodes]
|
|
|
|
else:
|
|
print('Event list request failed')
|
|
print(response.content)
|
|
quit()
|
|
|
|
|
|
|
|
def spoof_buy(token, androidIAPCodes):
|
|
|
|
url = 'https://dce-frontoffice.imggaming.com/api/v2/googlePlay/receipt'
|
|
|
|
headers = {
|
|
'accept': 'application/json, text/plain, */*',
|
|
'x-api-key': '4dc1e8df-5869-41ea-95c2-6f04c67459ed',
|
|
'app': 'dice',
|
|
'accept-language': 'en-US',
|
|
'realm': 'dce.ufc',
|
|
'authorization': 'Bearer ' + token,
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': '1802',
|
|
'Host': 'dce-frontoffice.imggaming.com',
|
|
'Connection': 'Keep-Alive',
|
|
'Accept-Encoding': 'gzip',
|
|
'User-Agent': 'okhttp/3.11.0'
|
|
}
|
|
|
|
order_id = '9990823415274633304.5480098017970283'
|
|
purchase_token = 'bfokzzrsaqtxcmwklgnxwztz.AO-J1OsFfDYXhRrvKGFBqjOUCiamHLDdHEetbxCVAoBNpOD-mTIQtoB_lLPEinJmPUMurynpiPutfRNNgHlcekcnGpWZiFsSGRYTHOveFjrKRgeLKDYcevlhZbWnRXOLhOGMjKztehMT'
|
|
purchase_time = str(current_milli_time())
|
|
|
|
reciept_string = '{"orderId":"' + order_id + '","packageName":"com.neulion.smartphone.ufc.android","productId":"' + androidIAPCodes + '","purchaseTime":' + purchase_time + ',"purchaseState":0,"purchaseToken":"' + purchase_token + '","developerPayload":"subs:' + androidIAPCodes + ':{\\"purchaseStrategy\\":{\\"type\\":\\"SUBSCRIPTION\\",\\"subscriptionPeriod\\":\\"P1M\\",\\"subscriptionMarketingPricePeriod\\":\\"P1M\\"},\\"id\\":168}","receiptData":"{\\"orderId\\":\\"6660823415974633704.5480098017970283\\",\\"packageName\\":\\"com.neulion.smartphone.ufc.android\\",\\"productId\\":\\"' + androidIAPCodes + '\\",\\"purchaseTime\\":' + purchase_time + ',\\"purchaseState\\":0,\\"purchaseToken\\":\\"' + purchase_token + '\\",\\"developerPayload\\":\\"subs:' + androidIAPCodes + ':{\\\\\\"purchaseStrategy\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"SUBSCRIPTION\\\\\\",\\\\\\"subscriptionPeriod\\\\\\":\\\\\\"P1M\\\\\\",\\\\\\"subscriptionMarketingPricePeriod\\\\\\":\\\\\\"P1M\\\\\\"},\\\\\\"id\\\\\\":168}\\"}"}'
|
|
reciept_encoded = base64.b64encode(reciept_string.encode()).decode()
|
|
|
|
post_data = {
|
|
"base64EncodedReceipt": reciept_encoded
|
|
}
|
|
|
|
post_data = json.dumps(post_data)
|
|
|
|
response = requests.post(url, headers=headers, data=post_data)
|
|
if response.status_code == 202:
|
|
print('Modified purchase accepted')
|
|
else:
|
|
print('Modified purchase rejected')
|
|
print(response.content)
|
|
quit()
|
|
|
|
def request_event(token, event_id):
|
|
url = 'https://dce-frontoffice.imggaming.com/api/v2/stream/event/' + event_id
|
|
|
|
headers = {
|
|
'accept': 'application/json, text/plain, */*',
|
|
'content-type': 'application/json',
|
|
'x-api-key': '4dc1e8df-5869-41ea-95c2-6f04c67459ed',
|
|
'app': 'dice',
|
|
'accept-language': 'en-US',
|
|
'realm': 'dce.ufc',
|
|
'authorization': 'Bearer ' + token,
|
|
'Host': 'dce-frontoffice.imggaming.com',
|
|
'Connection': 'Keep-Alive',
|
|
'Accept-Encoding': 'gzip',
|
|
'User-Agent': 'okhttp/3.11.0'
|
|
}
|
|
|
|
response = requests.get(url, headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
print('Event request successful')
|
|
data = response.json()
|
|
return data['playerUrlCallback']
|
|
else:
|
|
print('Event request failed')
|
|
print(response.content)
|
|
quit()
|
|
|
|
def request_stream(url):
|
|
|
|
headers = {
|
|
'accept': 'application/json, text/plain, */*',
|
|
'content-type': 'application/json',
|
|
'x-api-key': '4dc1e8df-5869-41ea-95c2-6f04c67459ed',
|
|
'app': 'dice',
|
|
'accept-language': 'en-US',
|
|
'realm': 'dce.ufc',
|
|
'Host': 'dge-streaming.imggaming.com',
|
|
'Connection': 'Keep-Alive',
|
|
'Accept-Encoding': 'gzip',
|
|
'User-Agent': 'okhttp/3.11.0',
|
|
}
|
|
|
|
response = requests.get(url, headers)
|
|
if response.status_code == 200:
|
|
print('Stream request successful')
|
|
data = response.json()
|
|
return [data['hls']['url'], data['dash']['url'], data['dash']['drm']['url'], data['dash']['drm']['jwtToken']]
|
|
else:
|
|
print('Stream request failed')
|
|
print(response.content)
|
|
quit()
|
|
|
|
def stream_exteract(url, user_agent):
|
|
try:
|
|
print('Processing temp m3u8')
|
|
|
|
base_url = url.split('/hls/live/', 1)[0] + '/hls/live'
|
|
|
|
perm_streams = []
|
|
|
|
headers = {
|
|
'User-Agent': user_agent,
|
|
}
|
|
|
|
m3u8_obj = m3u8.loads(requests.get(url, headers=headers).content.decode())
|
|
|
|
ascii_clear()
|
|
|
|
print('Extracted urls:')
|
|
|
|
i = 1
|
|
for playlist in m3u8_obj.playlists:
|
|
m3u8_url = playlist.uri.replace('../..', base_url)
|
|
bandwidth = str(playlist.stream_info.bandwidth)
|
|
perm_streams.append({'m3u8_url': m3u8_url, 'bandwidth': bandwidth})
|
|
|
|
print(str(i) + '. '+ bandwidth + ': ' + m3u8_url)
|
|
|
|
i+=1
|
|
|
|
choose = int(input('Choose url for export (0 for none): ' ))
|
|
|
|
if choose == 0 or choose > len(perm_streams):
|
|
quit()
|
|
else:
|
|
return perm_streams[choose-1]['m3u8_url']
|
|
|
|
|
|
except Exception as e:
|
|
print('Stream extraction failed')
|
|
print(e)
|
|
|
|
def get_pssh_perm_key_id(dash_url, user_agent):
|
|
headers = {
|
|
'User-Agent': user_agent
|
|
}
|
|
|
|
response = requests.get(dash_url, headers=headers, allow_redirects=False)
|
|
location_url = response.headers['location']
|
|
|
|
response = requests.get(location_url, headers=headers)
|
|
|
|
soup = BeautifulSoup(response.content, features="xml")
|
|
audio_repr = soup.findAll('Representation')[-1]
|
|
audio_base_url = audio_repr.find('BaseURL').text
|
|
audio_key_id = audio_repr.find('ContentProtection')['cenc:default_KID']
|
|
|
|
location_parts = location_url.split('/')
|
|
location_parts.pop()
|
|
|
|
init_url = '/'.join(location_parts) + '/' + audio_base_url + 'init_' + audio_repr['id'] + '.m4s'
|
|
|
|
init_response = requests.get(init_url, headers=headers)
|
|
|
|
return [to_pssh(init_response.content), location_url, audio_key_id]
|
|
|
|
ascii_clear()
|
|
#token = 'eyJhbGciOiJSUzI1NiIsInB1ciI6IkFVVCIsInNpZyI6ImciLCJ0eXAiOiJKV1QiLCJ2IjozfQ.eyJhcCI6eyJhcHQiOiJJRCJ9LCJhcHIiOiJJRCIsImF1ZCI6WyJkY2UudWZjIl0sImRldiI6IkFORFJPSURfUEhPTkUiLCJlbnYiOiJwcm9kIiwiZXhwIjoxNjg4MjY5NDY0LCJndWUiOmZhbHNlLCJpYXQiOjE2ODgyNjg4NjQsImlwIjoiMTQ5LjcuMTYuMTQ1IiwiaXNzIjoiZGNlLWlkIiwibG8yIjoiR0IsRW5nbGFuZCxFbmdsYW5kLExvbmRvbixFQzRSLDAsMSwwIiwicm9sIjoiQ1VTVE9NRVIiLCJzdWIiOiJWY29QdW58NjAzYjVhNDAtNDU0MC00ZjdjLTgzNGMtOTU5ZGI4Y2Y2M2MyIiwidXRwIjoiSFVNQU4ifQ.TdtxoCGagJbR6s_-PYsHteZVeizk0Ydbol9CZ5POA_stIGeuIrZGU9a99zMWgrpoVZ5L8a-xVbuVg-1G6wu6B4ub5kZToAzDHVG77CpuK8nI5jWGXwG0OqYvoFU398VDUEUpAzGBv_wnetPQEyNnxcaDJF_7sYhmPYrJGgWklNk'
|
|
token = 'eyJhbGciOiJSUzI1NiIsInB1ciI6IkFVVCIsInNpZyI6ImciLCJ0eXAiOiJKV1QiLCJ2IjozfQ.eyJhcCI6eyJhcHQiOiJJRCJ9LCJhcHIiOiJJRCIsImF1ZCI6WyJkY2UudWZjIl0sImRldiI6IkFORFJPSURfUEhPTkUiLCJlbnYiOiJwcm9kIiwiZXhwIjoxNjkwNjg1MDU4LCJndWUiOmZhbHNlLCJpYXQiOjE2OTA2ODQ0NTgsImlwIjoiMTQ2LjcwLjExNi4xODIiLCJpc3MiOiJkY2UtaWQiLCJsbzIiOiJBVCxWaWVubmEsVmllbm5hLFZpZW5uYSwxMjMwLDAsMSwwIiwicm9sIjoiQ1VTVE9NRVIiLCJzdWIiOiJhWHZLclp8MjQ2YWRiNGEtOGI4YS00NWYwLWI2NGYtYjg1ODgxODUzYjNmIiwidXRwIjoiSFVNQU4ifQ.c3fg5QpwJ3s9YB7R8Fd-UE59_N306MgG0S8vHOKdgD8EzNCyuflDTI-B8JgeYOlyfHN07KivjbZ9DFJwD8B9HRwalSP39hfCRUO00uqPbBZapys_zq0MEtG0SLiqJULewZEICm2Kv6mHR-OjtUy_PBmb9gv_ZWDUjWpNf1fN-Zw'
|
|
|
|
#user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'
|
|
user_agent = 'ExoDoris/12.11.0 (Linux;Android 9) ExoPlayerLib/2.18.4'
|
|
|
|
token = refresh_token(token)
|
|
|
|
event = choose_event(token)
|
|
event_id = event[0]
|
|
event_name = event[1]
|
|
androidIAPCodes = event[2]
|
|
|
|
if androidIAPCodes is not None:
|
|
spoof_buy(token, androidIAPCodes)
|
|
|
|
stream_request_url = request_event(token, event_id)
|
|
|
|
hls_url, dash_url, licence_url, licence_token = request_stream(stream_request_url)
|
|
|
|
pssh, perm_dash, key_id = get_pssh_perm_key_id(dash_url, user_agent)
|
|
|
|
key = do_cdm(pssh, licence_url, licence_token, key_id)
|
|
print('Retrieved key: ' + key)
|
|
|
|
fkeys = key.split(":")
|
|
ekeys = base64.b64encode(('{"' + fkeys[0] + '":"' + fkeys[1] + '"}').encode('ascii'))
|
|
print("\nReproductor M3U8 link: " + perm_dash + "?&ck=" + ekeys.decode('ascii'))
|
|
|
|
confirm = input('\nEnter Y to save to playlist.m3u: ')
|
|
|
|
if confirm.lower() != 'y':
|
|
quit()
|
|
|
|
f = open("playlist.m3u", "w+")
|
|
f.write('#EXTINF:-1 tvg-logo="https://www.sportsvideo.org/wp-content/uploads/2021/02/ufc-fight-pass-logo.png" group-title="PPV",' + event_name)
|
|
f.write('\n#EXTVLCOPT:http-user-agent=' + user_agent)
|
|
f.write('\n#KODIPROP:inputstream.adaptive.license_type=clearkey')
|
|
f.write('\n#KODIPROP:inputstream.adaptive.license_key=' + key)
|
|
f.write('\n' + perm_dash)
|
|
|
|
ascii_clear()
|
|
|
|
f.flush()
|
|
f.seek(0)
|
|
print('Exported ' + event_name + ' to playlist.m3u')
|
|
print('Preview: \n')
|
|
print(f.read())
|
|
f.close() |