Simple GUI Downloader Using yt-dlp in Python

Python
Simple GUI Downloader Using yt-dlp in Python

Introduction

How do I make my own youtube video downloader?

In this blog post we are going to make a simple gui downloader using yt-dlp in python programming language. For the yt-dlp gui we are going to use tkinter library. Making best gui for yt-dlp we are using ttkbootstrap. By using this downloader, you can download videos, playlists and music from yt-dlp supported sites.

Setting up virtual environment

How to create and activate Virtual Environment

To create virtual environment, type the following commands in Terminal/CMD

Create project directory using & change to directory.

mkdir pysimple-downloader && cd pysimple-downloader

python -m venv followed by virtual environment name to create a virtual environment.

After creating the virtual environment activate it by using

In Windows

In MacOS/Linux

Installing Dependencies

Make sure to activate the virtual environment before installing dependencies.

Making Downloader GUI

Creating Simple GUI Downloader Using yt-dlp binary by ttkbootstrap

#main.py
import json
import os
import subprocess
import threading
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from tkinter.filedialog import askopenfilename, askdirectory


class YoutubeDLGUI(ttk.Frame):
    def __init__(self, master):
        super().__init__(master, padding=15)
        self.pack(fill=BOTH, expand=YES)

        self.url = ttk.StringVar(value="")
        with open("settings.json", 'r') as f:
            self.settings = json.load(f)
        self.ytdl_path = ttk.StringVar(value=self.settings["ytdl_path"])
        self.aria2c_path = ttk.StringVar(value=self.settings["aria2c_path"])
        self.output_dir = ttk.StringVar(value=self.settings["output_dir"])

        self.create_form()

    def create_form(self):
        container = ttk.Frame(self)
        container.pack(fill=X, expand=YES, pady=5)
    
        # URL entry
        url_lbl = ttk.Label(container, text="URL", width=10)
        url_lbl.pack(side=LEFT, padx=5)
        url_ent = ttk.Entry(container, textvariable=self.url)
        url_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
    
        # Download button
        download_btn = ttk.Button(container, text="Download", command=self.threading_subprocess)
        download_btn.pack(side=LEFT, padx=5, fill=X)
    
        # yt-dlp path entry
        container2 = ttk.Frame(self)
        container2.pack(fill=X, expand=YES, pady=5)
        ytdlp_lbl = ttk.Label(container2, text="yt-dlp path:", width=10)
        ytdlp_lbl.pack(side=LEFT, padx=5)
        ytdlp_ent = ttk.Entry(container2, textvariable=self.ytdl_path)
        ytdlp_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container2, text="Browse", command=self.on_browse_ytdlp)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    
        # aria2c path entry
        container3 = ttk.Frame(self)
        container3.pack(fill=X, expand=YES, pady=5)
        aria2c_lbl = ttk.Label(container3, text="aria2c path:", width=10)
        aria2c_lbl.pack(side=LEFT, padx=5)
        aria2c_ent = ttk.Entry(container3, textvariable=self.aria2c_path)
        aria2c_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container3, text="Browse", command=self.on_browse_aria2c)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    
        # Output directory entry
        container4 = ttk.Frame(self)
        container4.pack(fill=X, expand=YES, pady=5)
        output_lbl = ttk.Label(container4, text="output dir:", width=10)
        output_lbl.pack(side=LEFT, padx=5)
        output_ent = ttk.Entry(container4, textvariable=self.output_dir)
        output_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container4, text="Browse", command=self.on_browse_output)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    

    def threading_subprocess(self):
        new_thread = threading.Thread(target=self.download)
        new_thread.start()
        
    def on_browse_ytdlp(self):
        path = askopenfilename(title="Select ytdlp.exe")
        with open("settings.json", "r+") as f:
            data = json.load(f)
            data["ytdl_path"] = path
            f.seek(0)
            json.dump(data, f, indent=4)
            f.truncate()
            
        self.ytdl_path.set(path)

    def on_browse_aria2c(self):
        path = askopenfilename(title="Select aria2c.exe")
        
        # Read the JSON data from the file
        with open("settings.json", "r+") as f:
            data = json.load(f)
            data["aria2c_path"] = path
            f.seek(0)
            json.dump(data, f, indent=4)
            f.truncate()
        
        self.aria2c_path.set(path)

    def on_browse_output(self):
        path = askdirectory(title="Select output dir")
        with open("settings.json", "r+") as f:
            data = json.load(f)
            data["output_dir"] = path
            f.seek(0)
            json.dump(data, f, indent=4)
            f.truncate()
        self.output_dir.set(path)

    def download(self):
        subprocess.run([self.ytdl_path.get(), self.url.get(), '--downloader', self.aria2c_path.get(), '-S', 'height:720', '-o', "%(title)s.%(ext)s", '-P', f"{self.output_dir.get()}/videos", '-P', f"temp:{self.output_dir.get()}/temp"])
        
