From b4eacb6c7f40e6e6442af7731591c87391dc2e90 Mon Sep 17 00:00:00 2001 From: Darshit Limbad Date: Mon, 2 Sep 2024 14:50:38 +0530 Subject: [PATCH] V2.0 --- .gitignore | 18 +++++++ Readme.md | 67 +++++++++++++++++++++++ app.ico | Bin 0 -> 4286 bytes main.py | 31 +++++++++++ module/download.py | 124 +++++++++++++++++++++++++++++++++++++++++++ module/formatting.py | 100 ++++++++++++++++++++++++++++++++++ module/utils.py | 66 +++++++++++++++++++++++ requirements.txt | 1 + 8 files changed, 407 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 app.ico create mode 100644 main.py create mode 100644 module/download.py create mode 100644 module/formatting.py create mode 100644 module/utils.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..038a82f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +#environment +.venv/ + +# Byte-compiled / optimized / DLL files +__pycache__/ + +# Distribution / packaging +build/ +dist/ +downloads/ + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..b56ae13 --- /dev/null +++ b/Readme.md @@ -0,0 +1,67 @@ +# YouTube Video Downloader 2.0 + +Effortlessly download YouTube videos in the highest quality with our user-friendly YouTube Video Downloader. Whether you need the video, audio, or both, our tool handles it all, directly saving files to your chosen folder for easy access and convenience. + +## Important Note + +**Users should have downloaded FFmpeg on their PC because `yt_dlp` needs it to function properly.** + +### How to Install FFmpeg + +1. Download FFmpeg from the official website: [FFmpeg.org](https://ffmpeg.org/download.html). +2. Follow the installation instructions for your operating system. +3. Make sure FFmpeg is added to your system's PATH. + +## Features + +- Download videos in the highest quality. +- Option to download only audio or video. +- Save files directly to your specified folder. + +## Requirements + +- Python 3.6 or higher +- `yt_dlp` module +- FFmpeg (ensure it is installed and available in your system's PATH) + +## Installation + +1. Clone the repository: + ```bash + git clone https://github.com/darshitlimbad/YT_Video_Downloader.git + ``` +2. Navigate to the project directory: + ```bash + cd YT_Video_Downloader + ``` +3. Install the required Python packages: + ```bash + pip install -r requirements.txt + ``` + +## Usage + +1. Run the script: + ```bash + python main.py + ``` +2. Follow the on-screen instructions to download your desired video. + +## Building the Executable + +To build the executable file using cxfreeze, follow these steps: + +1. Install cxfreeze: + ```bash + pip install cxfreeze + ``` +2. Generate the executable: + ```bash + cxfreeze main.py --build_exe dist/ --icon app.ico + ``` + +The executable will be available in the `dist` folder. + +## License + +IDK \ No newline at end of file diff --git a/app.ico b/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..154b90d8f4dce35569e7524a08839f238247feac GIT binary patch literal 4286 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 VG#t51n`E84ta$SO0srs!{{YA=tGECF literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100644 index 0000000..3f4865d --- /dev/null +++ b/main.py @@ -0,0 +1,31 @@ +import yt_dlp + +from module.download import * +from module.formatting import print_colored_text + +def main(): + print("-" * 100) + + # Display options to the user + print("Please choose an option for download:") + print("1. Playlist") + print("2. Video") + + option = input("Enter your choice (1 or 2): ").strip() + + # Execute based on user's choice + if option == "1": + print("-" * 100) + print_colored_text(text="Enter Playlist URL:", fg_color="green") + playlist_url = input() + download_playlist(playlist_url) + elif option == "2": + print("-" * 100) + print_colored_text(text="Enter Video URL:", fg_color="green") + video_url = input() + download_video(video_url) + else: + print_colored_text(text="Invalid choice! Please enter 1 or 2.", fg_color="red") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/module/download.py b/module/download.py new file mode 100644 index 0000000..7fe7224 --- /dev/null +++ b/module/download.py @@ -0,0 +1,124 @@ +import os +import yt_dlp + +from .utils import * +from .formatting import * + +def download_video_media(video_url, download_folder, video_format, audio_format): + ydl_options = { + 'format': f'{video_format}+{audio_format}', + 'outtmpl': os.path.join(download_folder, '%(title)s.%(ext)s'), + 'progress_hooks': [hook_function], + } + + with yt_dlp.YoutubeDL(ydl_options) as ydl: + ydl.download([video_url]) + +def download_video(video_url,download_folder:str=".\\Downloads"): + try: + if '/playlist?' in video_url: + download_playlist(video_url) + return + + if not os.path.exists(download_folder): + os.mkdir(download_folder) + + video_only_formats= {} + audio_only_formats= {} + + # Get video info + ydl_opts = { + 'quiet': True, + 'listformats': False, + 'noplaylist': True, + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + video_info = ydl.extract_info(video_url, download=False) + + print(f"\nProcessing: {video_info['title']}") + + formats = video_info.get('formats', []) + + video_only_formats = [f for f in formats if f.get('vcodec','none') != 'none' and f.get('acodec','none') == 'none'] + audio_only_formats = [f for f in formats if f.get('vcodec','none') == 'none' and f.get('acodec','none') != 'none'] + + if not len(video_only_formats) or not len(audio_only_formats): + print_colored_text("No Format found\n",fg_color="red") + return + + # Validate and get user input for the desired video and audio + print_formats(format_type="video",video_only_formats=video_only_formats) + print_colored_text(text="Enter Video Index:",fg_color="green") + video_index = get_valid_index("", len(video_only_formats)) + + print_formats(format_type="audio",audio_only_formats=audio_only_formats) + print_colored_text(text="Enter Audio Index: ",fg_color="green") + audio_index = get_valid_index("", len(audio_only_formats)) + + # Retrieve the selected format IDs + video_format = video_only_formats[video_index]['format_id'] + audio_format = audio_only_formats[audio_index]['format_id'] + + if not video_format or not audio_format: + print_colored_text("No Format found\n",fg_color="red") + return + + # Download the video with yt-dlp, which will automatically merge video and audio + download_video_media(video_url, download_folder, video_format, audio_format) + + except Exception as e: + print_colored_text("\nSomething went wrong:",fg_color="red") + print(e) + +def download_playlist(playlist_url): + try: + if '/watch?' in playlist_url: + download_video(playlist_url) + return + + download_root = '.\\Downloads' + if not os.path.exists(download_root): + os.mkdir(download_root) + + # Get playlist info + ydl_opts = { + 'quiet': True, + 'listformats': False, + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + playlist_info = ydl.extract_info(playlist_url, download=False) + + playlist_title = playlist_info.get('title', 'Playlist') + playlist_folder = os.path.join(download_root, playlist_title) + + if not os.path.exists(playlist_folder): + os.mkdir(playlist_folder) + + print_colored_text(text="\nDownloading Playlist: ",fg_color="green") + print(playlist_title) + print_colored_text(text="Saving files to:",fg_color="green") + print(playlist_folder,end="\n\n") + + for entry in playlist_info['entries']: + video_url = entry['webpage_url'] + download_video(video_url,download_folder=playlist_folder) + + print_colored_text(text="\nPlaylist download successfully completed.",fg_color="green") + print_colored_text(text="\nPlaylist folder:",fg_color="green") + print(playlist_folder,end="\n\n") + print("-"*100) + + + except KeyError as ke: + print_colored_text("\nProcess Terminated. \nIt looks like you entered a single video URL instead of a playlist URL.",fg_color="red") + + if os.path.exists(playlist_folder) and len(os.listdir(playlist_folder)) == 0: + os.rmdir(playlist_folder) + except Exception as e: + print_colored_text("\nSomething went wrong:",fg_color="red") + print(e) + + if os.path.exists(playlist_folder) and len(os.listdir(playlist_folder)) == 0: + os.rmdir(playlist_folder) + \ No newline at end of file diff --git a/module/formatting.py b/module/formatting.py new file mode 100644 index 0000000..7fb121f --- /dev/null +++ b/module/formatting.py @@ -0,0 +1,100 @@ +from .utils import get_file_size_from_url , convert_bytes + +# ascii escape codes +esc_codes = { + # color codes + 'fore': { + 'red': '\033[31m', # Red text + 'green': '\033[32m', # Green text + 'bright_red': '\033[91m', # Bright Red text + 'bright_green': '\033[92m', # Bright Green text + 'dark_grey': '\033[90m', # Dark Grey text + 'reset': '\033[39m', # Reset to default foreground + }, + 'back': { + 'red': '\033[41m', # Red background + 'green': '\033[42m', # Green background + 'light_red': '\033[101m', # Light Red background + 'light_green': '\033[102m', # Light Green background + 'light_yellow': '\033[103m', # Light Yellow background + 'light_blue': '\033[104m', # Light Blue background + 'light_magenta': '\033[105m', # Light Magenta background + 'light_cyan': '\033[106m', # Light Cyan background + 'light_white': '\033[107m', # Light White background + 'reset': '\033[49m', # Reset to default background + + }, + + 'bold': '\033[1m', # Bold text attribute + 'underline': '\033[4m', # Underlined text attribute + + 'reset': '\033[0m', # Reset all attributes +} + + +def print_formats(format_type:str="video", video_only_formats:dict =None, audio_only_formats:dict =None): + """ + Prints available formats in a structured table format based on the format type. + + Args: + format_type (str): Type of formats to print, either 'video' or 'audio'. + video_only_formats (list, optional): A list of dictionaries containing video format information. + audio_only_formats (list, optional): A list of dictionaries containing audio format information. + + Description: + This function displays the available formats (video or audio) in a tabular format. + It includes headers for each column and prints a list of formats with their details. + Each format is displayed with an index, which allows users to choose a format based + on its number. + """ + + if format_type == 'video': + header = f"{f'{esc_codes['fore']['bright_green']}Index{esc_codes['reset']}':<5} {'Format ID':<15} {'Codec':<15} {'Resolution':<20} {'FPS':<10} {'Filesize':<20}" + formats = video_only_formats + fields = ['format_id', 'vcodec', 'format_note', 'fps', 'filesize'] + elif format_type == 'audio': + header = f"{f'{esc_codes['fore']['bright_green']}Index{esc_codes['reset']}{esc_codes['bold']}':<5} {'Format ID':<15} {'Codec':<15} {'Quality':<20} {'Filesize':<20}" + formats = audio_only_formats + fields = ['format_id', 'acodec', 'format_note', 'filesize'] + else: + raise ValueError("Invalid format type. Choose 'video' or 'audio'.") + + heading = f"\n{esc_codes['bold']}{esc_codes['fore']['green']}Available {format_type} formats:{esc_codes['reset']}" + print(heading) + print("=" * len(header)) # Separator line + print(f'{esc_codes['bold']}{header}{esc_codes['reset']}') # Print the formats header + print("=" * len(header)) # Separator line + + for i, f in enumerate(formats, start=1): + + filesize = convert_bytes(f.get('filesize')) or f"{esc_codes['fore']['dark_grey']} ~ {convert_bytes(get_file_size_from_url(f.get('url')))} ( dash ) {esc_codes['reset']}" + + + # Define the common elements for both video and audio + base_format = (f"{esc_codes['fore']['green']}{esc_codes['bold']}{i:<5}{esc_codes['reset']} " + f"{f.get(fields[0], 'N/A'):<15} {f.get(fields[1], 'N/A'):<15} {f.get(fields[2], 'N/A'):<20}") + + # Add specific details based on the format type + if format_type == 'video': + print(f"{base_format} {f.get(fields[3], 'N/A'):<10} {filesize:<20}") + elif format_type == 'audio': + print(f"{base_format} {filesize:<20}") + + print("=" * len(header)) + +def print_colored_text(text, fg_color=None, bg_color=None): + """ + Prints text with specified foreground and background colors, with an option to reset color attributes. + + Args: + text (str): The text to print. + fg_color (str, optional): The foreground color. Choices: 'red', 'green', 'bright_red', 'bright_green', etc. + bg_color (str, optional): The background color. Choices: 'light_red', 'light_green', etc. + reset (str, optional): The type of reset. Choices: 'all', 'foreground', 'background'. + """ + fg_code = esc_codes['fore'].get(fg_color, '') + bg_code = esc_codes['back'].get(bg_color, '') + reset_code = esc_codes['reset'] + + # Print text with color codes + print(f"{fg_code}{bg_code}{text}{reset_code}",end="") \ No newline at end of file diff --git a/module/utils.py b/module/utils.py new file mode 100644 index 0000000..ea3b19d --- /dev/null +++ b/module/utils.py @@ -0,0 +1,66 @@ +from urllib.parse import urlparse, parse_qs + +def get_valid_index(prompt, max_index): + """ + Prompts the user to enter an index and validates the input. + + Args: + prompt (str): The message to display to the user when asking for input. + max_index (int): The maximum valid index value. The user must enter a number between 1 and this value (inclusive). + + Returns: + int: A valid 0-based index entered by the user. + """ + while True: + try: + # Prompt user for input and convert to integer + user_input = int(input(prompt)) + + # Check if the input is within the valid range (1 to max_index inclusive) + if 1 <= user_input <= max_index: + # Convert to 0-based index (subtract 1) and return + return user_input - 1 + else: + # Inform the user that the input is out of the valid range + print(f"Invalid input! Please enter a number between 1 and {max_index}.") + except ValueError: + # Inform the user that the input is not a valid integer + print("Invalid input! Please enter a valid number.") + +def hook_function(d): + if d['status'] == 'finished': + print(f"Downloaded {d['filename']}, now post-processing...") + +def get_file_size_from_url(url): + + # Parse the URL + parsed_url = urlparse(url) + path_parts = parsed_url.path.split('/') + + # Find the part that contains 'clen' and extract the value + for part in path_parts: + if 'clen' in part: + clen_value = part.split('%3D')[1].split('%3B')[0] + break + + return int(clen_value) if clen_value else None + +def convert_bytes(size_in_bytes=None): + if not size_in_bytes: + return None + + # Define the units + units = ["Bytes", "KB", "MB", "GB", "TB", "PB"] + + size = float(size_in_bytes) + + # Initialize the unit index + unit_index = 0 + + # Loop to convert the size to the appropriate unit + while size >= 1024 and unit_index < len(units) - 1: + size /= 1024 + unit_index += 1 + + # Format the size to 2 decimal places and append the unit + return f"{size:.2f} {units[unit_index]}" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a7d7630 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +yt_dlp \ No newline at end of file