Fixes for HULU 4K and HS AVC 4K

This commit is contained in:
chu23465 2025-04-17 14:03:47 +05:30
parent 3efc534b10
commit 3b1bbdb7fd
10 changed files with 222 additions and 12 deletions

Binary file not shown.

View File

@ -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):

View File

@ -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"
])

View File

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

View File

@ -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
View File

@ -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()