ProtoType_Stream_Link_Maker.py
import os
import re
import urllib.parse
import datetime
def current_time():
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + ": "
# Install simple-justwatch-python-api (https://github.com/Electronic-Mango/simple-justwatch-python-api) if not already installed
try:
from simplejustwatchapi.justwatch import search, details
except ImportError:
if os.name == "posix": # Unix-like systems (including macOS)
os.system("pip3 install simple-justwatch-python-api")
else:
os.system("pip install simple-justwatch-python-api")
# Check if the OS allows popup boxes
try:
import tkinter as tk
from tkinter import filedialog
from tkinter import simpledialog
from tkinter import messagebox
from tkinter import Tk
except ImportError:
# If popup boxes are not allowed, do not import tkinter
pass
# Get a directory path from the user using a file dialog or command line.
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.")
print("")
print(f"{current_time()} Chosen Directory: {directory_path}")
print("")
return directory_path
# Get user input from a popup box or command line.
def get_user_input(prompt):
while True:
if os.name == "posix": # Unix-like systems (including macOS)
user_input = input(prompt)
else:
try:
import tkinter as tk
from tkinter import simpledialog, messagebox
root = tk.Tk()
root.withdraw() # Hide the main window
user_input = simpledialog.askstring("Input", prompt)
root.destroy() # Close the pop-up window
except ImportError:
user_input = input(prompt)
# Validate if a value was entered
if user_input is not None and user_input.strip():
return user_input.strip() # Remove leading/trailing spaces
else:
print(f"{current_time()} Error: Please provide a valid input.")
# Get user selection from buttons or command line list.
def get_button_selection(prompt, options):
while True:
if os.name == "posix": # Unix-like systems (including macOS)
print(prompt)
for i, option in enumerate(options, start=1):
print(f"{i}. {option}")
try:
choice = int(input("Enter the number corresponding to your choice: "))
if 1 <= choice <= len(options):
return options[choice - 1]
else:
print("Invalid choice. Please try again.")
except ValueError:
print("Invalid input. Please enter a valid number.")
else:
try:
import tkinter as tk
root = tk.Tk()
root.withdraw() # Hide the main window
# Create a dialog box with radio buttons for each option
dialog = tk.Toplevel(root)
dialog.title("Select an Option")
# Calculate the height of the dialog based on the number of options
dialog_height = 150 + len(options) * 30
dialog.geometry(f"300x{dialog_height}")
# Add the prompt to the dialog
tk.Label(dialog, text=prompt).pack()
# Create a variable to store the selected option
selected_option = None
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
if selected_option:
return selected_option
else:
print(f"{current_time()} Error: Please select an option.")
except ImportError:
return input(prompt)
# Remove invalid characters (e.g., colons, slashes, etc.)
def sanitize_name(name):
sanitized = re.sub(r'[\\/:*?"<>|]', '', name)
return sanitized
# Create a directory if it doesn't exist.
def create_directory(directory_path):
if not os.path.exists(directory_path):
os.makedirs(directory_path)
# Create season folders with proper naming.
def create_season_folders(base_directory, starting_season, ending_season):
for season_number in range(starting_season, ending_season + 1):
season_folder_name = f"Season {season_number:02}"
season_folder_path = os.path.join(base_directory, season_folder_name)
create_directory(season_folder_path)
# Create episode files within a season folder.
def create_episode_files(season_folder, episode_count, prefix, episode_url):
for episode_number in range(1, episode_count + 1):
episode_name = f"S{season_folder[-2:]}E{episode_number:02}"
if prefix != "None":
episode_name = f"{prefix} {episode_name}"
episode_name += ".strmlnk"
episode_file_path = os.path.join(season_folder, episode_name)
print(f"{current_time()} Creating: {episode_file_path}")
if not os.path.exists(episode_file_path):
with open(episode_file_path, 'w') as file:
file.write(episode_url)
# Create movie files
def create_movie_file(movie_directory, movie_name_base, movie_url):
movie_name = f"{movie_name_base}"
movie_name += ".strmlnk"
movie_file_path = os.path.join(movie_directory, movie_name)
print(f"{current_time()} Creating: {movie_file_path}")
if not os.path.exists(movie_file_path):
with open(movie_file_path, 'w') as file:
file.write(movie_url)
# Find all the Streaming Services for the selected Program
def extract_offer_info(offers):
result = []
for offer in offers:
name = offer.package.name
price_string = offer.price_string if offer.price_string is not None else "$0.00"
url = urllib.parse.unquote(offer.url) # Decode the URL
result.append({"name": name, "price_string": price_string, "url": url})
return result
# Input the country code to search
def get_country_code():
while True:
country_code_input = input("Enter a 2-letter country code (or press Enter for default 'US'): ")
if not country_code_input:
return "US" # Default value
elif len(country_code_input) == 2 and country_code_input.isalpha():
return country_code_input.upper()
else:
print("")
print("Invalid input. Please enter a valid 2-letter country code.")
print("")
# Input the language code to search
def get_language_code():
while True:
language_code_input = input("Enter a 2-letter language code (or press Enter for default 'en'): ")
if not language_code_input:
return "en" # Default value
elif len(language_code_input) == 2 and language_code_input.isalpha():
return language_code_input.lower()
else:
print("")
print("Invalid input. Please enter a valid 2-letter language code.")
print("")
# Input the number of search results to return
def get_num_results():
while True:
try:
num_results_input = input("Enter the number of search results (or press Enter for default '9'): ")
if not num_results_input:
return 9 # Default value
num_results = int(num_results_input)
if num_results > 0:
return num_results
else:
print("")
print("Please enter a positive integer.")
print("")
except ValueError:
print("")
print("Invalid input. Please enter a valid positive integer.")
print("")
# Main Function
def main():
print("")
# Input the country code
country_code = get_country_code()
print("")
print(f"{current_time()} Selected country code: {country_code}")
print("")
# Input the language code
language_code = get_language_code()
print("")
print(f"{current_time()} Selected language code: {language_code}")
print("")
# Input the number of search results
num_results = get_num_results()
print("")
print(f"{current_time()} Selected number of search results: {num_results}")
print("")
# Loop to run again
while True:
# Search for a program
while True:
print("")
program_search = input("Enter the program to search for: ")
program_search_base = search(program_search, country_code, language_code, num_results, True)
# Extract the titles, release years, and object types from the response
program_search_results = [(entry.title, entry.release_year, entry.object_type, entry.entry_id) for entry in program_search_base]
# Display a list of options to the user
print("")
print("Select an option (or enter 0 to cancel and retry):")
print("")
for i, (title, release_year, object_type, entry_id) in enumerate(program_search_results, start=1):
print(f"{i}. {title} ({release_year}) | {object_type}")
while True:
# Get user input for the selected option
print("")
program_search_selected = input("Enter the number of the option you want to choose: ")
print("")
# Validate user input
try:
program_search_index = int(program_search_selected) - 1
if program_search_index == -1:
print("")
print(f"{current_time()} Selection canceled. Returning to the top.")
print("")
retry_step = 0
break
elif 0 <= program_search_index < len(program_search_base):
program_search_selected_entry_id = program_search_base[program_search_index].entry_id
program_search_selected_object_type = program_search_base[program_search_index].object_type
program_search_selected_program = f"{program_search_base[program_search_index].title} ({program_search_base[program_search_index].release_year})"
print("")
print(f"{current_time()} You selected: {program_search_selected_program}")
print(f"{current_time()} The selected program is a: {program_search_selected_object_type}")
print(f"{current_time()} The corresponding entry ID is: {program_search_selected_entry_id}")
print("")
retry_step = 1
break
else:
print("")
print(f"{current_time()} Invalid option. Please choose a valid number.")
print("")
retry_step = 3
continue
except ValueError:
print("")
print(f"{current_time()} Invalid input. Please enter a valid number.")
print("")
retry_step = 3
continue
if retry_step == 0:
continue
elif retry_step == 1:
break
# Select a Streaming Services Option
while True:
# Get the Streaming Services Offer based upon the selected Program
services_search_base = details(program_search_selected_entry_id, country_code, language_code, True)
services_search_results = extract_offer_info(services_search_base.offers)
if not services_search_results:
print(f"{current_time()} {program_search_base[program_search_index].title} is not currently available on any Straming Service.")
print("")
run_create = False
break
else:
run_create = True
# Display a list of Streaming Services Options to the user
print("")
print("Select a Streaming Service (or enter 0 to cancel and exit):")
print("")
for i, info in enumerate(services_search_results, start=1):
print(f"{i}. {info['name']} ({info['price_string']})")
while True:
# Get user input for the selected Streaming Services Option
print("")
services_search_selected = input("Enter the number of the Streaming Service Option you want to choose: ")
print("")
# Validate user input
try:
services_search_index = int(services_search_selected) - 1
if services_search_index == -1:
print("")
print(f"{current_time()} Cancelling...")
print("")
run_create = False
retry_step = 1
break
elif 0 <= services_search_index < len(services_search_results):
services_search_selected_service = services_search_results[services_search_index]
services_search_selected_url = services_search_selected_service['url'] # Save the URL
print("")
print(f"{current_time()} You selected: {services_search_selected_service['name']} ({services_search_selected_service['price_string']})")
print(f"{current_time()} The corresponding URL is: {services_search_selected_url}")
print("")
retry_step = 1
break
else:
print("")
print(f"{current_time()} Invalid option. Please choose a valid number.")
print("")
retry_step = 3
continue
except ValueError:
print("")
print(f"{current_time()} Invalid input. Please enter a valid number.")
print("")
retry_step = 3
continue
if retry_step == 0:
continue
elif retry_step == 1:
break
# Create directories (if ncessary) and file(s)
if run_create:
program_name = sanitize_name(program_search_selected_program)
print("")
print("Select a top level directory for the Stream Link(s):")
print("")
base_directory = get_directory()
if program_search_selected_object_type == "SHOW":
prefix_options = ["None", "(DUB)", "(SUB)"]
selected_prefix = get_button_selection("Select a Prefix option for the episodes:", prefix_options)
starting_season = int(get_user_input("Enter the Starting Season Number: "))
ending_season = int(get_user_input("Enter the Ending Season Number: "))
create_directory(os.path.join(base_directory, program_name))
create_season_folders(os.path.join(base_directory, program_name), starting_season, ending_season)
for season_number in range(starting_season, ending_season + 1):
season_folder_name = f"Season {season_number:02}"
season_folder_path = os.path.join(base_directory, program_name, season_folder_name)
episode_count = int(get_user_input(f"Number of Episodes for {season_folder_name}: "))
create_episode_files(season_folder_path, episode_count, selected_prefix, services_search_selected_url)
print(f"{current_time()} Directory structure and files created successfully.")
elif program_search_selected_object_type == "MOVIE":
create_movie_file(base_directory, program_name, services_search_selected_url)
print(f"{current_time()} File created successfully")
else:
print("")
print(f"{current_time()} ERROR: Unknown Program Type")
print("")
# Check to re-run
rerun_options = ["Yes", "No"]
rerun_select = get_button_selection("Do you wish to run again? (Yes/No):", rerun_options)
if rerun_select == "Yes":
continue
elif rerun_select == "No":
break
else:
print("")
print(f"{current_time()} Error encountered, exiting...")
print("")
break
# Other Actions
print("")
print(f"{current_time()} End of current commands.")
print("")
exit()
if __name__ == "__main__":
main()
The above script is a prototype proof-of-concept for the program search, selection of a program, and creation of expected Stream Link files. This is a fully functioning application with the following caveats:
-
TV Shows are not getting individual episode URLs (yet). The API that is used to access the base data needs an update before that is possible. Also, given this current situation, I have opted not to get more granular with episode selection at this time.
-
The Stream Links that are created might not be valid. I have especially noticed an issue with Disney+ that I'm working on a higher level approach to address. Although I know how to solve the issue for Disney+, I do not want to create fixes for individual services as it will be way too much to manage.
-
In a similar vein, although almost all Hulu programs are available on Disney+, the underlying data provider is not up to date on this fact and will usually only offer Hulu instead of Disney+ and Hulu. Again, I already have a resolution for this, but would rather avoid dealing with a specific service this way.
That all said, here's how it looks:
At the beginning, you are prompted for certain conditions for searching. If you want the default options (United States
, English
, 9 Results
), then you can just hit enter and parse past them.
At this point you can search for whatever you are looking for, not case sensitive. If you get enter-happy or forget to put anything, it will just return the most popular programs right now. Otherwise, you will see this:
From here, you can select 0
to search again, or select one of the numbers. If you mistype, it will warn you and let you select again until you make a valid choice. In this case, I'm going to select the first option:
The application confirms your choice and then offers you everywhere it is available and at what price (assuming you have a subscription for the $0.00
options). If it is not available anywhere, you will be told so and forced to the end of the routine (more on this later). Otherwise, similar to the prior step, you must select a valid option to move on, or 0
to cancel and maybe try again. I'm going to choose 9
for YouTube:
A confirmation appears for the selection and the URL. At this point, you would select a the directory where you want the file to be created (or, if a show, the parent directory). Note that because I am using Windows I am getting a pop-up selection box, but command line only systems would have to type in a directory path. This is the case for all popups below. Later, these will not exist and instead will default into their own directory in the Imports directory under Channels.
After selecting the folder, everything progresses as normal:
As can be seen above, a file is created automatically with the correct name and Stream Link inside!
Afterwards, you'll be asked if you want to run again and do another search.
This will bring you back to the search menu. If you do any of the earlier opportunities to cancel, you will also end up here. Now that we are here, let's go through a TV Show. Back to the prior test, I'm going to select 6
.
Now I get prompts for prefixes, number of seasons, and number of episodes per season. In the future, this will not be a selection at this point but part of the management system:
This all results in individual files being created:
Take note that the program had an invalid character (a colon :
) that was removed. This would happen with a movie, as well.
And that's it! With this, we have proven that we can search a 3rd party source and create Stream Links based upon it. I'll also be looking into the reverse of this with selecting a Streaming Service and being presented with the newest additions to see if you want to create the Stream Links.
Enjoy!