HuluToDisney+.py
import os
import platform
import subprocess
import time
import datetime
import getpass
# Get current time for logging
def current_time():
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + ": "
# Install Selenium if not already installed
try:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
except ImportError:
if os.name == "posix": # Unix-like systems (including macOS)
os.system("pip3 install selenium")
else:
os.system("pip install selenium")
# Check if the OS allows popup boxes
try:
import tkinter as tk
from tkinter import Tk, simpledialog, IntVar, Radiobutton
except ImportError:
# If popup boxes are not allowed, do not import tkinter
pass
#Initialization
def get_chrome_executable_path():
if 'CHROME_BIN' in os.environ:
return os.environ['CHROME_BIN']
chrome_executable_path = None
print(f"{current_time()} System Identified as {platform.system()}. Setting Chrome Executable Path based on this...")
if platform.system() == 'Linux':
try:
chrome_executable_path = subprocess.check_output(['which', 'chromium-browser']).decode().split('\n')[0]
except subprocess.CalledProcessError:
pass
if not chrome_executable_path:
try:
chrome_executable_path = subprocess.check_output(['which', 'chromium']).decode().split('\n')[0]
if not chrome_executable_path:
raise Exception('Chromium not found (which chromium)')
except subprocess.CalledProcessError:
pass
elif platform.system() == 'Darwin':
potential_paths = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
]
for path in potential_paths:
if os.path.exists(path):
chrome_executable_path = path
break
elif platform.system() == 'Windows':
potential_paths = [
r'C:\Program Files\Google\Chrome\Application\chrome.exe',
r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
os.path.join(os.getenv('USERPROFILE'), 'AppData', 'Local', 'Google', 'Chrome', 'Application', 'chrome.exe'),
os.path.join(os.getenv('USERPROFILE'), 'AppData', 'Local', 'Chromium', 'Application', 'chrome.exe'),
]
for path in potential_paths:
if os.path.exists(path):
chrome_executable_path = path
break
else:
raise Exception(f'Unsupported platform: {platform.system()}')
print(f"{current_time()} Chrome Executable Path found at {chrome_executable_path}.")
return chrome_executable_path
def create_chrome_driver():
# Initialize Chrome driver
chrome_options = webdriver.ChromeOptions()
chrome_arguments = [
'--headless',
'--window-size=1920,1080',
'--disable-web-security',
'--no-first-run',
'--disable-infobars',
'--hide-crash-restore-bubble',
'--disable-blink-features=AutomationControlled',
'--hide-scrollbars',
'--enable-automation',
'--disable-extensions',
'--disable-default-apps',
'--disable-component-update',
'--disable-component-extensions-with-background-pages',
'--enable-blink-features=IdleDetection',
'--no-sandbox',
'--verbose'
]
for arg in chrome_arguments:
chrome_options.add_argument(arg)
# Set the path to the Chrome executable (modify this based on your system)
chrome_options.binary_location = get_chrome_executable_path() # Use the previously defined function
# Initialize the Chrome driver
driver = webdriver.Chrome(options=chrome_options)
return driver
# Exit Chrome and kill all Chrome processes
def chrome_quit(driver):
print(f"{current_time()} Closing Chrome session...")
driver.quit()
print(f"{current_time()} Chrome session closed.")
# Fill in field
def get_user_input(prompt):
if os.name == "posix": # Unix-like systems (including macOS)
# Use command line input
while True:
user_input = input(prompt)
if user_input:
return user_input
else:
print(f"{current_time()} Input cannot be blank. Please try again.")
else:
# Use pop-up window
while True:
root = Tk()
root.withdraw() # Hide the main window
user_input = simpledialog.askstring("Input", prompt)
root.destroy() # Close the pop-up window
if user_input:
return user_input
else:
print(f"{current_time()} Input cannot be blank. Please try again.")
# Fill in field (masked)
def get_user_input_masked(prompt):
if os.name == "posix": # Unix-like systems (including macOS)
# Use command line input
while True:
user_input = getpass.getpass(prompt)
if user_input:
return user_input
else:
print(f"{current_time()} Input cannot be blank. Please try again.")
else:
# Use pop-up window
while True:
root = Tk()
root.withdraw() # Hide the main window
user_input = simpledialog.askstring("Input", prompt, show="*")
root.destroy() # Close the pop-up window
if user_input:
return user_input
else:
print(f"{current_time()} Input cannot be blank. Please try again.")
# Select an option from list
def get_user_input_select(prompt, options):
if os.name == "posix": # Unix-like systems (including macOS)
# Use command line input
print(prompt)
for i, option in enumerate(options):
print(f"{i + 1}. {option}")
while True:
try:
selected_option = int(input("Select an option: "))
if 1 <= selected_option <= len(options):
return options[selected_option - 1]
else:
print("Invalid option. Please try again.")
except ValueError:
print("Invalid input. Please enter a number.")
else:
# Use pop-up window
root = tk.Tk()
root.withdraw()
# Create a variable to store the selected option
selected_option = None
# Create a dialog box with buttons for each option
dialog = tk.Toplevel(root)
dialog.title("Select an option")
dialog.geometry("300x150")
# Add the prompt to the dialog
tk.Label(dialog, text=prompt).pack()
def select_option(option):
nonlocal selected_option
selected_option = option
dialog.destroy()
for option in options:
tk.Button(dialog, text=option, command=lambda o=option: select_option(o)).pack()
# Wait for the user to select an option
dialog.wait_window()
# Return the selected option
return selected_option
# Pick a directory
def get_directory():
while True:
if os.name == "posix": # Unix-like systems (including macOS)
directory_path = input("Enter a directory path: ")
else:
try:
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw() # Hide the main window
directory_path = filedialog.askdirectory(title="Select a base directory")
except ImportError:
directory_path = input("Enter a directory path: ")
# Validate if a value was entered
if directory_path:
break
else:
print(f"{current_time()} Error: Please provide a valid directory path.")
return directory_path
def main():
username_flag = True
password_flag = True
# Define the URLs to test is a username was successful (add more if needed)
username_urls = [
"https://www.disneyplus.com/identity/login/enter-password",
"https://www.disneyplus.com/identity/login/enter-passcode" #,
# Add additional condition URLs here
]
while True:
# Initialize the Chrome driver
driver = create_chrome_driver()
print(f"{current_time()} Chrome session is running in the background.")
# Navigate to the Disney+ login page
driver.get("https://www.disneyplus.com/identity/login")
print(f"{current_time()} Navigated to Disney+ login page.")
# Wait for the page to be fully rendered (e.g., wait for the login form to appear)
print(f"{current_time()} Waiting for page to fully render...")
wait = WebDriverWait(driver, 10)
username_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[id='email']")))
print(f"{current_time()} Page rendered, moving on after 1 second...")
# Wait for 1 second
time.sleep(1)
if username_flag:
# Prompt user for username (email)
print(f"{current_time()} Prompting for Disney+ username.")
username = get_user_input("Enter your Disney+ username (email address): ")
else:
print(f"{current_time()} Reusing previous Disney+ username.")
# Find the username field and enter the username
username_field = driver.find_element(By.CSS_SELECTOR, "[id='email']")
username_field.send_keys(username)
print(f"{current_time()} Entered username. Waiting 1 second...")
# Wait for 1 second
time.sleep(1)
# Find the continue button and click it
continue_button = driver.find_element(By.CSS_SELECTOR, "[data-testid='continue-btn']")
continue_button.click()
print(f"{current_time()} Clicked continue button. Waiting 5 seconds...")
# Wait for 5 seconds
time.sleep(5)
# Checking for Username success
print(f"{current_time()} Checking for username success...")
resolved_url = driver.current_url
if resolved_url not in username_urls:
print(f"{current_time()} Login (Username) failed, requesting next steps...")
retry_option = get_user_input_select("Login (Username) failed. Do you want to try again?", ["Yes, with same credentials", "Yes, with new credentials", "No, exit"])
if retry_option == "No, exit":
print(f"{current_time()} Exiting...")
retry_option = None
chrome_quit(driver)
exit()
elif retry_option == "Yes, with same credentials":
print(f"{current_time()} Retrying with same credentials...")
username_flag = False
password_flag = False
retry_option = None
chrome_quit(driver)
continue
elif retry_option == "Yes, with new credentials":
print(f"{current_time()} Retrying with new credentials...")
username_flag = True
password_flag = True
retry_option = None
chrome_quit(driver)
continue
else:
print(f"{current_time()} Invalid option. Please select a valid option.")
else:
print(f"{current_time()} Username successfully entered. Continuing login process...")
# Checking for Password or Verification request
print(f"{current_time()} Checking for Password or Verification request...")
password_or_verification_url = driver.current_url
if driver.current_url == "https://www.disneyplus.com/identity/login/enter-password":
print(f"{current_time()} Password request found.")
if password_flag:
# Prompt user for password (masked input)
print(f"{current_time()} Prompting for Disney+ password.")
password = get_user_input_masked("Enter your Disney+ password: ")
else:
print(f"{current_time()} Reusing previous Disney+ password.")
# Find the password field and enter the password
password_field = driver.find_element(By.CSS_SELECTOR, "[id='password']")
password_field.send_keys(password)
print(f"{current_time()} Entered password. Waiting 1 second...")
# Wait for 1 second
time.sleep(1)
# Find the login button and click it
login_button = driver.find_element(By.CSS_SELECTOR, "[type='submit']")
login_button.click()
print(f"{current_time()} Clicked password button. Waiting 5 seconds...")
# Wait for 5 seconds
time.sleep(5)
elif driver.current_url == "https://www.disneyplus.com/identity/login/enter-passcode":
print(f"{current_time()} Verification code request found.")
verification_code = get_user_input("Enter the six-digit verification code: ")
time.sleep(1)
actions = ActionChains(driver)
actions.send_keys(verification_code).perform()
# Wait for 1 second
time.sleep(1)
# Find the continue button and click it
verification_button = driver.find_element(By.CSS_SELECTOR, "[data-testid='continue-btn']")
verification_button.click()
print(f"{current_time()} Clicked verification button. Waiting 5 seconds...")
# Wait for 5 seconds
time.sleep(5)
else:
print(f"{current_time()} Neither Password nor Verification Requested.")
# Check if login was successful (webpage changed)
print(f"{current_time()} Checking login success...")
if driver.current_url != password_or_verification_url:
print(f"{current_time()} Login successful!")
break
else:
print(f"{current_time()} Login failed, requesting next steps...")
retry_option = get_user_input_select("Login failed. Do you want to try again?", ["Yes, with same credentials", "Yes, with new credentials", "Yes, with same username and new password", "No, exit"])
if retry_option == "No, exit":
print(f"{current_time()} Exiting...")
retry_option = None
chrome_quit(driver)
exit()
elif retry_option == "Yes, with same credentials":
print(f"{current_time()} Retrying with same credentials...")
username_flag = False
password_flag = False
retry_option = None
chrome_quit(driver)
continue
elif retry_option == "Yes, with new credentials":
print(f"{current_time()} Retrying with new credentials...")
username_flag = True
password_flag = True
retry_option = None
chrome_quit(driver)
continue
elif retry_option == "Yes, with same username and new password":
print(f"{current_time()} Retrying with same username and new password...")
username_flag = False
password_flag = True
retry_option = None
chrome_quit(driver)
continue
else:
print(f"{current_time()} Invalid option. Please select a valid option.")
# Define the successful URLs (add more if needed)
successful_urls = [
"https://www.disneyplus.com/home",
"https://www.disneyplus.com/select-profile" #,
# Add additional condition URLs here
]
while True:
if driver.current_url not in successful_urls:
print(f"{current_time()} Disney+ is having a loading issue. Trying again in 5 seconds...")
time.sleep(5) # Wait for 5 seconds
continue
else:
print(f"{current_time()} Disney+ has moved to next screen.")
break
# Check to see if Profile selection required
print(f"{current_time()} Checking for Profile selection request after waiting 5 seconds...")
time.sleep(5) # Wait for 5 seconds
if driver.current_url == "https://www.disneyplus.com/select-profile":
print(f"{current_time()} Profile selection request found. Clicking on first available...")
profile_button = driver.find_element(By.CSS_SELECTOR, "[data-testid='selected-avatar-image']")
profile_button.click()
print(f"{current_time()} Clicked on a Profile. Waiting 5 seconds...")
time.sleep(5) # Wait for 5 seconds
else:
print(f"{current_time()} No Profile selection found. Moving on...")
# Pick directory for .strmlnk files
print(f"{current_time()} Select a parent directory where Hulu Stream Links are...")
base_directory = get_directory()
print(f"{current_time()} Selected directory is {base_directory}")
print(f"{current_time()} Beginning Stream Link check...")
# Define the condition URLs (add more if needed)
condition_urls = [
"https://www.disneyplus.com/home" #,
# Add additional condition URLs here
]
# Recursively check each .strmlnk file
for root, _, files in os.walk(base_directory):
for file in files:
if file.endswith(".strmlnk"):
file_path = os.path.join(root, file)
with open(file_path, "r") as f:
url = f.read().strip()
exception_test = False
# Replace "hulu.com/watch" with "disneyplus.com/play" in the URL
modified_url = url.replace("hulu.com/watch", "disneyplus.com/play")
# Resolve the URL
try:
driver.get(modified_url)
# Wait for 5 seconds
time.sleep(5)
print(f"{current_time()} {modified_url} resolved to {driver.current_url}")
resolved_url = driver.current_url
except Exception as e:
# Handle HTTP error (e.g., 404)
resolved_url = str(e)
exception_test = True
if resolved_url in condition_urls or exception_test:
# Condition met: do not modify file
print(f"{current_time()} Maintained Hulu Stream Link for {file_path}")
else:
# Condition unmet: modify file
with open(file_path, "w") as f:
f.write(modified_url)
print(f"{current_time()} Changed Stream Link from Hulu to Disney+ for {file_path}")
# Other Actions
chrome_quit(driver)
print(f"{current_time()} End of current commands.")
quit()
if __name__ == "__main__":
main()
The above script does Part 2: Conversion. Based upon the the user selected directory, it will go through all the .strmlnk files and test the links against Disney+ by changing "hulu.com/watch
" to "disneyplus.com/play
" in a logged in session based upon the user's entered credentials. This gives the correct results for all Movies and TV Shows. Unlike the cleanup process above, this works in headless (non-visible) mode, so most of this is just happening in the background.
EDIT NOTE: Disney+ changed the login process after the original post of this. It is now updated to deal with that and a variety of other issues that may come up. As such, the directions below may not be exact as to what you'd see.
Running the process, you can see it launching Chrome and starting the login procedure:
2024-04-03 16:26:13.353269: System Identified as Windows. Setting Chrome Executable Path based on this...
2024-04-03 16:26:13.353269: Chrome Executable Path found at C:\Program Files\Google\Chrome\Application\chrome.exe.
DevTools listening on ws://127.0.0.1:58518/devtools/browser/2c6bfc5b-5794-4980-8c33-7b15b78145d2
2024-04-03 16:26:14.777033: Chrome session is running in the background.
2024-04-03 16:26:16.056738: Navigated to Disney+ login page.
2024-04-03 16:26:16.057739: Waiting for page to fully render...
2024-04-03 16:26:18.145867: Page rendered, moving on after 1 second...
[0403/162619.108:INFO:CONSOLE(0)] "Third-party cookie will be blocked. Learn more in the Issues tab.", source: https://www.disneyplus.com/identity/login/enter-email (0)
2024-04-03 16:26:19.158319: Prompting for Disney+ username.
The user inputs their username. For systems that do not support popups, all inputs are done on the command line.
The program then uses the input and moves on to various tests and the next steps.
2024-04-03 16:26:24.851620: Entered username. Waiting 1 second...
2024-04-03 16:26:25.916773: Clicked continue button. Waiting 5 seconds...
[0403/162626.048:INFO:CONSOLE(0)] "Third-party cookie will be blocked. Learn more in the Issues tab.", source: https://www.disneyplus.com/identity/login/enter-email (0)
2024-04-03 16:26:30.918192: Checking for username success...
2024-04-03 16:26:30.921209: Username successfully entered. Continuing login process...
2024-04-03 16:26:30.921209: Prompting for Disney+ password.
Eventually it will reach the point needed for a password or (more likely) a verification code.
And again, there are many checks and things done once that is entered.
2024-04-03 16:26:35.534005: Entered password. Waiting 1 second...
2024-04-03 16:26:36.588367: Clicked login button. Waiting 5 seconds...
[0403/162637.077:INFO:CONSOLE(0)] "Third-party cookie will be blocked. Learn more in the Issues tab.", source: https://www.disneyplus.com/onboarding? (0)
2024-04-03 16:26:41.589052: Checking for verification code request...
2024-04-03 16:26:41.600271: No verification code requested. Checking login success...
2024-04-03 16:26:41.605086: Login successful!
2024-04-03 16:26:41.605086: Checking for Profile selection request after waiting 5 seconds...
2024-04-03 16:26:46.610816: Profile selection request found. Clicking on first available...
2024-04-03 16:26:46.671775: Clicked on a Profile. Waiting 5 seconds...
2024-04-03 16:26:51.673849: Select a parent directory where Hulu Stream Links are...
Finally, the user selects the parent directory to all of the Hulu Stream Link files.
At this point, the real fun begins! It will go through every Stream Link and see if it exists on Disney+. If it does, the Stream Link is updated. If it isn't, the Stream Link is left alone.
2024-04-03 16:26:55.442398: Selected directory is C:/Temp/Test/HtoD
2024-04-03 16:26:55.443407: Beginning Stream Link check...
2024-04-03 16:27:02.425655: https://www.disneyplus.com/play/c4bda460-f3a1-4f91-9b2f-2eac8b82fafb resolved to https://www.disneyplus.com/play/c4bda460-f3a1-4f91-9b2f-2eac8b82fafb
2024-04-03 16:27:02.734570: Changed Stream Link from Hulu to Disney+ for C:/Temp/Test/HtoD\Becoming Bond (2017).strmlnk
2024-04-03 16:27:08.219809: https://www.disneyplus.com/play/d2ca67b9-0e07-4448-bede-115daf26cf0d resolved to https://www.disneyplus.com/home
2024-04-03 16:27:08.219809: Maintained Hulu Stream Link for C:/Temp/Test/HtoD\Huluween Dragstravaganza (2022).strmlnk
2024-04-03 16:27:13.463093: https://www.disneyplus.com/play/025e53a2-4539-4784-a42a-8b295b134408 resolved to https://www.disneyplus.com/home
2024-04-03 16:27:13.470102: Maintained Hulu Stream Link for C:/Temp/Test/HtoD\I Kill Giants (2017).strmlnk
2024-04-03 16:27:18.850521: https://www.disneyplus.com/play/df471734-b23c-4d66-9124-960173810541 resolved to https://www.disneyplus.com/play/df471734-b23c-4d66-9124-960173810541
2024-04-03 16:27:18.939632: Changed Stream Link from Hulu to Disney+ for C:/Temp/Test/HtoD\Scarface (1983).strmlnk
In this example, you can see the files as they were before, and what they looked like after the process was run:
Before...
After...
If you have a non-Hulu Stream Link in the directory, you may see something like this:
2024-04-03 16:51:13.075436: https://www.kanopy.com/video/13448336 resolved to https://www.kanopy.com/en/product/13448336
2024-04-03 16:51:13.086212: Changed Stream Link from Hulu to Disney+ for C:/Temp/Test/HtoD\Raiders! The Story of the Greatest Fan Film Ever Made (2015).strmlnk
However, although it updates the file, it only puts the same link back in:
I've decided it is too minor of an issue to address as it causes no harm.
After that, it is just cleanup and closeout.
2024-04-03 16:27:18.939632: Closing Chrome session...
2024-04-03 16:27:21.051067: Chrome session closed.
2024-04-03 16:27:21.051067: End of current commands.
Enjoy!