Fixes for HULU 4K and HS AVC 4K
This commit is contained in:
parent
3efc534b10
commit
3b1bbdb7fd
Binary file not shown.
Binary file not shown.
@ -223,6 +223,8 @@ class Hulu(BaseService):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for track in tracks:
|
||||
track.needs_proxy = False
|
||||
return tracks
|
||||
|
||||
def get_chapters(self, title):
|
||||
|
||||
@ -273,7 +273,7 @@ async def m3u8dl(uri, out, track, headers=None, proxy=None):
|
||||
arguments.extend(["--http-request-timeout", "8"])
|
||||
if track.__class__.__name__ == "VideoTrack":
|
||||
from vinetrimmer.objects.tracks import Track
|
||||
if track.height and not (track.descriptor == Track.Descriptor.M3U):
|
||||
if track.height and not (track.descriptor == Track.Descriptor.M3U) and track.source != "HS":
|
||||
arguments.extend([
|
||||
"-sv", f"res='{track.height}*':codec='{track.codec}':for=best"
|
||||
])
|
||||
|
||||
@ -3,7 +3,7 @@ import hashlib
|
||||
import logging
|
||||
import random
|
||||
|
||||
import pyhulu
|
||||
from vinetrimmer.vendor.pyhulu.client import HuluClient
|
||||
|
||||
|
||||
class Device: # pylint: disable=too-few-public-methods
|
||||
@ -30,7 +30,7 @@ class Device: # pylint: disable=too-few-public-methods
|
||||
)
|
||||
|
||||
|
||||
class HuluClient(pyhulu.HuluClient):
|
||||
class HuluClient(HuluClient):
|
||||
def __init__(self, device, session, version=1, **kwargs):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.device = device
|
||||
|
||||
3
vinetrimmer/vendor/pyhulu/__init__.py
vendored
Normal file
3
vinetrimmer/vendor/pyhulu/__init__.py
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
"""pyhulu - Python library for interacting with the E2E encrypted Hulu API"""
|
||||
|
||||
from vinetrimmer.vendor.pyhulu.client import HuluClient
|
||||
176
vinetrimmer/vendor/pyhulu/client.py
vendored
Normal file
176
vinetrimmer/vendor/pyhulu/client.py
vendored
Normal file
@ -0,0 +1,176 @@
|
||||
"""
|
||||
Client module
|
||||
|
||||
Main module for Hulu API requests
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import requests
|
||||
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util import Padding
|
||||
|
||||
from vinetrimmer.vendor.pyhulu.device import Device
|
||||
|
||||
|
||||
class HuluClient(object):
|
||||
"""
|
||||
HuluClient class
|
||||
|
||||
Main class for Hulu API requests
|
||||
|
||||
__init__:
|
||||
|
||||
@param device_code: Three-digit string or integer (doesn't matter)
|
||||
denoting the device you will make requests as
|
||||
|
||||
@param device_key: 16-byte AES key that corresponds to the device
|
||||
code you're using. This is used to decrypt the
|
||||
device config response.
|
||||
|
||||
@param cookies: Either a cookie jar object or a dict of cookie
|
||||
key / value pairs. This is passed to the requests library,
|
||||
so whatever it takes will work. Examples here:
|
||||
http://docs.python-requests.org/en/master/user/quickstart/#cookies
|
||||
|
||||
@return: HuluClient object
|
||||
"""
|
||||
|
||||
def __init__(self, device_code, device_key, cookies, version=1, extra_playlist_params={}):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.device = Device(device_code, device_key)
|
||||
self.cookies = cookies
|
||||
self.version = version
|
||||
self.extra_playlist_params = extra_playlist_params
|
||||
|
||||
self.session_key, self.server_key = self.get_session_key()
|
||||
|
||||
def load_playlist(self, video_id):
|
||||
"""
|
||||
load_playlist()
|
||||
|
||||
Method to get a playlist containing the MPD
|
||||
and license URL for the provided video ID and return it
|
||||
|
||||
@param video_id: String of the video ID to get a playlist for
|
||||
|
||||
@return: Dict of decrypted playlist response
|
||||
"""
|
||||
|
||||
base_url = 'https://play.hulu.com/v4/playlist'
|
||||
params = {
|
||||
'device_identifier': hashlib.md5().hexdigest().upper(),
|
||||
'deejay_device_id': int(self.device.device_code),
|
||||
'version': self.version,
|
||||
'content_eab_id': video_id,
|
||||
'rv': random.randrange(1E5, 1E6),
|
||||
'kv': self.server_key,
|
||||
}
|
||||
params.update(self.extra_playlist_params)
|
||||
|
||||
resp = requests.post(url=base_url, json=params, cookies=self.cookies)
|
||||
ciphertext = self.__get_ciphertext(resp.text, params)
|
||||
|
||||
return self.decrypt_response(self.session_key, ciphertext)
|
||||
|
||||
def decrypt_response(self, key, ciphertext):
|
||||
"""
|
||||
decrypt_response()
|
||||
|
||||
Method to decrypt an encrypted response with provided key
|
||||
|
||||
@param key: Key in bytes
|
||||
@param ciphertext: Ciphertext to decrypt in bytes
|
||||
|
||||
@return: Decrypted response as a dict
|
||||
"""
|
||||
|
||||
aes_cbc_ctx = AES.new(key, AES.MODE_CBC, iv=b'\0'*16)
|
||||
|
||||
try:
|
||||
plaintext = Padding.unpad(aes_cbc_ctx.decrypt(ciphertext), 16)
|
||||
except ValueError:
|
||||
self.logger.error('Error decrypting response')
|
||||
self.logger.error('Ciphertext:')
|
||||
self.logger.error(base64.b64encode(ciphertext).decode('utf8'))
|
||||
self.logger.error(
|
||||
'Tried decrypting with key %s',
|
||||
base64.b64encode(key).decode('utf8')
|
||||
)
|
||||
|
||||
raise ValueError('Error decrypting response')
|
||||
|
||||
return json.loads(plaintext.decode('latin-1'))
|
||||
|
||||
def get_session_key(self):
|
||||
"""
|
||||
get_session_key()
|
||||
|
||||
Method to do a Hulu config request and calculate
|
||||
the session key against device key and current server key
|
||||
|
||||
@return: Session key in bytes
|
||||
"""
|
||||
|
||||
random_value = random.randrange(1E5, 1E6)
|
||||
|
||||
base = '{device_key},{device},{version},{random_value}'.format(
|
||||
device_key=binascii.hexlify(self.device.device_key).decode('utf8'),
|
||||
device=self.device.device_code,
|
||||
version=self.version,
|
||||
random_value=random_value
|
||||
).encode('utf8')
|
||||
|
||||
nonce = hashlib.md5(base).hexdigest()
|
||||
|
||||
url = 'https://play.hulu.com/config'
|
||||
payload = {
|
||||
'rv': random_value,
|
||||
'mozart_version': '1',
|
||||
'region': 'US',
|
||||
'version': self.version,
|
||||
'device': self.device.device_code,
|
||||
'encrypted_nonce': nonce
|
||||
}
|
||||
|
||||
resp = requests.post(url=url, data=payload)
|
||||
ciphertext = self.__get_ciphertext(resp.text, payload)
|
||||
|
||||
config_dict = self.decrypt_response(
|
||||
self.device.device_key,
|
||||
ciphertext
|
||||
)
|
||||
|
||||
derived_key_array = bytearray()
|
||||
for device_byte, server_byte in zip(self.device.device_key,
|
||||
bytes.fromhex(config_dict['key'])):
|
||||
derived_key_array.append(device_byte ^ server_byte)
|
||||
|
||||
return bytes(derived_key_array), config_dict['key_id']
|
||||
|
||||
def __get_ciphertext(self, text, request):
|
||||
try:
|
||||
ciphertext = bytes.fromhex(text)
|
||||
except ValueError:
|
||||
self.logger.error('Error decoding response hex')
|
||||
self.logger.error('Request:')
|
||||
for line in json.dumps(request, indent=4).splitlines():
|
||||
self.logger.error(line)
|
||||
|
||||
self.logger.error('Response:')
|
||||
for line in text.splitlines():
|
||||
self.logger.error(line)
|
||||
|
||||
raise ValueError('Error decoding response hex')
|
||||
|
||||
return ciphertext
|
||||
|
||||
def __repr__(self):
|
||||
return '<HuluClient session_key=%s>' % base64.b64encode(
|
||||
self.session_key
|
||||
).decode('utf8')
|
||||
31
vinetrimmer/vendor/pyhulu/device.py
vendored
Normal file
31
vinetrimmer/vendor/pyhulu/device.py
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
"""
|
||||
Device module
|
||||
|
||||
Module containing Device data class
|
||||
"""
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
class Device(object): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Device()
|
||||
|
||||
Data class used for containing device attributes
|
||||
"""
|
||||
|
||||
def __init__(self, device_code, device_key):
|
||||
self.device_code = str(device_code)
|
||||
self.device_key = device_key
|
||||
|
||||
if len(self.device_code) != 3:
|
||||
raise ValueError('Invalid device code length')
|
||||
|
||||
if len(self.device_key) != 16:
|
||||
raise ValueError('Invalid device key length')
|
||||
|
||||
def __repr__(self):
|
||||
return '<Device device_code=%s, device_key=%s>' % (
|
||||
self.device_code,
|
||||
base64.b64encode(self.device_key).decode('utf8')
|
||||
)
|
||||
@ -54,7 +54,9 @@ def main(debug):
|
||||
|
||||
log = logging.getLogger("vt")
|
||||
|
||||
log.info("vinetrimmer - Widevine DRM downloader and decrypter")
|
||||
log.debug(sys.argv)
|
||||
|
||||
log.info("vinetrimmer - Widevine and Playready DRM downloader and decrypter")
|
||||
log.info(f"[Root Config] : {filenames.user_root_config}")
|
||||
log.info(f"[Service Configs] : {directories.service_configs}")
|
||||
log.info(f"[Cookies] : {directories.cookies}")
|
||||
@ -71,8 +73,5 @@ def main(debug):
|
||||
|
||||
dl()
|
||||
|
||||
# D:\PlayReady-Amazon-Tool-main\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Aswin\AppData\Local\JetBrains\PyCharm2024.3\cpython-cache "C:/Program Files (x86)/JetBrains/PyCharm 2024.2.4/plugins/python-ce/helpers/pydev/pydevd.py" --port 42000 --module --multiprocess --save-signatures --qt-support=auto --file poetry run vt dl --no-cache --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4
|
||||
# Above seems to work
|
||||
if __name__ == "__main__":
|
||||
#sys.argv = ["vinetrimmer", "dl", "--no-cache", "--keys", "AMZN", "0H7LY5ZKKBM1MIW0244WE9O2C4"]
|
||||
main()
|
||||
|
||||
7
vt.py
7
vt.py
@ -54,7 +54,9 @@ def main(debug):
|
||||
|
||||
log = logging.getLogger("vt")
|
||||
|
||||
log.info("vinetrimmer - Widevine DRM downloader and decrypter")
|
||||
log.debug(sys.argv)
|
||||
|
||||
log.info("vinetrimmer - Widevine and Playready DRM downloader and decrypter")
|
||||
log.info(f"[Root Config] : {filenames.user_root_config}")
|
||||
log.info(f"[Service Configs] : {directories.service_configs}")
|
||||
log.info(f"[Cookies] : {directories.cookies}")
|
||||
@ -71,8 +73,5 @@ def main(debug):
|
||||
|
||||
dl()
|
||||
|
||||
# D:\PlayReady-Amazon-Tool-main\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Aswin\AppData\Local\JetBrains\PyCharm2024.3\cpython-cache "C:/Program Files (x86)/JetBrains/PyCharm 2024.2.4/plugins/python-ce/helpers/pydev/pydevd.py" --port 42000 --module --multiprocess --save-signatures --qt-support=auto --file poetry run vt dl --no-cache --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4
|
||||
# Above seems to work
|
||||
if __name__ == "__main__":
|
||||
#sys.argv = ["vinetrimmer", "dl", "--no-cache", "--keys", "AMZN", "0H7LY5ZKKBM1MIW0244WE9O2C4"]
|
||||
main()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user