├── requirements.txt ├── .DS_Store ├── create_mac_binary.sh ├── create_windows_exe_binary.bat ├── hulusubs_dl ├── .DS_Store ├── __version__.py ├── cust_utils │ ├── __init__.py │ ├── path_util.py │ ├── browser_instance.py │ └── utils.py ├── __init__.py ├── __main__.py ├── subtitle_processing.py ├── hulu_api.py ├── hulu.py └── hulu_subs_dl.py ├── .gitattributes ├── Changelog.md ├── docs ├── Changelog.md └── index.md ├── ToDo.md ├── .gitignore ├── setup.py ├── LICENSE ├── .travis.yml └── ReadMe.md /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pathlib -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xonshiz/Hulusubs_dl/HEAD/.DS_Store -------------------------------------------------------------------------------- /create_mac_binary.sh: -------------------------------------------------------------------------------- 1 | pyinstaller --onefile --hidden-import=queue "hulusubs_dl\__main__.py" -------------------------------------------------------------------------------- /create_windows_exe_binary.bat: -------------------------------------------------------------------------------- 1 | pyinstaller --onefile --hidden-import=queue "hulusubs_dl\__main__.py" -------------------------------------------------------------------------------- /hulusubs_dl/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xonshiz/Hulusubs_dl/HEAD/hulusubs_dl/.DS_Store -------------------------------------------------------------------------------- /hulusubs_dl/__version__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = "2021.06.07_1" 5 | -------------------------------------------------------------------------------- /hulusubs_dl/cust_utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from . import path_util 5 | from . import utils 6 | from . import browser_instance 7 | -------------------------------------------------------------------------------- /hulusubs_dl/__init__.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # # -*- coding: utf-8 -*- 3 | # 4 | # from hulusubs_dl import cust_utils 5 | # from hulusubs_dl import __version__ 6 | # from hulusubs_dl import hulu_api 7 | -------------------------------------------------------------------------------- /hulusubs_dl/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | sys.path.append("..") 7 | from hulusubs_dl.__version__ import __version__ 8 | from hulusubs_dl.cust_utils import * 9 | from hulusubs_dl.hulu_subs_dl import HuluSubsDl 10 | 11 | if __name__ == "__main__": 12 | HuluSubsDl(sys.argv[1:], os.getcwd()) 13 | sys.exit() 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /hulusubs_dl/cust_utils/path_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from pathlib import Path 5 | 6 | 7 | def get_abs_path_name(file_path, file_name): 8 | return os.path.abspath(file_path + file_name) 9 | 10 | 11 | def file_exists(file_path, file_name): 12 | return os.path.isfile(get_abs_path_name(file_path, file_name)) 13 | 14 | 15 | def create_paths(directory): 16 | Path(os.path.abspath(directory)).mkdir(parents=True, exist_ok=True) 17 | return os.path.abspath(directory) 18 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | - Re-did the entire codebase and implemented hulu API to scrap data. 4 | - Proxy support added. 5 | - Fix for #22 6 | - Added Verbose Logging. 7 | - `#22` was still happening. Verified and fixed it. 8 | - Updated TravisCI Configs to do a Github Release. 9 | - Fixed #27 10 | - Added support to download subtitles of "movies". 11 | - Fix for #29 and #28 12 | - Fix for #25 13 | - Fix for #31 14 | - Subtitles will now show proper language names instead of 'en', 'es', etc. 15 | - Fix for path naming issues #31 -------------------------------------------------------------------------------- /docs/Changelog.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | - Re-did the entire codebase and implemented hulu API to scrap data. 4 | - Proxy support added. 5 | - Fix for #22 6 | - Added Verbose Logging. 7 | - `#22` was still happening. Verified and fixed it. 8 | - Updated TravisCI Configs to do a Github Release. 9 | - Fixed #27 10 | - Added support to download subtitles of "movies". 11 | - Fix for #29 and #28 12 | - Fix for #25 13 | - Fix for #31 14 | - Subtitles will now show proper language names instead of 'en', 'es', etc. 15 | - Fix for path naming issues #31 -------------------------------------------------------------------------------- /ToDo.md: -------------------------------------------------------------------------------- 1 | # To Do 2 | Things that I have in mind, things that I want to include later in this script/tool. 3 | - Encrypt Cookie saved in the text file. Currently it's saved in plain text and if anyone gets access to it, they can exploit it. 4 | - Use authentication APIs from Hulu to remove the un-necessary step of fetching Cookies manually. 5 | - Be able to see if cookies have expired and ask the user again for input and update the current values. 6 | - Provide config and cookie file locations based on operating system so people can directly edit the config and cookie files. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.xml 3 | *.iml 4 | *.xml 5 | *.iml 6 | .idea/Hulu-Subs-Downloader.iml 7 | *.xml 8 | *.pyc 9 | .idea/Hulu-Subs-Downloader.iml 10 | 11 | /dist/* 12 | /build/* 13 | *.manifest 14 | *.toc 15 | *.pkg 16 | *.pyz 17 | build/HuluSubsDownloader/warnHuluSubsDownloader.txt 18 | build/HuluSubsDownloader/xref-HuluSubsDownloader.html 19 | *.spec 20 | .idea/workspace.xml 21 | *.xml 22 | ReadMe.txt 23 | setup - Copy.py 24 | hulusubs_dl/.cookie 25 | hulusubs_dl/.config 26 | *.pyc 27 | hulusubs_dl/venv/* 28 | hulusubs_dl/__pycache__/* 29 | 30 | *.pyc 31 | hulusubs_dl.egg-info/* 32 | hulusubs_dl/cust_utils/__pycache__/* 33 | hulusubs_dl/cust_utils/__pycache__/utils.cpython-37.pyc 34 | hulusubs_dl/cust_utils/__pycache__/browser_instance.cpython-37.pyc 35 | hulusubs_dl/build/* 36 | hulusubs_dl/dist/* 37 | hulusubs_dl/dev_test/* 38 | hulusubs_dl/dev_test/main_test.py 39 | hulusubs_dl/Hulusubs_dl_Error_Log.log 40 | *.log 41 | venv/* 42 | hulusubs_dl/.DS_Store 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | from hulusubs_dl import __version__ 4 | 5 | readme = open('ReadMe.md').read() 6 | history = open('Changelog.md').read() 7 | 8 | setuptools.setup( 9 | name="hulusubs_dl", # Replace with your own username 10 | version=__version__.__version__, 11 | author="Xonshiz", 12 | author_email='xonshiz@gmail.com', 13 | url='https://github.com/Xonshiz/Hulu-Subs-Downloader', 14 | download_url='https://github.com/Xonshiz/Hulu-Subs-Downloader/releases/latest', 15 | description="hulusubs_dl is a command line tool to download subtitles from Hulu.", 16 | long_description='{0}\n\n{1}'.format(readme, history), 17 | long_description_content_type="text/markdown", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | python_requires='>=2.6', 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013-2021 Blackrock Digital LLC. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /hulusubs_dl/subtitle_processing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Using these open-source libraries for the conversion. I've modified the code as per requirements of the current project. 5 | Thanks to these amazing folks. 6 | vtt_to_srt.py - (c) Jansen A. Simanullang (https://github.com/jansenicus/vtt-to-srt.py/blob/master/vtt_to_srt.py) 7 | """ 8 | import re 9 | 10 | 11 | def convert_content(file_contents): 12 | replacement = re.sub(r'(\d\d:\d\d:\d\d).(\d\d\d) --> (\d\d:\d\d:\d\d).(\d\d\d)(?:[ \-\w]+:[\w\%\d:]+)*\n', 13 | r'\1,\2 --> \3,\4\n', file_contents) 14 | replacement = re.sub(r'(\d\d:\d\d).(\d\d\d) --> (\d\d:\d\d).(\d\d\d)(?:[ \-\w]+:[\w\%\d:]+)*\n', 15 | r'\1,\2 --> \3,\4\n', replacement) 16 | replacement = re.sub(r'(\d\d).(\d\d\d) --> (\d\d).(\d\d\d)(?:[ \-\w]+:[\w\%\d:]+)*\n', r'\1,\2 --> \3,\4\n', 17 | replacement) 18 | replacement = re.sub(r'WEBVTT\n', '', replacement) 19 | replacement = re.sub(r'Kind:[ \-\w]+\n', '', replacement) 20 | replacement = re.sub(r'Language:[ \-\w]+\n', '', replacement) 21 | replacement = re.sub(r'', '', replacement) 22 | replacement = re.sub(r'', '', replacement) 23 | replacement = re.sub(r'<\d\d:\d\d:\d\d.\d\d\d>', '', replacement) 24 | replacement = re.sub(r'::[\-\w]+\([\-.\w\d]+\)[ ]*{[.,:;\(\) \-\w\d]+\n }\n', '', replacement) 25 | replacement = re.sub(r'Style:\n##\n', '', replacement) 26 | 27 | return replacement 28 | 29 | -------------------------------------------------------------------------------- /hulusubs_dl/hulu_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from . import cust_utils 5 | import json 6 | 7 | BASE_URL = 'https://discover.hulu.com/content/v3/entity' 8 | 9 | 10 | def get_playlist_information(payload, cookie_value): 11 | if isinstance(payload, dict): 12 | payload = json.dumps(payload) 13 | response = cust_utils.browser_instance.post_request(url='https://play.hulu.com/v6/playlist', data=payload, 14 | cookie_value=cookie_value) 15 | return response 16 | 17 | 18 | def get_eab_id_metadata(eab_id, cookie_value, subtitle_lang='en'): 19 | url = BASE_URL + '?device_context_id=1&eab_ids={0}&language={1}&referral_host=www.hulu.com&schema=4'.format( 20 | str(eab_id).replace(':', '%3A'), subtitle_lang) 21 | response = cust_utils.browser_instance.get_request(url=url, cookie_value=cookie_value) 22 | return response 23 | 24 | 25 | def get_full_eab_id(eab_id, cookie_value): 26 | url = 'https://discover.hulu.com/content/v5/deeplink/playback?namespace=entity&id={0}&schema=1&device_info=web:3.10.0&referralHost=production'.format( 27 | eab_id) 28 | response = cust_utils.browser_instance.get_request(url=url, cookie_value=cookie_value) 29 | return response 30 | 31 | 32 | def get_series_metadata(series_eab_id, cookie_value): 33 | url = 'https://discover.hulu.com/content/v5/hubs/series/{0}?schema=1&limit=999&device_info=web:3.10.0&referralHost=production'.format( 34 | series_eab_id) 35 | response = cust_utils.browser_instance.get_request(url=url, cookie_value=cookie_value) 36 | return response 37 | 38 | 39 | def get_series_season_metadata(series_eab_id, cookie_value, season): 40 | url = 'https://discover.hulu.com/content/v5/hubs/series/{0}/season/{1}?limit=999&schema=1&offset=0&device_info=web:3.10.0&referralHost=production'.format( 41 | series_eab_id, season) 42 | response = cust_utils.browser_instance.get_request(url=url, cookie_value=cookie_value) 43 | return response 44 | -------------------------------------------------------------------------------- /hulusubs_dl/cust_utils/browser_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import requests 4 | import json 5 | from random import random 6 | import logging 7 | 8 | 9 | def get_user_agent(): 10 | user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)' 11 | user_agent = user_agent + ' Chrome/56.0.2924.87 Safari/537.36' 12 | return user_agent 13 | 14 | 15 | def get_request(url, cookie_value, text_only=False, **kwargs): 16 | _proxy = kwargs.get("proxy") 17 | _rand_proxy = None 18 | if not cookie_value: 19 | raise Warning("No Cookie Value Provided. Exiting") 20 | headers = { 21 | 'User-Agent': get_user_agent(), 22 | 'Accept-Encoding': 'gzip, deflate', 23 | 'Cookie': cookie_value 24 | } 25 | if _proxy and len(_proxy) > 0: 26 | try: 27 | _rand_proxy = random.choice(_proxy) 28 | except IndexError as error: 29 | print("Proxy Failed : {0}".format(error)) 30 | print("Continuing Without Proxy.") 31 | _rand_proxy = None 32 | 33 | proxy = { 34 | "http": _rand_proxy, 35 | "https": _rand_proxy 36 | } 37 | 38 | logging.debug('GET url: {0}'.format(url)) 39 | logging.debug('GET proxy: {0}'.format(proxy)) 40 | 41 | sess = requests.session() 42 | connection = sess.get(url, headers=headers, proxies=proxy) 43 | 44 | if connection.status_code != 200: 45 | print("Whoops! Seems like I can't connect to website.") 46 | print("It's showing : %s" % connection) 47 | print("Run this script with the --verbose argument and report the issue along with log file on Github.") 48 | print("Can't connect to website %s" % url) 49 | return None 50 | else: 51 | if text_only: 52 | return connection.content 53 | return json.loads(connection.text.encode("utf-8")) 54 | 55 | 56 | def post_request(url, data, cookie_value, **kwargs): 57 | _proxy = kwargs.get("proxy") 58 | _rand_proxy = None 59 | if not cookie_value: 60 | raise Warning("No Cookie Value Provided. Exiting") 61 | headers = { 62 | 'User-Agent': get_user_agent(), 63 | 'Accept-Encoding': 'gzip, deflate, br', 64 | 'Accept': '*/*', 65 | 'Content-Type': 'application/json', 66 | 'Cookie': cookie_value 67 | } 68 | if _proxy and len(_proxy) > 0: 69 | try: 70 | _rand_proxy = random.choice(_proxy) 71 | except IndexError as error: 72 | print("Proxy Failed : {0}".format(error)) 73 | print("Continuing Without Proxy.") 74 | _rand_proxy = None 75 | 76 | proxy = { 77 | "http": _rand_proxy, 78 | "https": _rand_proxy 79 | } 80 | logging.debug('POST url: {0}'.format(url)) 81 | logging.debug('POST proxy: {0}'.format(proxy)) 82 | sess = requests.session() 83 | connection = sess.post(url, data=data, headers=headers, proxies=proxy) 84 | 85 | if connection.status_code != 200: 86 | print("Whoops! Seems like I can't connect to website.") 87 | print("It's showing : %s" % connection) 88 | print("Run this script with the --verbose argument and report the issue along with log file on Github.") 89 | print("Can't connect to website %s" % url) 90 | return None 91 | else: 92 | return json.loads(connection.text.encode("utf-8")) 93 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - language: python 4 | python: 5 | - 2.7 6 | dist: trusty 7 | install: 8 | - "pip install -r requirements.txt" 9 | script: 10 | - cd hulusubs_dl 11 | - python __main__.py --version 12 | - cd .. 13 | deploy: 14 | - provider: pypi 15 | edge: 16 | branch: v1.8.45 17 | username: xonshiz 18 | password: $PYPI_TOKEN 19 | distributions: "sdist bdist_wheel" 20 | on: 21 | branch: master 22 | notifications: 23 | email: 24 | - xonshiz@gmail.com 25 | - language: python 26 | python: 27 | - 3.5 28 | dist: trusty 29 | install: 30 | - "pip install -r requirements.txt" 31 | script: 32 | - cd hulusubs_dl 33 | - python __main__.py --version 34 | - cd .. 35 | notifications: 36 | email: 37 | - xonshiz@gmail.com 38 | - os: linux 39 | language: python 40 | python: 41 | - 3.8 42 | dist: xenial 43 | before_install: 44 | - "pip install --upgrade pip" 45 | install: 46 | - "python --version" 47 | - "pip install requests" 48 | - "pip install pyinstaller" 49 | script: 50 | - cd hulusubs_dl 51 | - python __main__.py --version 52 | - pyinstaller --onefile --hidden-import=queue "__main__.py" -n "hulusubs_dl_linux" 53 | - ls 54 | - ls "dist" 55 | - cd .. 56 | notifications: 57 | email: 58 | - xonshiz@gmail.com 59 | before_deploy: 60 | - export TRAVIS_TAG="1.0.$TRAVIS_BUILD_NUMBER" 61 | - echo "$TRAVIS_TAG" "$TRAVIS_COMMIT" 62 | - git config --local user.name "$USER_NAME" 63 | - git config --local user.email "$USER_EMAIL" 64 | - git tag "$TRAVIS_TAG" "$TRAVIS_COMMIT" 65 | deploy: 66 | - provider: releases 67 | tag_name: $TRAVIS_TAG 68 | overwrite: true 69 | api_key: $GITHUB_TOKEN 70 | name: "Hulusubs_dl" 71 | file: "hulusubs_dl/dist/hulusubs_dl_linux" 72 | skip_cleanup: true 73 | draft: false 74 | on: 75 | branch: master 76 | - os: windows 77 | language: sh 78 | python: "3.8" 79 | before_install: 80 | - choco install python --version 3.8.0 81 | - python --version 82 | - export PATH="/c/Python38:/c/Python38/Scripts:$PATH" 83 | - python -m pip install --upgrade pip 84 | env: PATH=/c/Python38:/c/Python38/Scripts:$PATH 85 | install: 86 | - "pip install -r requirements.txt" 87 | - "pip install pyinstaller" 88 | script: 89 | - cd hulusubs_dl 90 | - python __main__.py --version 91 | - pyinstaller --onefile --hidden-import=queue "__main__.py" -n "hulusubs_dl.exe" 92 | - cd dist 93 | - dir -s 94 | - cd .. 95 | - cd .. 96 | notifications: 97 | email: 98 | - xonshiz@gmail.com 99 | before_deploy: 100 | - export TRAVIS_TAG="1.0.$TRAVIS_BUILD_NUMBER" 101 | - echo "$TRAVIS_TAG" "$TRAVIS_COMMIT" 102 | - git config --local user.name "$USER_NAME" 103 | - git config --local user.email "$USER_EMAIL" 104 | - git tag "$TRAVIS_TAG" "$TRAVIS_COMMIT" 105 | deploy: 106 | - provider: releases 107 | tag_name: $TRAVIS_TAG 108 | overwrite: true 109 | api_key: $GITHUB_TOKEN 110 | name: "Hulusubs_dl" 111 | file: "hulusubs_dl/dist/hulusubs_dl.exe" 112 | skip_cleanup: true 113 | draft: false 114 | on: 115 | branch: master 116 | - os: osx 117 | language: sh 118 | python: "3.8" 119 | before_install: 120 | - python3 --version 121 | - python3 -m pip install --upgrade pip 122 | install: 123 | - "pip install -r requirements.txt" 124 | - "pip install pyinstaller" 125 | script: 126 | - cd hulusubs_dl 127 | - python3 __main__.py --version 128 | - pyinstaller --onefile --hidden-import=queue "__main__.py" -n "hulusubs_dl_osx" 129 | - ls 130 | - ls "dist" 131 | - export huluVersion=`cat version.txt` 132 | - cd .. 133 | notifications: 134 | email: 135 | - xonshiz@gmail.com 136 | before_deploy: 137 | - export TRAVIS_TAG="1.0.$TRAVIS_BUILD_NUMBER" 138 | - echo "$TRAVIS_TAG" "$TRAVIS_COMMIT" 139 | - git config --local user.name "$USER_NAME" 140 | - git config --local user.email "$USER_EMAIL" 141 | deploy: 142 | - provider: releases 143 | tag_name: $TRAVIS_TAG 144 | overwrite: true 145 | api_key: $GITHUB_TOKEN 146 | name: "Hulusubs_dl" 147 | file: "hulusubs_dl/dist/hulusubs_dl_osx" 148 | skip_cleanup: true 149 | draft: false 150 | on: 151 | branch: master 152 | -------------------------------------------------------------------------------- /hulusubs_dl/cust_utils/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from . import path_util 4 | 5 | DEFAULT_SUB_EXT = ['webvtt', 'ttml', 'smi'] 6 | 7 | 8 | def get_value_from_list(value, list_object): 9 | if type(list_object) == type(list): 10 | for x in list_object: 11 | if str(x).lower().strip() == str(value).lower().strip(): 12 | return x 13 | return list_object[0] 14 | 15 | 16 | def get_playlist_body(eab_id): 17 | body = { 18 | "deejay_device_id": 190, 19 | "version": 1, 20 | "all_cdn": True, 21 | "content_eab_id": eab_id, 22 | "region": "US", 23 | "xlink_support": False, 24 | "limit_ad_tracking": False, 25 | "ignore_kids_block": False, 26 | "language": "en", 27 | "unencrypted": True, 28 | "interface_version": "1.9.0", 29 | "network_mode": "wifi", 30 | "play_intent": "resume", 31 | "playback": { 32 | "version": 2, 33 | "video": { 34 | "codecs": { 35 | "values": [ 36 | { 37 | "type": "H264", 38 | "width": 1024, 39 | "height": 546, 40 | "framerate": 60, 41 | "level": "4.2", 42 | "profile": "HIGH" 43 | } 44 | ], 45 | "selection_mode": "ONE" 46 | } 47 | }, 48 | "audio": { 49 | "codecs": { 50 | "values": [ 51 | { 52 | "type": "AAC" 53 | } 54 | ], 55 | "selection_mode": "ALL" 56 | } 57 | }, 58 | "drm": { 59 | "values": [ 60 | { 61 | "type": "WIDEVINE", 62 | "version": "MODULAR", 63 | "security_level": "L3" 64 | }, 65 | { 66 | "type": "PLAYREADY", 67 | "version": "V2", 68 | "security_level": "SL2000" 69 | } 70 | ], 71 | "selection_mode": "ALL", 72 | "hdcp": False 73 | }, 74 | "manifest": { 75 | "type": "DASH", 76 | "https": True, 77 | "multiple_cdns": True, 78 | "patch_updates": True, 79 | "hulu_types": True, 80 | "live_dai": True, 81 | "secondary_audio": True, 82 | "live_fragment_delay": 3 83 | }, 84 | "segments": { 85 | "values": [ 86 | { 87 | "type": "FMP4", 88 | "encryption": { 89 | "mode": "CENC", 90 | "type": "CENC" 91 | }, 92 | "https": True 93 | } 94 | ], 95 | "selection_mode": "ONE" 96 | } 97 | } 98 | } 99 | return body 100 | 101 | 102 | def create_file(file_path, file_name, data_to_write): 103 | if not isinstance(data_to_write, str): 104 | data_to_write = str(data_to_write) 105 | if not data_to_write or not str(data_to_write).strip(): 106 | print("Empty data provided for {0}".format(file_name)) 107 | return False 108 | file_location = path_util.get_abs_path_name(file_path, file_name) 109 | with open(file_location, 'w') as f: 110 | f.write(data_to_write) 111 | f.flush() 112 | return True 113 | 114 | 115 | def create_file_binary_mode(file_path, file_name, data_to_write): 116 | if not data_to_write or not str(data_to_write).strip(): 117 | print("Empty data provided for {0}".format(file_name)) 118 | return False 119 | file_location = path_util.get_abs_path_name(file_path, file_name) 120 | try: 121 | with open(file_location, 'wb') as f: 122 | f.write(data_to_write) 123 | f.flush() 124 | except Exception as ex: 125 | print("Exception Happened while writing: {0}".format(ex)) 126 | return True 127 | 128 | 129 | def read_file_data(file_path, file_name): 130 | file_location = path_util.get_abs_path_name(file_path, file_name) 131 | content = None 132 | with open(file_location, 'r') as f: 133 | content = f.read().strip() 134 | return None if content == "" else content 135 | 136 | 137 | def get_clean_path_name(path_name): 138 | for cha in '\/*?:"<>|,;\'': 139 | path_name = path_name.replace(cha, ' -') 140 | return path_name 141 | 142 | 143 | def get_language_name(lang_code): 144 | lang_dict = { 145 | 'en': 'English', 146 | 'es': 'Spanish', 147 | 'jp': 'Japanese' 148 | } 149 | return lang_dict.get(lang_code, lang_code) 150 | -------------------------------------------------------------------------------- /hulusubs_dl/hulu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .cust_utils import * 5 | from . import subtitle_processing 6 | from . import hulu_api 7 | import os 8 | import re 9 | from time import sleep 10 | import logging 11 | 12 | 13 | class Hulu: 14 | def __init__(self, url, cookie_value, language, extension, download_location, proxy): 15 | if "/movie/" in url: 16 | eab_id_matches = str(url).split('/movie/')[-1].split('-')[-5:] 17 | if eab_id_matches: 18 | url = 'https://www.hulu.com/watch/{0}'.format('-'.join(eab_id_matches)) 19 | if "/series/" in url: 20 | self.show_link(url, cookie_value, language, extension, download_location, proxy) 21 | elif "/watch/" in url: 22 | self.episode_link(url, cookie_value, language, extension, download_location, proxy) 23 | else: 24 | print("URL Not Supported") 25 | 26 | def episode_link(self, url, cookie_value, language, extension, download_location, proxy=None): 27 | logging.debug('Called: episode_link()') 28 | is_dub = False 29 | transcript_urls = {} 30 | eab_id = str(url).split('/watch/')[-1].replace('/', '') 31 | logging.debug('initial eab_id: {0}'.format(eab_id)) 32 | eab_id_information = hulu_api.get_full_eab_id(eab_id, cookie_value) 33 | logging.debug('\n----\neab_id_information: {0}\n----\n'.format(eab_id_information)) 34 | eab_id = dict(eab_id_information).get('eab_id', None) 35 | logging.debug('eab_id: {0}'.format(eab_id)) 36 | if not eab_id: 37 | print("You seem to be out of USA. Use a VPN/Proxy.") 38 | return False 39 | payload = utils.get_playlist_body(eab_id=eab_id) 40 | logging.debug('\n----\npayload: {0}\n----\n'.format(payload)) 41 | if payload: 42 | # Logic to find the URL for the language and extension provided and download it. 43 | playlist_info = hulu_api.get_playlist_information(payload, cookie_value) 44 | logging.debug('\n----\nplaylist_info: {0}\n----\n'.format(playlist_info)) 45 | if playlist_info: 46 | playlist_info = dict(playlist_info) 47 | transcripts = playlist_info.get('transcripts_urls', None) 48 | logging.debug('\n----\ntranscripts: {0}\n----\n'.format(transcripts)) 49 | if not transcripts: 50 | print("Couldn't find transcript URLs.Exiting.") 51 | return False 52 | else: 53 | transcript_urls = dict(transcripts) 54 | # we will convert webvtt to any other subtitle format.So,we'll use that URL to get subtitle content. 55 | if extension not in utils.DEFAULT_SUB_EXT: 56 | transcript_urls[extension] = transcript_urls.get('webvtt', {}) 57 | logging.debug('\n----\ntranscript_urls: {0}\n----\n'.format(transcript_urls)) 58 | video_metadata = dict(hulu_api.get_eab_id_metadata(eab_id, cookie_value, language)).get('items', {}) 59 | logging.debug('\n----\nvideo_metadata: {0}\n----\n'.format(video_metadata)) 60 | video_metadata = dict(list(video_metadata)[0]) 61 | episode_title = video_metadata.get('name', None) 62 | if episode_title and "(dub)" in str(episode_title).lower(): 63 | is_dub = True 64 | series_name = utils.get_clean_path_name(video_metadata.get('series_name', video_metadata.get("name", "No Name Found"))) 65 | is_movie = True if video_metadata.get('_type', None) == "movie" else False 66 | season_number = video_metadata.get('season', "01") 67 | episode_number = video_metadata.get('number', "01") 68 | if not is_dub: 69 | file_name = '{0} - S{1}E{2} [{3} Sub].{4}'.format(series_name, season_number, episode_number, 70 | utils.get_language_name(language), extension) 71 | else: 72 | file_name = '{0} - S{1}E{2} [{3} Dub].{4}'.format(series_name, season_number, episode_number, 73 | utils.get_language_name(language), extension) 74 | if is_movie: 75 | if not is_dub: 76 | file_name = '{0} [{1} Sub].{2}'.format(series_name, utils.get_language_name(language), 77 | extension) 78 | else: 79 | file_name = '{0} [{1} Dub].{2}'.format(series_name, utils.get_language_name(language), 80 | extension) 81 | logging.debug('\n----\nfile_name: {0}\n----\n'.format(file_name)) 82 | print("Downloading Subtitle For {0}".format(file_name)) 83 | selected_extension = transcript_urls.get(extension, None) 84 | if not selected_extension: 85 | print("Couldn't find {0} in Hulu".format(extension)) 86 | else: 87 | url = str(dict(selected_extension).get(language, None)).strip() 88 | logging.debug('subtitle url eab_id: {0}'.format(url)) 89 | subtitle_content = browser_instance.get_request(url, cookie_value, text_only=True) 90 | path_created = False 91 | if is_movie: 92 | path_created = path_util.create_paths(download_location + os.sep + series_name) 93 | else: 94 | path_created = path_util.create_paths(download_location + os.sep + series_name + os.sep + season_number) 95 | if path_created: 96 | if extension == 'srt': 97 | subtitle_content = subtitle_processing.convert_content(subtitle_content.decode('utf-8')).encode('utf-8') 98 | file_written = utils.create_file_binary_mode(path_created, os.sep + file_name, subtitle_content) 99 | if file_written: 100 | return True 101 | else: 102 | return False 103 | else: 104 | print("Failed To build payload.") 105 | return False 106 | 107 | def show_link(self, url, cookie_value, language, extension, download_location, proxy=None): 108 | # We need just the EAB_ID, which is typically split via - and is of 5 words from end. 109 | eab_id_matches = str(url).split('/series/')[-1].split('-')[-5:] 110 | logging.debug('eab_id_matches: {0}'.format(eab_id_matches)) 111 | if eab_id_matches and len(eab_id_matches) > 1: 112 | eab_id = '-'.join(eab_id_matches) 113 | logging.debug('initial eab_id: {0}'.format(eab_id)) 114 | series_metadata = {} 115 | series_metadata = hulu_api.get_series_metadata(eab_id, cookie_value) 116 | logging.debug('\n----\ninitial series_metadata: {0}\n----\n'.format(series_metadata)) 117 | if not series_metadata: 118 | print("Couldn't get series metadata. Exiting") 119 | return False 120 | series_metadata_components = dict(series_metadata).get('components', {}) 121 | if not series_metadata_components or len(series_metadata_components) == 0: 122 | print("Data not available. Check your Proxy/VPN.") 123 | else: 124 | episodes_metadata = {} 125 | for curr_item in series_metadata_components: 126 | if dict(curr_item).get('name', None) == 'Episodes': 127 | episodes_metadata = curr_item 128 | break 129 | series_metadata = dict(episodes_metadata).get('items', {}) 130 | logging.debug('\n----\nseries_metadata: {0}\n----\n'.format(series_metadata)) 131 | season_numbers = [] 132 | season_episodes = [] 133 | if not series_metadata: 134 | print("No Series information found.Make sure you're in USA region.") 135 | print("Trying using a VPN/Proxy.") 136 | return False 137 | for item in series_metadata: 138 | # Saving the season numbers 139 | current_id = dict(item).get('id', None) 140 | if current_id: 141 | season_numbers.append(str(current_id).split('::')[-1]) 142 | print("Getting Data For {0} Seasons.".format(len(season_numbers))) 143 | logging.debug('\n----\nseason_numbers: {0}\n----\n'.format(season_numbers)) 144 | for season in season_numbers: 145 | # For every season, we'll collect EAB IDs of the episodes. 146 | season_metadata = dict(hulu_api.get_series_season_metadata(eab_id, cookie_value, season)).get('items', 147 | {}) 148 | for season_item in season_metadata: 149 | season_episodes.append(dict(season_item).get('id', None)) 150 | print("Total Episodes To Download: {0}".format(len(season_episodes))) 151 | logging.debug('\n----\nseason_episodes: {0}\n----\n'.format(season_episodes)) 152 | for episode_eab in season_episodes: 153 | if episode_eab: 154 | sleep(1) # Let's not bombard Hulu's API with requests. 155 | self.episode_link('https://www.hulu.com/watch/{0}'.format(episode_eab), cookie_value, language, 156 | extension, download_location) 157 | return True 158 | -------------------------------------------------------------------------------- /hulusubs_dl/hulu_subs_dl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import argparse 7 | import logging 8 | import platform 9 | from .cust_utils import * 10 | from .__version__ import __version__ 11 | from .hulu import Hulu 12 | 13 | 14 | class HuluSubsDl: 15 | def __init__(self, argv, cwd): 16 | cookie_file_name = '/.cookie' 17 | config_file_name = '/.config' 18 | supported_languages = ['en', 'es', 'jp'] 19 | supported_extensions = ['srt', 'webvtt', 'smi', 'ttml'] 20 | self.max_tries_for_cookie = 5 21 | cookie_file_data = None 22 | config_file_data = None 23 | url = None 24 | self.download_location = None 25 | self.subtitle_lang = None 26 | self.subtitle_extension = None 27 | self.proxy = [] 28 | self.logger = False 29 | skip_config = False 30 | 31 | args = self.add_argparse() 32 | if args.version: 33 | print(__version__) 34 | sys.exit(0) 35 | if args.verbose: 36 | print("\n***Starting the script in Verbose Mode***\n") 37 | try: 38 | os.remove("Error_Log.log") 39 | # with open(str(args.download_directory[0]) + str(os.sep) + "Error_Log.log", "w") as wf: 40 | # wf.write("Writing...") 41 | except Exception as VerboseError: 42 | # print(VerboseError) 43 | pass 44 | logging.basicConfig(format='%(levelname)s: %(message)s', 45 | filename=str(cwd) + str(os.sep) + "Hulusubs_dl_Error_Log.log", 46 | level=logging.DEBUG) 47 | logging.debug("Arguments Provided : %s" % args) 48 | logging.debug("Operating System : %s - %s - %s" % (platform.system(), 49 | platform.release(), 50 | platform.version() 51 | )) 52 | logging.debug("Python Version : %s (%s)" % (platform.python_version(), platform.architecture()[0])) 53 | logging.debug("Hulusubs_dl Version : {0}".format(__version__)) 54 | self.logger = True 55 | if args.make_config: 56 | config_object = self.get_config_file_data() 57 | config_data = self.ask_config_file_data(config_object) 58 | file_written = utils.create_file(cwd, config_file_name, config_data) 59 | if not file_written: 60 | print("Couldn't write config file.") 61 | sys.exit(0) 62 | if args.hulu_url: 63 | url = args.hulu_url[0] 64 | else: 65 | while not url: 66 | url = input("Enter Hulu URL : ") 67 | url = url.strip() 68 | if args.skip_config: 69 | skip_config = args.skip_config[0] 70 | 71 | if not skip_config: 72 | print("Reading Configuration File.") 73 | if path_util.file_exists(cwd, config_file_name): 74 | config_file_data = eval(utils.read_file_data(cwd, config_file_name)) 75 | logging.debug("\n----\nconfig_file_data: {0}\n----\n".format(config_file_data)) 76 | else: 77 | # ask config data from user 78 | config_object = self.get_config_file_data() 79 | config_data = self.ask_config_file_data(config_object) 80 | file_written = utils.create_file(cwd, config_file_name, config_data) 81 | if file_written: 82 | config_file_data = config_data 83 | if not config_file_data: 84 | print("Resetting Config File") 85 | config_file_data = self.get_config_file_data() 86 | else: 87 | self.set_config_file_data(config_file_data) 88 | 89 | if args.proxy: 90 | user_proxy = eval(args.proxy) 91 | if isinstance(user_proxy, list): 92 | self.proxy = args.proxy 93 | elif isinstance(user_proxy, str): 94 | self.proxy = str(user_proxy).split(';') 95 | else: 96 | print("Wrong proxy format specified.Exiting") 97 | sys.exit(0) 98 | 99 | if not self.subtitle_lang: 100 | self.subtitle_lang = utils.get_value_from_list(args.subtitle_language[0], supported_languages) 101 | if not self.subtitle_extension: 102 | self.subtitle_extension = utils.get_value_from_list(args.subtitle_extension[0], supported_extensions) 103 | # If user provides VTT, we have to make it webvtt 104 | if self.subtitle_extension.lower().strip() == "vtt": 105 | self.subtitle_extension = "webvtt" 106 | if not self.download_location: 107 | if args.download_directory: 108 | self.download_location = args.download_directory[0] 109 | else: 110 | while not self.download_location: 111 | self.download_location = input("Enter Download Location : ") 112 | 113 | if not args.set_cookie and path_util.file_exists(cwd, cookie_file_name): 114 | cookie_file_data = utils.read_file_data(cwd, cookie_file_name) 115 | if not cookie_file_data: 116 | logging.debug("No Cookie Found") 117 | else: 118 | cookie_from_user = self.get_cookie_from_user() 119 | cookie_written = utils.create_file(cwd, cookie_file_name, 120 | cookie_from_user if not args.set_cookie else args.set_cookie[0]) 121 | if cookie_written: 122 | cookie_file_data = cookie_from_user 123 | 124 | if not cookie_file_data: 125 | current_try = 1 126 | while not cookie_file_data and self.max_tries_for_cookie >= current_try: 127 | cookie_from_user = self.get_cookie_from_user() 128 | cookie_written = utils.create_file(cwd, cookie_file_name, cookie_from_user) 129 | if cookie_written: 130 | cookie_file_data = cookie_from_user 131 | else: 132 | current_try += 1 133 | 134 | try: 135 | # Everything is set, let's call the main boss 136 | Hulu(url, cookie_file_data, self.subtitle_lang, self.subtitle_extension, self.download_location, self.proxy) 137 | except Exception as HuluException: 138 | logging.debug("HuluException: {0}".format(HuluException)) 139 | 140 | def set_config_file_data(self, config_file_data): 141 | # We'll map the data to variables in this method 142 | config_file_data = dict(config_file_data) 143 | self.max_tries_for_cookie = config_file_data.get('max_tries_for_cookie', 5) 144 | self.download_location = config_file_data.get('download_location', None) 145 | self.subtitle_lang = config_file_data.get('subtitle_lang', None) 146 | self.subtitle_extension = config_file_data.get('subtitle_extension', None) 147 | # We're saving ';' separated proxy values. So, get the object and split it. 148 | _proxies = config_file_data.get('proxy', None) 149 | if _proxies: 150 | self.proxy = str(_proxies).split(';') 151 | return None 152 | 153 | @staticmethod 154 | def get_config_file_data(): 155 | config = { 156 | 'max_tries_for_cookie': None, 157 | 'download_location': None, 158 | 'subtitle_lang': None, 159 | 'subtitle_extension': None, 160 | 'proxies': [] 161 | } 162 | return config 163 | 164 | @staticmethod 165 | def ask_config_file_data(config): 166 | config = dict(config) 167 | for conf in config: 168 | if conf == "max_tries_for_cookie": 169 | config[conf] = input("Value For {0}: ".format(conf)) 170 | config[conf] = 5 if not config[conf] else config[conf] 171 | elif conf == "download_location": 172 | config[conf] = input("Value For {0}: ".format(conf)) 173 | config[conf] = os.getcwd() if not config[conf] else config[conf] 174 | elif conf == "subtitle_lang": 175 | config[conf] = input("Value For {0}: ".format(conf)) 176 | config[conf] = 'en' if not config[conf] else config[conf] 177 | elif conf == "subtitle_extension": 178 | config[conf] = input("Value For {0}: ".format(conf)) 179 | config[conf] = 'srt' if not config[conf] else config[conf] 180 | elif conf == "proxies": 181 | config[conf] = input("Value For {0} (Split multiple proxies by ';'): ".format(conf)) 182 | else: 183 | config[conf] = input("Value For {0} : ".format(conf)) 184 | return config 185 | 186 | @staticmethod 187 | def get_cookie_from_user(): 188 | cookie = None 189 | while not cookie: 190 | cookie = input("Paste Hulu Cookie Value : ") 191 | # Fix for #25 192 | cookie = cookie.replace('\\u2026', '') 193 | return cookie 194 | 195 | @staticmethod 196 | def add_argparse(): 197 | parser = argparse.ArgumentParser( 198 | description="HuluSubs_dl is a command line tool to download subtitles from Hulu.") 199 | parser.add_argument('--version', action='store_true', help='Shows version and exits.') 200 | parser.add_argument('-cookie', '--set-cookie', nargs=1, help='Saves Hulu Cookie.', default=None) 201 | parser.add_argument('-url', '--hulu-url', nargs=1, help='Provides URL of the hulu video.', default=None) 202 | parser.add_argument('-dd', '--download-directory', nargs=1, 203 | help='Decides the download directory of the subtitle(s).', default=None) 204 | parser.add_argument('-ext', '--subtitle-extension', nargs=1, 205 | help='Decides the file extension of the final file.', default='srt') 206 | parser.add_argument('-lang', '--subtitle-language', nargs=1, help='Decides the language of the subtitle file.', 207 | default='en') 208 | parser.add_argument('-skip-conf', '--skip-config', action='store_true', help='Skips reading config file.') 209 | parser.add_argument('-proxy', '--proxy', nargs=1, help='Provides the Proxy to be used.', default=[]) 210 | parser.add_argument('-config', '--make-config', action='store_true', help='Creates/Resets Config File & exits.') 211 | parser.add_argument("-v", "--verbose", help="Prints important debugging messages on screen.", action="store_true") 212 | args = parser.parse_args() 213 | return args 214 | 215 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Hulusubs_dl | [![Build Status](https://travis-ci.com/Xonshiz/Hulusubs_dl.svg?branch=master)](https://travis-ci.com/github/Xonshiz/Hulusubs_dl) | [![GitHub release](https://img.shields.io/github/v/release/xonshiz/hulusubs_dl.svg?style=flat-square)](https://github.com/Xonshiz/Hulusubs_dl/releases/) | [![Github All Releases](https://img.shields.io/github/downloads/xonshiz/Hulusubs_dl/total.svg?style=flat-square)](https://github.com/xonshiz/Hulusubs_dl/releases) [![Open Source Helpers](https://www.codetriage.com/xonshiz/hulu-subs-downloader/badges/users.svg)](https://www.codetriage.com/xonshiz/hulu-subs-downloader) 2 | Hulusubs_dl is a command line tool to download subtitles from Hulu. Made for educational purposes. 3 | Since it's Python based, it can be easily deployed on every platform (Windows, macOS, Linux/Ubuntu etc.). 4 | You can find the installation instructions in #Installation Section of this readme. 5 | 6 | **NOTE:** 7 | 8 | This tool is based around and work with your hulu account's COOKIES. But, please do remember that you should NEVER share your account cookies with anyone and anywhere. 9 | Person having access to your cookies can use your account. Even when you're sharing any script failure, remember to not share/post your account cookies. 10 | 11 | ## Table of Content 12 | * [Prerequisite](#prerequisite) 13 | * [Python Support](#python-support) 14 | * [Usage](#usage) 15 | * [Things To Remember](#things-to-remember) 16 | * [How To Find Hulu Cookie](#how-to-find-hulu-cookie) 17 | * [Walkthrough Video](#walkthrough-video) 18 | * [Installation](#installation) 19 | * [Windows](#windows-exe-binary-installation) 20 | * [Linux/Debian/Ubuntu](#linuxubuntukubuntu-or-any-other-linux-flavor-installation) 21 | * [Mac OS X](#macos) 22 | * [List of Arguments](#list-of-arguments) 23 | * [Supported Formats](#supported-formats) 24 | * [Proxy Usage](#proxy-usage) 25 | * [Opening Issues](#opening-issues) 26 | * [How To Contribute](#how-to-contribute) 27 | * [Donations](#donations) 28 | 29 | ## Prerequisite: 30 | Since Hulu has now protected their content behind an "auth" wall, we can't access the website. In layman words, we need to log in to Hulu, in order to watch anything or to be able to get basic things to extract the subtitles. 31 | When you run the tool first time, it asks for "cookie" value. You can see it in [`#How To Find Hulu Cookie`](#how-to-find-hulu-cookie) section of this readme. 32 | Also, there's a "configuration file" that is automatically made by this tool. It has some basic settings that you can use as "default" values. 33 | Some values saved in config file are: 34 | - Default Download Location: Tool will download the subtitles files in this directory (the tool makes proper folders). 35 | - Extension: Extension of final subtitle file. You can choose from "Srt, ttml, vtt, smi". Most players will play SRT subtitle files. 36 | - Language: Hulu has 2 languages available at the moment, i.e., "English" & "Spanish". You can download either of them. Type in "en" or "es" for respective languages. 37 | 38 | You can specify these values in the file once and then tool will use these defaults. You can use "arguments" to override these anytime. You would need to pass the argument with the script (described later in this readme). 39 | 40 | ## Python Support 41 | This script should run on both Python 2 and 3. Check travisCI builds for exact python versions. 42 | 43 | ## Usage 44 | Using this tool can be a little tricky for some people at first, but it's pretty much straightforward. Try to follow along. 45 | Make sure you've gone through [`#Prerequisites`](#prerequisite) and have proper version downloaded and installed on your system from the #Installation section. 46 | 47 | ## Things To Remember 48 | - You should renew your cookie value from time to time. These cookies expire after some time. So, if you're not able to log in or get the subtitles, try to renew your cookies. Renew cookies meaning, do the steps of [`#How To Find Hulu Cookie`](#how-to-find-hulu-cookie) again. 49 | - If the tool isn't working, always try to download the latest release and then try again. If it still fails, open an issue here. 50 | - Account COOKIES is sensitive data. Never share/post them anywhere. 51 | 52 | ## How To Find Hulu Cookie 53 | - Make sure you're in US region (use a VPN or Proxy) and open up your browser. 54 | - Go to hulu.com and make sure you're not signed in (If you're signed in, just logout). 55 | - Open developer console (Most browsers have shortcut F12). 56 | - Navigate to "Network" tab. 57 | - Log into hulu now. You'll see that "Network" tab now has many urls populated. 58 | - There should be a "filter" option somewhere in developer console. You need to paste and filter this URL `discover.hulu.com/content/v5/me/state` 59 | - You'll see only some URLs will be there now. Just select anyone of them and in that you need to see "Request Header" section. 60 | - Copy the "Cookie" value from it. It'll be a very long text value. Copy all of it. 61 | - Paste that cookie value when the hulusubs_dl asks for it. 62 | 63 | According to @IRLPinkiePie , when you copy cookies from firefox, it adds an "ellipse" in the cookie. Be watchful of that. 64 | More on that here : [Firefox Cookie Issue](https://github.com/Xonshiz/Hulusubs_dl/issues/25#issuecomment-808623911) 65 | 66 | Refer to this screenshot for some clarification: 67 | 68 | [![N|Solid](https://i.imgur.com/4Z0KOn4.png)](https://i.imgur.com/4Z0KOn4.png) 69 | 70 | ## Walkthrough Video 71 | If you're stuck somewhere or need clarification, here's an in-depth video on how to install and use this tool (Windows & Mac). 72 | Video will be sharing in a week or so from now. 73 | 74 | ## Installation 75 | ### Windows EXE Binary Installation 76 | If you're on windows, it's recommended that you download and use "windows exe binary" to save your time. 77 | You can download the latest windows release from [RELEASE SECTION](https://github.com/Xonshiz/Hulu-Subs-Downloader/releases/latest) 78 | Go there and download the ".exe" file. Then follow the usage instructions in [Usage](#usage). 79 | After downloading this exe file, place it in some location that you can access. Because you would need to run this script every time you want to download subtitles. 80 | Don't put this in your "Windows" or "System" folders. It might cause conflicts with permissions. 81 | 82 | ### Linux/Ubuntu/Kubuntu or any other linux flavor Installation 83 | Since I cannot generate a "binary" for these distributions, you will have to install and use python version directly. 84 | It's pretty much straightforward, you can just use pip to install hulusubs_dl. 85 | `pip install hulusubs_dl` 86 | 87 | If for some reason, you're unable to use `pip`, try with `easy_install`. 88 | 89 | If everything fails, you can download code from this repository and then run. 90 | But, now you'll need to install the dependencies yourself. After downloading, navigate to this folder in your terminal and you can see a "requirements.txt" file. 91 | You can install all dependencies via `pip install -r requirements.txt` 92 | All the external dependencies required by this tool are mentioned in that file. You can install them one by one. 93 | Since you're doing things manually, you might need to give this file executable rights, which can be done like this: 94 | `chmod +x __main__.py` 95 | 96 | ### MacOS 97 | If you're on macOS, it's recommended that you download and use "macOS binary" to save your time. 98 | You can download the latest macOS release from [RELEASE SECTION](https://github.com/Xonshiz/Hulu-Subs-Downloader/releases/latest) 99 | Go there and download the "hulusubs_dl" file. Do verify that you're not downloading "hulusubs_dl.exe". Then follow the usage instructions in [Usage](#usage). 100 | After downloading this file, place it in some location that you can access. Because you would need to run this script every time you want to download subtitles. 101 | Don't put this in restricted places like "/bin/ or "System" folders. It might cause conflicts with permissions. 102 | 103 | ## List of Arguments 104 | Currently, the script supports these arguments : 105 | 106 | ``` 107 | -h, --help Prints the basic help menu of the script and exits. 108 | -url,--hulu-url Url of the Hulu video or series to download subtitles from. 109 | --version Prints the VERSION and exits. 110 | -v,--verbose Enables Verbose logging. 111 | -dd,--download-directory Specifies custom download location for the subtitles. 112 | -cookie, --set-cookie Saves/Updates Hulu Cookie 113 | -ext, --subtitle-extension Specifies the format of final subtitle file. Default is SRT. 114 | -lang, --subtitle-language Specifies the language of the subtitle to download (subtitle in that language should be available on Hulu). 115 | -skip-conf, --skip-config Skips reading the config file (default values). Could be handy if you're writing batch scipts. 116 | -proxy, --proxy If you have an http/https proxy, you can provide it here. Tool will use this proxy to make all connections to Hulu. 117 | -config, --make-config Creates/Resets config file & exits(overwrites current default values). 118 | ``` 119 | 120 | ## Supported Formats 121 | Some arguments support some specific range of values. You can see them below here. 122 | Values are separated via ';'. 123 | 124 | ``` 125 | -lang, --subtitle-language : en (default);es 126 | -ext, --subtitle-extension: srt (default);ttml;vtt;smi 127 | ``` 128 | 129 | ## Proxy Usage 130 | If you're not in US region and don't want to set up a system wide VPN, then you can provide any http/https proxy and hulusubs_dl would use this proxy to make all connections to Hulu. 131 | Using proxy is simple, you can provide it like this 132 | `python __main__.py -proxy 123.456.789.0123:4444` 133 | 134 | If you're on windows, you won't be using `__main__.py`, instead you'll use `hulusubs_dl.exe`. Command would become: 135 | `python hulusubs_dl.exe -proxy 123.456.789.0123:4444` 136 | 137 | You can also save proxies in the config file, so that you don't have to pass them as arguments everytime. 138 | Trigger make config via `--make-config` flag and when the tool asks for Proxy, input proxy like this: 139 | 140 | Single Proxy : `123.456.789.0123:4444` 141 | 142 | Multiple Proxies: `123.456.789.0123:4444;2212.127.11.32:5555` (Notice how we're splitting multiple proxies based on ';') 143 | 144 | If you provide multiple proxies, the tool randomly chooses either of the proxies for every connection. This could avoid proxy ban, if you're using this too much. 145 | 146 | ## Opening Issues 147 | If you're opening a new Issue, please keep these points in your issue description: 148 | - Run the script with `-v` or `--verbose` option and upload the errorlog file that tool creates. (remove your login credentials from it). 149 | - Your operating system (Windows, MacOS, Ubuntu etc.) 150 | - Operating System version: Windows 10/MacOS Catalina/Ubuntu 16 etc. 151 | - Which version are you using: Python Script/Windows EXE Binary/MacOS Homebrew 152 | - URL to the Hulu series which failed. 153 | - Detailed Description of the issue you're facing. 154 | 155 | If you're opening an issue to recommend some enhancements/changes, please be as detailed as possible. Also keep these things in mind: 156 | - Will this "enhancement" be good for general public? or is it just for "you"? I cannot develop things just for 1 person. This tool is built for general masses and any custom enhancement would be charged. 157 | - What you're about to write, does it explain the problem and solution properly? IS it enough for anyone to understand? 158 | 159 | ## How To Contribute 160 | - If you can make this tool better or fix some edge case(s), please feel free to `fork` this repository and then raise a `PR` after your changes. 161 | - Send PRs to `dev` branch only (don't send direct to master). 162 | - Just make sure that the imports are proper, basic python naming conventions are followed. 163 | - Add the necessary information about the change in "changelog.md". 164 | - Remember to bump up the version in __version__.py. (Read how to name the version below). 165 | - If it's just a typo in some file, please just open an issue about it. When I have multiple open issues with typo fixes, I'll make the necessary changes. Reason being that I want to avoid useless CI getting triggered and pushing useless updates across the channel. 166 | 167 | ### Version Convention 168 | You can find the version in `__verion__.py`. Just update the value according to these rules. 169 | 170 | Convention: Year.Month.Date 171 | 172 | So, if you're making that PR on 23rd June, 2020, version would be : 2020.06.23 173 | 174 | What if you've raised multiple PRs on same day? Simple, just append version for the day like: 175 | 176 | Convention: Year.Month.Date.RecurrenceCount 177 | 178 | Again, taking example of 23rd June, 2020, let's say you've made 3 different PRs, different versions would be: `2020.06.23.1`, `2020.06.23.2` and `2020.06.23.3` 179 | 180 | # Donations 181 | If you're feeling generous, you can donate some $$ via Paypal: 182 | 183 | Paypal : [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/xonshiz) 184 | 185 | Any amount is appreciated :) 186 | 187 | 188 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Hulusubs_dl | [![Build Status](https://travis-ci.com/Xonshiz/Hulusubs_dl.svg?branch=master)](https://travis-ci.com/github/Xonshiz/Hulusubs_dl) | [![GitHub release](https://img.shields.io/github/v/release/xonshiz/hulusubs_dl.svg?style=flat-square)](https://github.com/Xonshiz/Hulusubs_dl/releases/) | [![Github All Releases](https://img.shields.io/github/downloads/xonshiz/Hulusubs_dl/total.svg?style=flat-square)](https://github.com/xonshiz/Hulusubs_dl/releases) [![Open Source Helpers](https://www.codetriage.com/xonshiz/hulu-subs-downloader/badges/users.svg)](https://www.codetriage.com/xonshiz/hulu-subs-downloader) 2 | Hulusubs_dl is a command line tool to download subtitles from Hulu. Made for educational purposes. 3 | Since it's Python based, it can be easily deployed on every platform (Windows, macOS, Linux/Ubuntu etc.). 4 | You can find the installation instructions in #Installation Section of this readme. 5 | 6 | **NOTE:** 7 | 8 | This tool is based around and work with your hulu account's COOKIES. But, please do remember that you should NEVER share your account cookies with anyone and anywhere. 9 | Person having access to your cookies can use your account. Even when you're sharing any script failure, remember to not share/post your account cookies. 10 | 11 | ## Table of Content 12 | * [Prerequisite](#prerequisite) 13 | * [Python Support](#python-support) 14 | * [Usage](#usage) 15 | * [Things To Remember](#things-to-remember) 16 | * [How To Find Hulu Cookie](#how-to-find-hulu-cookie) 17 | * [Walkthrough Video](#walkthrough-video) 18 | * [Installation](#installation) 19 | * [Windows](#windows-exe-binary-installation) 20 | * [Linux/Debian/Ubuntu](#linuxubuntukubuntu-or-any-other-linux-flavor-installation) 21 | * [Mac OS X](#macos) 22 | * [List of Arguments](#list-of-arguments) 23 | * [Supported Formats](#supported-formats) 24 | * [Proxy Usage](#proxy-usage) 25 | * [Opening Issues](#opening-issues) 26 | * [How To Contribute](#how-to-contribute) 27 | * [Donations](#donations) 28 | 29 | ## Prerequisite: 30 | Since Hulu has now protected their content behind an "auth" wall, we can't access the website. In layman words, we need to log in to Hulu, in order to watch anything or to be able to get basic things to extract the subtitles. 31 | When you run the tool first time, it asks for "cookie" value. You can see it in [`#How To Find Hulu Cookie`](#how-to-find-hulu-cookie) section of this readme. 32 | Also, there's a "configuration file" that is automatically made by this tool. It has some basic settings that you can use as "default" values. 33 | Some values saved in config file are: 34 | - Default Download Location: Tool will download the subtitles files in this directory (the tool makes proper folders). 35 | - Extension: Extension of final subtitle file. You can choose from "Srt, ttml, vtt, smi". Most players will play SRT subtitle files. 36 | - Language: Hulu has 2 languages available at the moment, i.e., "English" & "Spanish". You can download either of them. Type in "en" or "es" for respective languages. 37 | 38 | You can specify these values in the file once and then tool will use these defaults. You can use "arguments" to override these anytime. You would need to pass the argument with the script (described later in this readme). 39 | 40 | ## Python Support 41 | This script should run on both Python 2 and 3. Check travisCI builds for exact python versions. 42 | 43 | ## Usage 44 | Using this tool can be a little tricky for some people at first, but it's pretty much straightforward. Try to follow along. 45 | Make sure you've gone through [`#Prerequisites`](#prerequisite) and have proper version downloaded and installed on your system from the #Installation section. 46 | 47 | ## Things To Remember 48 | - You should renew your cookie value from time to time. These cookies expire after some time. So, if you're not able to log in or get the subtitles, try to renew your cookies. Renew cookies meaning, do the steps of [`#How To Find Hulu Cookie`](#how-to-find-hulu-cookie) again. 49 | - If the tool isn't working, always try to download the latest release and then try again. If it still fails, open an issue here. 50 | - Account COOKIES is sensitive data. Never share/post them anywhere. 51 | 52 | ## How To Find Hulu Cookie 53 | - Make sure you're in US region (use a VPN or Proxy) and open up your browser. 54 | - Go to hulu.com and make sure you're not signed in (If you're signed in, just logout). 55 | - Open developer console (Most browsers have shortcut F12). 56 | - Navigate to "Network" tab. 57 | - Log into hulu now. You'll see that "Network" tab now has many urls populated. 58 | - There should be a "filter" option somewhere in developer console. You need to paste and filter this URL `discover.hulu.com/content/v5/me/state` 59 | - You'll see only some URLs will be there now. Just select anyone of them and in that you need to see "Request Header" section. 60 | - Copy the "Cookie" value from it. It'll be a very long text value. Copy all of it. 61 | - Paste that cookie value when the hulusubs_dl asks for it. 62 | 63 | According to @IRLPinkiePie , when you copy cookies from firefox, it adds an "ellipse" in the cookie. Be watchful of that. 64 | More on that here : [Firefox Cookie Issue](https://github.com/Xonshiz/Hulusubs_dl/issues/25#issuecomment-808623911) 65 | 66 | Refer to this screenshot for some clarification: 67 | 68 | [![N|Solid](https://i.imgur.com/4Z0KOn4.png)](https://i.imgur.com/4Z0KOn4.png) 69 | 70 | ## Walkthrough Video 71 | If you're stuck somewhere or need clarification, here's an in-depth video on how to install and use this tool (Windows & Mac). 72 | Video will be sharing in a week or so from now. 73 | 74 | ## Installation 75 | ### Windows EXE Binary Installation 76 | If you're on windows, it's recommended that you download and use "windows exe binary" to save your time. 77 | You can download the latest windows release from [RELEASE SECTION](https://github.com/Xonshiz/Hulu-Subs-Downloader/releases/latest) 78 | Go there and download the ".exe" file. Then follow the usage instructions in [Usage](#usage). 79 | After downloading this exe file, place it in some location that you can access. Because you would need to run this script every time you want to download subtitles. 80 | Don't put this in your "Windows" or "System" folders. It might cause conflicts with permissions. 81 | 82 | ### Linux/Ubuntu/Kubuntu or any other linux flavor Installation 83 | Since I cannot generate a "binary" for these distributions, you will have to install and use python version directly. 84 | It's pretty much straightforward, you can just use pip to install hulusubs_dl. 85 | `pip install hulusubs_dl` 86 | 87 | If for some reason, you're unable to use `pip`, try with `easy_install`. 88 | 89 | If everything fails, you can download code from this repository and then run. 90 | But, now you'll need to install the dependencies yourself. After downloading, navigate to this folder in your terminal and you can see a "requirements.txt" file. 91 | You can install all dependencies via `pip install -r requirements.txt` 92 | All the external dependencies required by this tool are mentioned in that file. You can install them one by one. 93 | Since you're doing things manually, you might need to give this file executable rights, which can be done like this: 94 | `chmod +x __main__.py` 95 | 96 | ### MacOS 97 | If you're on macOS, it's recommended that you download and use "macOS binary" to save your time. 98 | You can download the latest macOS release from [RELEASE SECTION](https://github.com/Xonshiz/Hulu-Subs-Downloader/releases/latest) 99 | Go there and download the "hulusubs_dl" file. Do verify that you're not downloading "hulusubs_dl.exe". Then follow the usage instructions in [Usage](#usage). 100 | After downloading this file, place it in some location that you can access. Because you would need to run this script every time you want to download subtitles. 101 | Don't put this in restricted places like "/bin/ or "System" folders. It might cause conflicts with permissions. 102 | 103 | ## List of Arguments 104 | Currently, the script supports these arguments : 105 | 106 | ``` 107 | -h, --help Prints the basic help menu of the script and exits. 108 | -url,--hulu-url Url of the Hulu video or series to download subtitles from. 109 | --version Prints the VERSION and exits. 110 | -v,--verbose Enables Verbose logging. 111 | -dd,--download-directory Specifies custom download location for the subtitles. 112 | -cookie, --set-cookie Saves/Updates Hulu Cookie 113 | -ext, --subtitle-extension Specifies the format of final subtitle file. Default is SRT. 114 | -lang, --subtitle-language Specifies the language of the subtitle to download (subtitle in that language should be available on Hulu). 115 | -skip-conf, --skip-config Skips reading the config file (default values). Could be handy if you're writing batch scipts. 116 | -proxy, --proxy If you have an http/https proxy, you can provide it here. Tool will use this proxy to make all connections to Hulu. 117 | -config, --make-config Creates/Resets config file & exits(overwrites current default values). 118 | ``` 119 | 120 | ## Supported Formats 121 | Some arguments support some specific range of values. You can see them below here. 122 | Values are separated via ';'. 123 | 124 | ``` 125 | -lang, --subtitle-language : en (default);es 126 | -ext, --subtitle-extension: srt (default);ttml;vtt;smi 127 | ``` 128 | 129 | ## Proxy Usage 130 | If you're not in US region and don't want to set up a system wide VPN, then you can provide any http/https proxy and hulusubs_dl would use this proxy to make all connections to Hulu. 131 | Using proxy is simple, you can provide it like this 132 | `python __main__.py -proxy 123.456.789.0123:4444` 133 | 134 | If you're on windows, you won't be using `__main__.py`, instead you'll use `hulusubs_dl.exe`. Command would become: 135 | `python hulusubs_dl.exe -proxy 123.456.789.0123:4444` 136 | 137 | You can also save proxies in the config file, so that you don't have to pass them as arguments everytime. 138 | Trigger make config via `--make-config` flag and when the tool asks for Proxy, input proxy like this: 139 | 140 | Single Proxy : `123.456.789.0123:4444` 141 | 142 | Multiple Proxies: `123.456.789.0123:4444;2212.127.11.32:5555` (Notice how we're splitting multiple proxies based on ';') 143 | 144 | If you provide multiple proxies, the tool randomly chooses either of the proxies for every connection. This could avoid proxy ban, if you're using this too much. 145 | 146 | ## Opening Issues 147 | If you're opening a new Issue, please keep these points in your issue description: 148 | - Run the script with `-v` or `--verbose` option and upload the errorlog file that tool creates. (remove your login credentials from it). 149 | - Your operating system (Windows, MacOS, Ubuntu etc.) 150 | - Operating System version: Windows 10/MacOS Catalina/Ubuntu 16 etc. 151 | - Which version are you using: Python Script/Windows EXE Binary/MacOS Homebrew 152 | - URL to the Hulu series which failed. 153 | - Detailed Description of the issue you're facing. 154 | 155 | If you're opening an issue to recommend some enhancements/changes, please be as detailed as possible. Also keep these things in mind: 156 | - Will this "enhancement" be good for general public? or is it just for "you"? I cannot develop things just for 1 person. This tool is built for general masses and any custom enhancement would be charged. 157 | - What you're about to write, does it explain the problem and solution properly? IS it enough for anyone to understand? 158 | 159 | ## How To Contribute 160 | - If you can make this tool better or fix some edge case(s), please feel free to `fork` this repository and then raise a `PR` after your changes. 161 | - Send PRs to `dev` branch only (don't send direct to master). 162 | - Just make sure that the imports are proper, basic python naming conventions are followed. 163 | - Add the necessary information about the change in "changelog.md". 164 | - Remember to bump up the version in __version__.py. (Read how to name the version below). 165 | - If it's just a typo in some file, please just open an issue about it. When I have multiple open issues with typo fixes, I'll make the necessary changes. Reason being that I want to avoid useless CI getting triggered and pushing useless updates across the channel. 166 | 167 | ### Version Convention 168 | You can find the version in `__verion__.py`. Just update the value according to these rules. 169 | 170 | Convention: Year.Month.Date 171 | 172 | So, if you're making that PR on 23rd June, 2020, version would be : 2020.06.23 173 | 174 | What if you've raised multiple PRs on same day? Simple, just append version for the day like: 175 | 176 | Convention: Year.Month.Date.RecurrenceCount 177 | 178 | Again, taking example of 23rd June, 2020, let's say you've made 3 different PRs, different versions would be: `2020.06.23.1`, `2020.06.23.2` and `2020.06.23.3` 179 | 180 | # Donations 181 | If you're feeling generous, you can donate some $$ via Paypal: 182 | 183 | Paypal : [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/xonshiz) 184 | 185 | Any amount is appreciated :) 186 | 187 | 188 | --------------------------------------------------------------------------------