if __name__ == '__main__':
    if os.path.exists("settings.json"):
        pass
    else:
        with open("settings.json", "a+") as f:
            data = {}
            data["ytdl_path"] = ""
            data["aria2c_path"] = ""
            data["output_dir"] = ""
            json.dump(data, f, indent=4)
    app = ttk.Window("YoutubeDL GUI", 'superhero',)
    YoutubeDLGUI(app)
    app.mainloop()

Importing necessary packages

import json
import os
import subprocess
import threading
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from tkinter.filedialog import askopenfilename, askdirectory

For making simple GUI downloader using yt-dlp we need some packages to be imported.

“json” package is used to handle json file to save the yt-dlp path, aria2c path and select default output directory which is act as setting file for the program.

“os” package is used to check whether settings.json exists in current directory or not.

“subprocess” package is used to execute yt-dlp program in the background.

“threading” package is used to for multhreading process. To run multiple subprocess of yt-dlp.

“ttkbootstrap” package is used to style the gui items.

“tkinter.filedialog” is used to select directory and items.

Creating Basic Window

class YoutubeDLGUI(ttk.Frame):
    def __init__(self, master):
        super().__init__(master, padding=15)
        self.pack(fill=BOTH, expand=YES)
  1. class YoutubeDLGUI(ttk.Frame):: This line defines a new class called YoutubeDLGUI that inherits from the ttk.Frame class. This means that the YoutubeDLGUI class will inherit all the attributes and methods of the ttk.Frame class and can also have its own custom attributes and methods.
  2. def __init__(self, master):: This is the constructor method (also called initializer) for the YoutubeDLGUI class. It takes two parameters: self (referring to the instance of the class) and master (which is typically a reference to the parent widget or container where this GUI will be placed).
  3. super().__init__(master, padding=15): This line calls the constructor of the parent class (ttk.Frame) using the super() function. It initializes the ttk.Frame part of the class and sets the padding option to 15, which adds some padding around the edges of the frame.
  4. self.pack(fill=BOTH, expand=YES): This line packs the frame widget within its parent widget (master). The fill option is set to BOTH, which means the frame will expand both horizontally and vertically to fill the available space. The expand option is set to YES, allowing the frame to expand as much as possible within its parent.

Creating variables for dynamic data

        self.url = ttk.StringVar(value="")
        with open("settings.json", 'r') as f:
            self.settings = json.load(f)
        self.ytdl_path = ttk.StringVar(value=self.settings["ytdl_path"])
        self.aria2c_path = ttk.StringVar(value=self.settings["aria2c_path"])
        self.output_dir = ttk.StringVar(value=self.settings["output_dir"])
  1. self.url = ttk.StringVar(value=""): This creates an instance variable named url using the ttk.StringVar class. This variable is used to store the URL of the video that the user wants to download. It is initialized with an empty string.
  2. with open("settings.json", 'r') as f:: This line opens a file named "settings.json" in read mode ('r'). The file is assumed to be in the same directory as the script. It is opened using a context manager (with statement), which ensures that the file is properly closed after reading.
  3. self.settings = json.load(f): This line reads the contents of the "settings.json" file using the json.load() function and stores the loaded JSON data in the instance variable self.settings. This JSON data likely contains various configuration settings for the GUI.
  4. self.ytdl_path = ttk.StringVar(value=self.settings["ytdl_path"]): This creates an instance variable named ytdl_path using ttk.StringVar. It is initialized with the value of the "ytdl_path" key from the self.settings dictionary. This variable is likely used to store the path to the youtube-dl executable.
  5. self.aria2c_path = ttk.StringVar(value=self.settings["aria2c_path"]): Similar to the previous line, this creates an instance variable named aria2c_path initialized with the value of the "aria2c_path" key from the self.settings dictionary. It is likely used to store the path to the aria2c executable, which is a command-line download utility.
  6. self.output_dir = ttk.StringVar(value=self.settings["output_dir"]): This creates an instance variable named output_dir initialized with the value of the "output_dir" key from the self.settings dictionary. It is likely used to store the directory where downloaded files will be saved.

