From 7c94fd23d71b5a718411743f311a3bdc8142a2ad Mon Sep 17 00:00:00 2001
From: TPD94 <>
Date: Fri, 17 Nov 2023 01:26:42 -0500
Subject: [PATCH] TPD-Ripper
---
Licence_cURL.py | 0
TPD-Ripper.py | 813 +++++++++++++++++++++++++++++++++++++++++++++++
requirements.txt | 2 +
3 files changed, 815 insertions(+)
create mode 100644 Licence_cURL.py
create mode 100644 TPD-Ripper.py
create mode 100644 requirements.txt
diff --git a/Licence_cURL.py b/Licence_cURL.py
new file mode 100644
index 0000000..e69de29
diff --git a/TPD-Ripper.py b/TPD-Ripper.py
new file mode 100644
index 0000000..0a74c2f
--- /dev/null
+++ b/TPD-Ripper.py
@@ -0,0 +1,813 @@
+# Import needed dependencies
+import asyncio
+import requests
+import sqlite3
+import re
+import os
+from os import urandom
+from tqdm import tqdm
+import subprocess
+import uuid
+import glob
+import zipfile
+import shutil
+import base64
+import json
+import argparse
+import Licence_cURL
+
+# Initialize argparse and set variable
+parser = argparse.ArgumentParser(description="Options for manual download")
+# Video argument
+parser.add_argument('-v', '--video-res', help="Desired video resolution (by width)", metavar="", default=None)
+# Audio argument
+parser.add_argument('-alang', '--audio-lang', help="Desired audio language", metavar="", default=None)
+# Subtitle argument
+parser.add_argument('-slang', '--subtitle-lang', help="Desired subtitle language", metavar="", default=None)
+# Assign the args a variable
+args = parser.parse_args()
+
+# Get current working directory
+main_directory = os.getcwd()
+
+# Get directories in the current working directory
+directories_in_current_working_directory = os.listdir(fr'{main_directory}')
+
+# Set required directories
+required_directories = ['binaries', 'temp', 'downloads', 'keys']
+
+# Create directories in current working directory if they do not exist
+for directory in required_directories:
+ if directory not in directories_in_current_working_directory:
+ os.makedirs(f'{main_directory}\\{directory}')
+
+# Create database and table for local key caching if they don't exist
+dbconnection = sqlite3.connect(f"{main_directory}\\keys\\database.db")
+dbcursor = dbconnection.cursor()
+dbcursor.execute('CREATE TABLE IF NOT EXISTS "DATABASE" ( "pssh" TEXT, "keys" TEXT, PRIMARY KEY("pssh") )')
+dbconnection.close()
+
+# Assigning and checking all required external binaries exist
+required_binaries = ["n_m3u8dl-re.exe", "mp4decrypt.exe", "ffmpeg.exe"]
+
+# Check if the required binaries exist, if not, download them.
+for binary in required_binaries:
+ if not os.path.isfile(f"{main_directory}\\binaries\\{binary}"):
+ save_path = f"{main_directory}\\temp"
+ if binary == "ffmpeg.exe":
+ ffmpeg_download = requests.get("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", stream=True)
+ total_size = int(ffmpeg_download.headers.get('content-length', 0))
+ with open(f"{save_path}\\ffmpeg.zip", 'wb') as download:
+ with tqdm(total=total_size, unit='B', unit_scale=True, desc="Downloading ffmpeg.zip") as progress_bar:
+ for data in ffmpeg_download.iter_content(chunk_size=1024):
+ download.write(data)
+ progress_bar.update(len(data))
+ with zipfile.ZipFile(f"{main_directory}\\temp\\ffmpeg.zip", "r") as ffmpeg_zip:
+ file_count = len(ffmpeg_zip.infolist())
+ with tqdm(total=file_count, unit='file', desc="Extracting ffmpeg.zip") as unzip_progress_bar:
+ for file in ffmpeg_zip.infolist():
+ ffmpeg_zip.extract(file, path=f"{main_directory}\\temp")
+ unzip_progress_bar.update(1)
+ shutil.copy2(f"{main_directory}\\temp\\ffmpeg-master-latest-win64-gpl\\bin\\ffmpeg.exe", f"{main_directory}\\binaries")
+ os.remove(f"{main_directory}\\temp\\ffmpeg.zip")
+ shutil.rmtree(f"{main_directory}\\temp\\ffmpeg-master-latest-win64-gpl")
+ print()
+ elif binary == "mp4decrypt.exe":
+ mp4decrypt_download = requests.get("https://www.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-639.x86_64-microsoft-win32.zip", stream=True)
+ total_size = int(mp4decrypt_download.headers.get('content-length', 0))
+ with open(f"{save_path}\\mp4decrypt.zip", 'wb') as download:
+ with tqdm(total=total_size, unit='B', unit_scale=True, desc="Downloading mp4decrypt.zip") as progress_bar:
+ for data in mp4decrypt_download.iter_content(chunk_size=1024):
+ download.write(data)
+ progress_bar.update(len(data))
+ with zipfile.ZipFile(f"{main_directory}\\temp\\mp4decrypt.zip", "r") as mp4decrypt_zip:
+ file_count = len(mp4decrypt_zip.infolist())
+ with tqdm(total=file_count, unit='file', desc="Extracting mp4decrypt.zip") as unzip_progress_bar:
+ for file in mp4decrypt_zip.infolist():
+ mp4decrypt_zip.extract(file, path=f"{main_directory}/temp")
+ unzip_progress_bar.update(1)
+ shutil.copy2(f"{main_directory}\\temp\\Bento4-SDK-1-6-0-639.x86_64-microsoft-win32\\bin\\mp4decrypt.exe", f"{main_directory}\\binaries")
+ os.remove(f"{main_directory}\\temp\\mp4decrypt.zip")
+ shutil.rmtree(f"{main_directory}\\temp\\Bento4-SDK-1-6-0-639.x86_64-microsoft-win32")
+ print()
+ elif binary == "n_m3u8dl-re.exe":
+ n_m3u8dl_re_download = requests.get("https://github.com/nilaoda/N_m3u8DL-RE/releases/download/v0.1.6-beta/N_m3u8DL-RE_Beta_win-x64_20230412.zip", stream=True)
+ total_size = int(n_m3u8dl_re_download.headers.get('content-length', 0))
+ with open(f"{save_path}\\n_m3u8dl-re.zip", 'wb') as download:
+ with tqdm(total=total_size, unit='B', unit_scale=True, desc="Downloading n_m3u8dl-re.zip") as progress_bar:
+ for data in n_m3u8dl_re_download.iter_content(chunk_size=1024):
+ download.write(data)
+ progress_bar.update(len(data))
+ with zipfile.ZipFile(f"{main_directory}\\temp\\n_m3u8dl-re.zip", "r") as nm3u8dl_re_zip:
+ file_count = len(nm3u8dl_re_zip.infolist())
+ with tqdm(total=file_count, unit='file', desc="Extracting n_m3u8dl-re.zip") as unzip_progress_bar:
+ for file in nm3u8dl_re_zip.infolist():
+ nm3u8dl_re_zip.extract(file, path=f"{main_directory}\\temp")
+ unzip_progress_bar.update(1)
+ shutil.copy2(f"{main_directory}\\temp\\N_m3u8DL-RE_Beta_win-x64\\N_m3u8DL-RE.exe", f"{main_directory}/binaries")
+ os.remove(f"{main_directory}\\temp\\n_m3u8dl-re.zip")
+ shutil.rmtree(f"{main_directory}\\temp\\N_m3u8DL-RE_Beta_win-x64")
+ print()
+
+# Assign binaries a variable
+n_m3u8dl_re = f'{main_directory}\\binaries\\{required_binaries[0]}'
+mp4decrypt = f'{main_directory}\\binaries\\{required_binaries[1]}'
+ffmpeg = f'{main_directory}\\binaries\\{required_binaries[2]}'
+
+# Check if API key exists, if not create file and ask for key
+if not os.path.isfile(f"{main_directory}\\api-key.txt"):
+ with open(f'{main_directory}\\api-key.txt', 'w') as api_key_text:
+ api_key = input(f"\nIf you have an API key please input it now: ")
+ api_key_text.write(api_key)
+
+# Get API key if file already exists
+with open(f'{main_directory}\\api-key.txt') as api_key:
+ api_key = api_key.readline()
+
+# Print out API key
+print(f"\nYour API key: {api_key}")
+
+
+# Define MPD / m3u8 PSSH parser
+async def manifest_pssh_parse(manifest_url):
+ manifest = manifest_url
+ try:
+ response = requests.get(manifest)
+ except:
+ pssh = input("Couldn't retrieve manifest, please input PSSH: ")
+ return pssh
+ try:
+ matches = re.finditer(r'(.*))>(?P(.*))', response.text)
+ pssh_list = []
+
+ for match in matches:
+ if match.group and not match.group("pssh") in pssh_list and len(match.group("pssh")) < 300:
+ pssh_list.append(match.group("pssh"))
+
+ if len(pssh_list) < 1:
+ matches = re.finditer(r'URI="data:text/plain;base64,(?P(.*))"', response.text)
+ for match in matches:
+ if match.group("pssh") and match.group("pssh").upper().startswith("A") and len(match.group("pssh")) < 300:
+ pssh_list.append(match.group("pssh"))
+ return f'{pssh_list[0]}'
+ except:
+ pssh = input("Couldn't find PSSH in manifest, please input PSSH")
+ return pssh
+
+
+# Define key cache function
+async def key_cache(pssh: str, db_keys: str):
+ dbconnection = sqlite3.connect(f"{main_directory}\\keys\\database.db")
+ dbcursor = dbconnection.cursor()
+ dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?)", (pssh, db_keys))
+ dbconnection.commit()
+ dbconnection.close()
+
+
+# Define retrieve keys remotely function
+async def retrieve_keys_remotely(pssh: str = None, license_url: str = None):
+ # Set the API URL to a pywidevine served API
+ api_url = "https://api.cdm-project.com"
+
+ # Set the API device you want to use for decryption
+ api_device = "CDM"
+
+ # Get the PSSH
+ pssh = pssh
+
+ # Get the license URL
+ license_url = license_url
+
+ # Set your API key to be sent with headers
+ x_headers = {
+ "X-Secret-Key": api_key
+ }
+
+ # Open a session on your API device via your API key sent in headers
+ open_session = requests.get(url=f"{api_url}/{api_device}/open", headers=x_headers)
+
+ # Get the session ID
+ session_id = open_session.json()["data"]["session_id"]
+
+ # Set JSON data to send to create the license challenge
+ license_challenge_json_data = {
+ "session_id": session_id,
+ "init_data": pssh
+ }
+
+ # Create the license challenge
+ licence_challenge = requests.post(url=f"{api_url}/{api_device}/get_license_challenge/AUTOMATIC", headers=x_headers,
+ json=license_challenge_json_data)
+
+ # Retrieve the license message
+ license_message = licence_challenge.json()["data"]["challenge_b64"]
+
+ # Send the license challenge
+ license = requests.post(
+ headers=Licence_cURL.headers,
+ url=license_url,
+ data=base64.b64decode(license_message)
+ )
+
+ # Set JSON data to parse the license
+ parse_license_json_data = {
+ "session_id": session_id,
+ "license_message": f"{base64.b64encode(license.content).decode()}"
+ }
+
+ # Send the request to parse the license
+ requests.post(f"{api_url}/{api_device}/parse_license", json=parse_license_json_data, headers=x_headers)
+
+ # Retrieve the keys from the parsed license
+ get_keys = requests.post(f"{api_url}/{api_device}/get_keys/ALL", json={"session_id": session_id}, headers=x_headers)
+
+ # Set DB keys
+ db_keys = ''
+
+ # Set mp4decrypt keys
+ mp4decrypt_keys = []
+
+ # Iterate through key response, if signing key, ignore
+ for key in get_keys.json()["data"]["keys"]:
+ if not key["type"] == "SIGNING":
+ db_keys += f"{key['key_id']}:{key['key']}\n"
+ mp4decrypt_keys.append('--key')
+ mp4decrypt_keys.append(f"{key['key_id']}:{key['key']}\n")
+ await key_cache(pssh=pssh, db_keys=db_keys)
+
+ # Close the session
+ requests.get(f"{api_url}/{api_device}/close/{session_id}", headers=x_headers)
+
+ # Return keys if function is called from variable assignment
+ return db_keys, mp4decrypt_keys
+
+
+# Define retrieve keys remotely VDOCipher function
+async def retrieve_keys_remotely_vdocipher(mpd: str = None):
+ # Get URL from function
+ url = input(f"Video URL: ")
+
+ # Set the VDOCipher token headers
+ token_headers = {
+ 'accept': '*/*',
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
+ # Comment this line out if using for anything other than https://www.vdocipher.com/blog/2014/12/add-text-to-videos-with-watermark/
+ 'Origin': f"https://{urandom(8).hex()}.com",
+ }
+
+ # Set the token response
+ token_response = requests.get(url, cookies=Licence_cURL.cookies, headers=token_headers)
+ try:
+ otp_match = re.findall(r"otp: '(.*)',", token_response.text)[0]
+ playbackinfo_match = re.findall(r"playbackInfo: '(.*)',", token_response.text)[0]
+ except IndexError:
+ try:
+ otp_match = re.findall(r"otp=(.*)&", token_response.text)[0]
+ playbackinfo_match = re.findall(r"playbackInfo=(.*)", token_response.text)[0]
+ except IndexError:
+ print("\nAn error occurred while getting otp/playback")
+ exit()
+
+ # Set the video ID
+ video_id = json.loads(base64.b64decode(playbackinfo_match).decode())["videoId"]
+
+ # Set new token response (1)
+ token_response = requests.get(f'https://dev.vdocipher.com/api/meta/{video_id}', headers=token_headers)
+ try:
+ license_url = token_response.json()["dash"]["licenseServers"]["com.widevine.alpha"].rsplit(":", 1)[0]
+ mpd = token_response.json()["dash"]["manifest"]
+ except KeyError:
+ print("\n An error occurred while getting mpd/license url")
+
+ # Set new token response (2)
+ token_response = requests.get(mpd, headers=token_headers)
+
+ # Set API URL
+ api_url = "https://api.cdrm-project.com"
+
+ # Set API Device
+ api_device = "CDM"
+
+ # Retrieve PSSH
+ pssh = re.search(r"(.*)", token_response.text).group(1)
+
+ # Set API headers
+ x_headers = {
+ "X-Secret-Key": api_key
+ }
+
+ # Open API session
+ open_session = requests.get(url=f"{api_url}/{api_device}/open", headers=x_headers)
+
+ # Set the session ID
+ session_id = open_session.json()["data"]["session_id"]
+
+ # Send json data to get license challenge
+ license_challenge_json_data = {
+ "session_id": session_id,
+ "init_data": pssh
+ }
+
+ # Get the license challenge from PSSH
+ licence_challenge = requests.post(url=f"{api_url}/{api_device}/get_license_challenge/AUTOMATIC", headers=x_headers,
+ json=license_challenge_json_data)
+
+ # Set the final token
+ token = {
+ "otp": otp_match,
+ "playbackInfo": playbackinfo_match,
+ "href": url,
+ "tech": "wv",
+ "licenseRequest": licence_challenge.json()["data"]["challenge_b64"]
+ }
+
+ # Send challenge
+ license = requests.post(
+ url=license_url,
+ json={'token': f'{base64.b64encode(json.dumps(token).encode("utf-8")).decode()}'}
+ )
+
+ # Set the parsing JSON data
+ parse_license_json_data = {
+ "session_id": session_id,
+ "license_message": license.json()["license"]
+ }
+
+ # Send the parsing JSON data
+ requests.post(f"{api_url}/{api_device}/parse_license", json=parse_license_json_data, headers=x_headers)
+
+ # Get the keys
+ get_keys = requests.post(f"{api_url}/{api_device}/get_keys/ALL",
+ json={"session_id": session_id}, headers=x_headers)
+
+ # Set DB keys
+ db_keys = ''
+
+ # Set mp4 decrypt keys
+ mp4decrypt_keys = []
+
+ for key in get_keys.json()["data"]["keys"]:
+ if not key["type"] == "SIGNING":
+ db_keys += f"{key['key_id']}:{key['key']}\n"
+ mp4decrypt_keys.append('--key')
+ mp4decrypt_keys.append(f"{key['key_id']}:{key['key']}\n")
+ await key_cache(pssh=pssh, db_keys=db_keys)
+
+ # Close the session
+ requests.get(f"{api_url}/{api_device}/close/{session_id}", headers=x_headers)
+
+ # Return keys if function is called from variable assignment
+ return db_keys, mp4decrypt_keys
+
+
+# Define function for encrypted video download
+async def encrypted_video_download(manifest_url: str, res: str = None):
+ # Create the encrypted filename for easy use
+ random_encrypted_video_file_name = str(uuid.uuid4())
+
+ # List of n_m3u8dl-re video downloads commands for best 1280x720p video
+ video_nm3u8_720 = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-sv',
+ f'res="1280*":for=best',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_encrypted_video_file_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ # List of n_m3u8dl-re video downloads commands for best 1920x1080p video
+ video_nm3u8_1080 = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-sv',
+ f'res="1920*":for=best',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_encrypted_video_file_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ # List of n_m3u8dl-re video downloads commands for best manual res video
+ video_nm3u8_manual = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-sv',
+ f'res="{res}*":for=best',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_encrypted_video_file_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ # Check if res option has been passed, if not auto download 1080p/720p
+ if res is None:
+ # Run n_m3u8dl-re from above list for best 1080p video if available
+ subprocess.run(video_nm3u8_1080)
+ # Find the encrypted video file path and extension
+ try:
+ encrypted_video_file_path_and_name = \
+ glob.glob(f'{main_directory}\\downloads\\{random_encrypted_video_file_name}.*')[0]
+ return encrypted_video_file_path_and_name
+ # Add exception if video file isn't found / available in 1080p
+ except IndexError:
+ try:
+ # Run n_m3u8dl-re from above list for best 720p video if available
+ subprocess.run(video_nm3u8_720)
+ encrypted_audio_file_path_and_name = \
+ glob.glob(f'{main_directory}\\downloads\\{random_encrypted_video_file_name}.*')[0]
+ return encrypted_audio_file_path_and_name
+ # If 720/1080p isn't available, return None value
+ except:
+ return None
+ else:
+ # Run n_m3u8dl-re from above list for specified res
+ subprocess.run(video_nm3u8_manual)
+ try:
+ encrypted_video_file_path_and_name = \
+ glob.glob(f'{main_directory}\\downloads\\{random_encrypted_video_file_name}.*')[0]
+ return encrypted_video_file_path_and_name
+ except:
+ return None
+
+
+# Define function for encrypted audio download
+async def encrypted_audio_download(manifest_url: str, alang: str = None):
+ # Create the encrypted filename for easy use
+ random_encrypted_audio_file_name = str(uuid.uuid4())
+
+ # List of n_m3u8dl-re best english audio commands
+ audio_nm3u8_best_english = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-sa',
+ 'lang=en:for=best',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_encrypted_audio_file_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ # Create a general download in case english isn't available or properly tagged
+ audio_nm3u8_best_general = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-sa',
+ 'all',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_encrypted_audio_file_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ # List of commands if audio is selected
+ audio_nm3u8_manual = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-sa',
+ f'{alang}',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_encrypted_audio_file_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ if alang is None:
+ # Run n_m3u8dl-re from above list for best english audio
+ subprocess.run(audio_nm3u8_best_english)
+ try:
+ encrypted_audio_file_path_and_name = \
+ glob.glob(f'{main_directory}\\downloads\\{random_encrypted_audio_file_name}.*')[0]
+ return encrypted_audio_file_path_and_name
+ # If english is not found, download the best audio available
+ except:
+ try:
+ subprocess.run(audio_nm3u8_best_general)
+ encrypted_audio_file_path_and_name = \
+ glob.glob(f'{main_directory}\\downloads\\{random_encrypted_audio_file_name}.*')[0]
+ return encrypted_audio_file_path_and_name
+ # If n_m3u8dl-re can't find anything, return None value
+ except:
+ return None
+ else:
+ subprocess.run(audio_nm3u8_manual)
+ # Run n_m3u8dl-re from above list for specified audio language
+ try:
+ encrypted_video_file_path_and_name = \
+ glob.glob(f'{main_directory}\\downloads\\{random_encrypted_audio_file_name}.*')[0]
+ return encrypted_video_file_path_and_name
+ except:
+ return None
+
+
+# Define subtitle download function
+async def subtitle_download(manifest_url: str, slang: str = None):
+ # Create the encrypted filename for easy use
+ random_subtitle_name = str(uuid.uuid4())
+
+ # List of n_m3u8dl-re subtitle downloads commands
+ subtitle_nm3u8_english = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-ss',
+ 'name="English":for=all',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_subtitle_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ subtitle_nm3u8_foreign = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-ss',
+ 'all',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_subtitle_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ subtitle_nm3u8_manual = [
+ f"{n_m3u8dl_re}",
+ f"{manifest_url}",
+ '-ss',
+ f'name="{slang}":for=all',
+ "--tmp-dir",
+ f"{main_directory}\\temp\\",
+ "--save-dir",
+ f"{main_directory}\\downloads\\",
+ "--save-name",
+ f"{random_subtitle_name}",
+ "--log-level",
+ "OFF"
+ ]
+
+ if slang is None:
+ # Run n_m3u8dl-re from above list for subtitles
+ subprocess.run(subtitle_nm3u8_english)
+
+ # Find the subtitle path and extension
+ try:
+ subtitle_file_path_and_name = glob.glob(f'{main_directory}\\downloads\\{random_subtitle_name}.*')[0]
+ return subtitle_file_path_and_name
+ # If english is not available, download all.
+ except:
+ subprocess.run(subtitle_nm3u8_foreign)
+ try:
+ subtitle_file_path_and_name = glob.glob(f'{main_directory}\\downloads\\{random_subtitle_name}.*')[0]
+ return subtitle_file_path_and_name
+ # If no subtitles are found, return None value.
+ except:
+ return None
+ else:
+ subprocess.run(subtitle_nm3u8_manual)
+ try:
+ subtitle_file_path_and_name = glob.glob(f'{main_directory}\\downloads\\{random_subtitle_name}.*')[0]
+ return subtitle_file_path_and_name
+ # If no subtitles are found, return None value.
+ except:
+ return None
+
+
+# Define function to decrypt files
+async def decrypt_file(input_file_path_and_name: str, mp4decrypt_keys: list):
+ # Get the encrypted filename and extension
+ try:
+ filepath, file_ext = os.path.splitext(input_file_path_and_name)
+ # Send None value if not found
+ except:
+ return None
+
+ # Set random file name for easy use
+ random_decrypted_file_name = str(uuid.uuid4())
+
+ # Set the mp4decrypt command
+ mp4decrypt_command = [
+ f'{mp4decrypt}',
+ f'{filepath}{file_ext}',
+ f'{main_directory}\\downloads\\{random_decrypted_file_name}{file_ext}'
+ ] + mp4decrypt_keys
+
+ # Run mp4decrypt
+ subprocess.run(mp4decrypt_command)
+
+ # set the decrypted file name and extension
+ decrypted_file_path_and_name = f'{main_directory}\\downloads\\{random_decrypted_file_name}{file_ext}'
+
+ # Remove the encrypted file
+ os.remove(input_file_path_and_name)
+
+ # Return the decrypted file path, name, and extension.
+ return decrypted_file_path_and_name
+
+
+# Define function to merge all decrypted files
+async def ffmpeg_merge(input_video_file: str, input_audio_file: str, input_subtitle_file: str = None):
+ # Assign a random name for easy use
+ random_merge_name = f'{str(uuid.uuid4())}.mkv'
+
+ # FFmpeg merge command if subtitles are present
+ ffmpeg_merge_files_with_subtitles = [
+ f"{ffmpeg}",
+ '-i',
+ f"{input_video_file}",
+ '-i',
+ f"{input_audio_file}",
+ '-i',
+ f"{input_subtitle_file}",
+ '-vcodec',
+ 'copy',
+ '-acodec',
+ 'copy',
+ '-scodec',
+ 'copy',
+ f"{main_directory}\\downloads\\{random_merge_name}",
+ "-loglevel",
+ "panic"
+ ]
+
+ # FFmpeg merge command if subtitles are not present
+ ffmpeg_merge_files_without_subtitles = [
+ f"{ffmpeg}",
+ '-i',
+ f"{input_video_file}",
+ '-i',
+ f"{input_audio_file}",
+ '-vcodec',
+ 'copy',
+ '-acodec',
+ 'copy',
+ f"{main_directory}\\downloads\\{random_merge_name}",
+ "-loglevel",
+ "panic"
+ ]
+
+ # Check if there is any subtitles
+ if input_subtitle_file is not None:
+ try:
+ # Merge if there are subtitles
+ subprocess.run(ffmpeg_merge_files_with_subtitles)
+ os.remove(input_video_file)
+ os.remove(input_audio_file)
+ os.remove(input_subtitle_file)
+ return f'{main_directory}\\downloads\\{random_merge_name}'
+ except:
+ return None
+ else:
+ try:
+ # Merge if there are no subtitles
+ subprocess.run(ffmpeg_merge_files_without_subtitles)
+ os.remove(input_video_file)
+ os.remove(input_audio_file)
+ return f'{main_directory}\\downloads\\{random_merge_name}'
+ except:
+ return None
+
+
+# Define main function
+async def main():
+ # Get manifest from the user
+ manifest = input(f"\nInput manifest URL: ")
+
+ # Retrieve the PSSH
+ print_pssh = await manifest_pssh_parse(manifest)
+
+ # Print out the PSSH if found, else exit
+ if print_pssh is not None:
+ print(f'\nPSSH: {print_pssh}\n')
+
+ license_url = input("License URL: ")
+
+ # Retrieve and set decryption keys
+ dbkeys, mp4decrypt_keys = await retrieve_keys_remotely(pssh=print_pssh, license_url=license_url)
+
+ # Print out keys if exist, else exit
+ if dbkeys != "":
+ print(f"\nKeys found:\n{dbkeys}")
+ else:
+ print(f"\nCouldn't retrieve keys!")
+ exit()
+
+ # Downloading and assigning path/name for encrypted video using the encrypted_video_download function
+
+ # Check if manual resolution was specified
+ if args.video_res is not None:
+ print(f"\nUser specified {args.video_res} resolution\n")
+ encrypted_video_file_path_and_name = await encrypted_video_download(manifest_url=manifest, res=args.video_res)
+ # Default if none specified
+ else:
+ encrypted_video_file_path_and_name = await encrypted_video_download(manifest_url=manifest)
+
+ # Exit the process if failed
+ if encrypted_video_file_path_and_name is None:
+ print("Couldn't download video!")
+ exit()
+ # If exists, print filepath name/extension
+ else:
+ print(f"\nDownloaded encrypted video to {encrypted_video_file_path_and_name}")
+
+ # Downloading and assigning path/name for encrypted audio using the encrypted_audio_download function
+
+ # Check if audio language was manually specified
+ if args.audio_lang is not None:
+ print(f"\nUser specified {args.audio_lang} audio language\n")
+ encrypted_audio_file_path_and_name = await encrypted_audio_download(manifest_url=manifest, alang=args.audio_lang)
+ # Default if none specified
+ else:
+ encrypted_audio_file_path_and_name = await encrypted_audio_download(manifest_url=manifest)
+
+ # Exit the process if failed
+ if encrypted_audio_file_path_and_name is None:
+ print("Couldn't download audio!")
+ exit()
+ # If exists, print filepath and name/extension
+ else:
+ print(f"\nDownloaded encrypted audio to {encrypted_audio_file_path_and_name}")
+
+ # Downloading and assigning path/name for subtitles using the susbtitle_download function
+
+ # Check if subtitle language was manually specified
+ if args.subtitle_lang is not None:
+ print(f"\nUser specified {args.subtitle_lang} subtitle language\n")
+ subtitle_file_path_and_name = await subtitle_download(manifest_url=manifest, slang=args.subtitle_lang)
+ # Default if none specified
+ else:
+ subtitle_file_path_and_name = await subtitle_download(manifest_url=manifest)
+
+ # Print to the user no subtitles could be found
+ if subtitle_file_path_and_name is None:
+ print(f"\nCouldn't download subtitles!")
+ # If exists, print filepath and name/extension
+ else:
+ print(f"\nDownloaded subtitles to {subtitle_file_path_and_name}")
+
+ # Decrypting the encrypted video file with mp4decrypt function and mp4decrypt keys
+ decrypted_video_file_path_and_name = await decrypt_file(input_file_path_and_name=encrypted_video_file_path_and_name,
+ mp4decrypt_keys=mp4decrypt_keys)
+ # Exit the process if decryption failed
+ if decrypted_video_file_path_and_name is None:
+ print(f"\nVideo decryption failed!")
+ exit()
+ # If exists, print filepath and name/extension
+ else:
+ print(f"\nDecrypted video located at {decrypted_video_file_path_and_name}")
+
+ # Decrypting the encrypted audio file with mp4decrypt function and mp4decrypt keys
+ decrypted_audio_file_path_and_name = await decrypt_file(input_file_path_and_name=encrypted_audio_file_path_and_name,
+ mp4decrypt_keys=mp4decrypt_keys)
+ # Exit the process if decryption failed
+ if decrypted_audio_file_path_and_name is None:
+ print(f"\nAudio decryption failed!")
+ exit()
+ # If exists, print filepath and name/extension
+ else:
+ print(f"\nDecrypted audio located at {decrypted_audio_file_path_and_name}")
+
+ # Muxing the decrypted video/audio, and susbtitles if available with ffmpeg_merge function
+ final_muxed_file_path_and_name = await ffmpeg_merge(input_video_file=decrypted_video_file_path_and_name,
+ input_audio_file=decrypted_audio_file_path_and_name,
+ input_subtitle_file=subtitle_file_path_and_name)
+ # Exit the process if muxing failed
+ if final_muxed_file_path_and_name is None:
+ print(f"\nMuxing failed!")
+ exit()
+ # If exists, print filepath and name/extension
+ else:
+ print(f"\nFinal mux located at {final_muxed_file_path_and_name}")
+
+# Run the main program
+asyncio.run(main())
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2c8b74e
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+requests
+tqdm
\ No newline at end of file