Creating Basic layout for GUI

        self.create_form()

    def create_form(self):
        container = ttk.Frame(self)
        container.pack(fill=X, expand=YES, pady=5)
    
        # URL entry
        url_lbl = ttk.Label(container, text="URL", width=10)
        url_lbl.pack(side=LEFT, padx=5)
        url_ent = ttk.Entry(container, textvariable=self.url)
        url_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
    
        # Download button
        download_btn = ttk.Button(container, text="Download", command=self.threading_subprocess)
        download_btn.pack(side=LEFT, padx=5, fill=X)
    
        # yt-dlp path entry
        container2 = ttk.Frame(self)
        container2.pack(fill=X, expand=YES, pady=5)
        ytdlp_lbl = ttk.Label(container2, text="yt-dlp path:", width=10)
        ytdlp_lbl.pack(side=LEFT, padx=5)
        ytdlp_ent = ttk.Entry(container2, textvariable=self.ytdl_path)
        ytdlp_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container2, text="Browse", command=self.on_browse_ytdlp)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    
        # aria2c path entry
        container3 = ttk.Frame(self)
        container3.pack(fill=X, expand=YES, pady=5)
        aria2c_lbl = ttk.Label(container3, text="aria2c path:", width=10)
        aria2c_lbl.pack(side=LEFT, padx=5)
        aria2c_ent = ttk.Entry(container3, textvariable=self.aria2c_path)
        aria2c_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container3, text="Browse", command=self.on_browse_aria2c)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    
        # Output directory entry
        container4 = ttk.Frame(self)
        container4.pack(fill=X, expand=YES, pady=5)
        output_lbl = ttk.Label(container4, text="output dir:", width=10)
        output_lbl.pack(side=LEFT, padx=5)
        output_ent = ttk.Entry(container4, textvariable=self.output_dir)
        output_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container4, text="Browse", command=self.on_browse_output)
        browse_btn.pack(side=LEFT, padx=5, fill=X)

It adds two widgets (a label and an entry) for the URL input field, as well as a “Download” button to the form.

  1. container = ttk.Frame(self): This line creates a new instance of the ttk.Frame class called container. This new frame will serve as a sub-container within the main YoutubeDLGUI frame to hold input fields and widgets related to the form. The self argument passed to the ttk.Frame constructor indicates that the new frame is a child of the main YoutubeDLGUI frame.
  2. container.pack(fill=X, expand=YES, pady=5): This line packs (places) the container frame within the main YoutubeDLGUI frame. The fill option is set to X, which means the sub-container (container) will expand horizontally to fill the available space. The expand option is set to YES, allowing the sub-container to expand within its parent frame. The pady option adds a vertical padding of 5 units around the sub-container.
  3. url_lbl = ttk.Label(container, text="URL", width=10): This line creates a ttk.Label widget labeled “URL” within the container frame. The width option is set to 10, which determines the width of the label.
  4. url_lbl.pack(side=LEFT, padx=5): The label widget is packed (placed) on the left side of the container frame, and a horizontal padding of 5 units is added on the left side.
  5. url_ent = ttk.Entry(container, textvariable=self.url): This line creates a ttk.Entry widget within the container frame. It provides a text input field where the user can enter a URL. The textvariable parameter is set to self.url, which is a ttk.StringVar instance representing the URL input field.
  6. url_ent.pack(side=LEFT, padx=5, fill=X, expand=YES): The entry widget is packed on the left side of the container frame, with a horizontal padding of 5 units on the left side. The fill option is set to X, causing the entry widget to expand horizontally. The expand option is set to YES, allowing the entry widget to take up available space within its parent.
  7. download_btn = ttk.Button(container, text="Download", command=self.threading_subprocess): This line creates a ttk.Button widget labeled “Download” within the container frame. The command parameter is set to self.threading_subprocess, which indicates that the threading_subprocess method will be called when the button is clicked.
  8. download_btn.pack(side=LEFT, padx=5, fill=X): The button widget is packed on the left side of the container frame, with a horizontal padding of 5 units on the left side. The fill option is set to X, causing the button to expand horizontally.
        # yt-dlp path entry
        container2 = ttk.Frame(self)
        container2.pack(fill=X, expand=YES, pady=5)
        ytdlp_lbl = ttk.Label(container2, text="yt-dlp path:", width=10)
        ytdlp_lbl.pack(side=LEFT, padx=5)
        ytdlp_ent = ttk.Entry(container2, textvariable=self.ytdl_path)
        ytdlp_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container2, text="Browse", command=self.on_browse_ytdlp)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    
        # aria2c path entry
        container3 = ttk.Frame(self)
        container3.pack(fill=X, expand=YES, pady=5)
        aria2c_lbl = ttk.Label(container3, text="aria2c path:", width=10)
        aria2c_lbl.pack(side=LEFT, padx=5)
        aria2c_ent = ttk.Entry(container3, textvariable=self.aria2c_path)
        aria2c_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container3, text="Browse", command=self.on_browse_aria2c)
        browse_btn.pack(side=LEFT, padx=5, fill=X)
    
        # Output directory entry
        container4 = ttk.Frame(self)
        container4.pack(fill=X, expand=YES, pady=5)
        output_lbl = ttk.Label(container4, text="output dir:", width=10)
        output_lbl.pack(side=LEFT, padx=5)
        output_ent = ttk.Entry(container4, textvariable=self.output_dir)
        output_ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
        browse_btn = ttk.Button(container4, text="Browse", command=self.on_browse_output)
        browse_btn.pack(side=LEFT, padx=5, fill=X)

Similar to url container we create ytdlp-path, aria2c-path and output-path containers.

Multi-Threading Download Method

    def threading_subprocess(self):
        new_thread = threading.Thread(target=self.download)
        new_thread.start()

The threading_subprocess method in ourYoutubeDLGUI class is responsible for creating a new thread to run the download method concurrently.

  1. new_thread = threading.Thread(target=self.download): This line creates a new instance of the Thread class from the threading module. The target parameter is set to self.download, which means the download method of the current instance (an instance of the YoutubeDLGUI class) will be the target function that the new thread runs.
  2. new_thread.start(): This line starts the newly created thread, causing it to begin executing the download method concurrently with the main thread of the GUI.

File Selection & Directory Selection

    def on_browse_ytdlp(self):
        path = askopenfilename(title="Select ytdlp.exe")
        with open("settings.json", "r+") as f:
            data = json.load(f)
            data["ytdl_path"] = path
            f.seek(0)
            json.dump(data, f, indent=4)
            f.truncate()
            
        self.ytdl_path.set(path)

The on_browse_ytdlp method in ourYoutubeDLGUI class is used for selecting the path to the ytdlp.exe executable file and updating it in the GUI and settings file.

  1. path = askopenfilename(title="Select ytdlp.exe"): This line uses the askopenfilename function, presumably from a file dialog library like tkinter (assuming you have imported it), to open a file dialog window and prompt the user to select the ytdlp.exe executable file. The selected file path is stored in the path variable.
  2. with open("settings.json", "r+") as f:: This line opens the "settings.json" file in read and write mode ("r+") using a context manager. The file is opened to read and modify its contents.
  3. data = json.load(f): This line reads the JSON data from the "settings.json" file and loads it into the data dictionary.
  4. data["ytdl_path"] = path: This line updates the "ytdl_path" key in the data dictionary with the selected ytdlp.exe file path.
  5. f.seek(0): This line moves the file cursor to the beginning of the file, preparing it for writing.
  6. json.dump(data, f, indent=4): This line writes the updated data dictionary back to the "settings.json" file with an indentation of 4 spaces, making the JSON data more human-readable.
  7. f.truncate(): This line truncates any remaining content in the file beyond the updated data. It ensures that the file size matches the size of the written JSON data, preventing any leftover content from previous writes.
  8. self.ytdl_path.set(path): This line updates the value of the self.ytdl_path ttk.StringVar instance with the newly selected ytdlp.exe path. This update will likely cause the associated widget in the GUI to reflect the new path.
    def on_browse_aria2c(self):
        path = askopenfilename(title="Select aria2c.exe")
        
        # Read the JSON data from the file
        with open("settings.json", "r+") as f:
            data = json.load(f)
            data["aria2c_path"] = path
            f.seek(0)
            json.dump(data, f, indent=4)
            f.truncate()
        
        self.aria2c_path.set(path)

    def on_browse_output(self):
        path = askdirectory(title="Select output dir")
        with open("settings.json", "r+") as f:
            data = json.load(f)
            data["output_dir"] = path
            f.seek(0)
            json.dump(data, f, indent=4)
            f.truncate()
        self.output_dir.set(path)

Similar to ytdlp-path, aria2c-path and output-path are selected and updated.

Code to Download Videos

    def download(self):
        subprocess.run([self.ytdl_path.get(), self.url.get(), '--downloader', self.aria2c_path.get(), '-S', 'height:720', '-o', "%(title)s.%(ext)s", '-P', f"{self.output_dir.get()}/videos", '-P', f"temp:{self.output_dir.get()}/temp"])

The download method in our YoutubeDLGUI class is responsible for initiating a video download using the youtube-dl library, along with the specified settings provided by the user through the GUI.

  1. subprocess.run(...): This line initiates a new subprocess using the subprocess.run() function. It will execute the command specified in the list within the parentheses.
  2. [self.ytdl_path.get(), self.url.get(), ...]: The list contains a sequence of arguments that will be passed to the subprocess. These arguments include:
    • self.ytdl_path.get(): The path to the ytdlp.exe executable.
    • self.url.get(): The URL of the video to be downloaded.
  3. '--downloader', self.aria2c_path.get(): This specifies the --downloader option followed by the path to the aria2c executable. This indicates that aria2c will be used as the downloader when fetching video fragments.
  4. '-S', 'height:720': This specifies the -S option to select the best quality video with a height of 720 pixels.
  5. '-o', "%(title)s.%(ext)s": This specifies the -o option to set the output filename format. The format string % (title)s.%(ext)s uses placeholders to form the output filename as the video title followed by the file extension.
  6. '-P', f"{self.output_dir.get()}/videos": This specifies the -P option to set the output directory for downloaded videos. The output directory is obtained from self.output_dir.get(), which represents the output directory path specified by the user in the GUI.
  7. '-P', f"temp:{self.output_dir.get()}/temp": This specifies another -P option for temporary files, allowing them to be stored in a separate temporary directory within the user-defined output directory.

Calling Mainloop

if __name__ == '__main__':
    if os.path.exists("settings.json"):
        pass
    else:
        with open("settings.json", "a+") as f:
            data = {}
            data["ytdl_path"] = ""
            data["aria2c_path"] = ""
            data["output_dir"] = ""
            json.dump(data, f, indent=4)
    app = ttk.Window("YoutubeDL GUI", 'superhero',)
    YoutubeDLGUI(app)
    app.mainloop()

Creates and runs a GUI application for interacting with the youtube-dl library using the ttk (themed Tkinter) module. It sets up the GUI interface, initializes default settings, and starts the application’s main loop.

  1. if os.path.exists("settings.json"): ...: This block checks if the "settings.json" file exists in the current directory. If it does, the code proceeds without making any changes.
  2. else: ...: If the "settings.json" file does not exist, this block creates it and initializes default settings. It opens the file in append mode ("a+"), which will also create the file if it doesn’t exist.
  3. The data dictionary is populated with default values for ytdl_path, aria2c_path, and output_dir.
  4. json.dump(data, f, indent=4): The default settings data is written to the "settings.json" file in JSON format with an indentation of 4 spaces.
  5. app = ttk.Window("YoutubeDL GUI", 'superhero'): This line creates an instance of the ttk.Window class, representing the main application window. It sets the window title to “YoutubeDL GUI” and uses the 'superhero' theme style.
  6. YoutubeDLGUI(app): This line creates an instance of the YoutubeDLGUI class within the main application window (app).
  7. app.mainloop(): This line starts the main event loop of the application, which is necessary to handle user interactions and keep the GUI responsive.

Packaging

For packaging the script. First we need to install pyinstaller package.

Then execute the following line.

pyinstaller main.py --onefile -w

You can find the source code for Simple GUI Downloader Using yt-dlp on github.

Conclusion

In this tutorial, we’ve demonstrated how to create a simple GUI video downloader using Python’s tkinter library and ttkbootstrap. We’ve integrated the yt-dlp library to enable video downloads.

Remember to test your code thoroughly and consider adding more features and error handling to enhance the user experience of your GUI application.

Feel free to explore further and customize the GUI downloader to meet your specific needs and preferences. Happy coding and blogging!

(Note: This tutorial provides a basic outline of creating a GUI downloader using yt-dlp and ttkbootstrap. You can expand upon this foundation by adding more features, error handling, and customization as desired.)

Leave a Reply

Your email address will not be published. Required fields are marked *