Simple YT Python App Overview

in #python5 months ago (edited)

Traveling and music go hand in hand, especially for road trips or long journeys. Recently, I built an app to help my dad make a personalized playlist while he was traveling through Ireland. This app, built with Python and Tkinter, simplifies the process of downloading and organizing YouTube videos as MP3 files. Here's how it works and how you can use it to create your playlists.

Just letting you know this is just a quick job to get the mp3 done and plenty of ways to do it professionally and better!

image.png

Overview of the app

image.png

  1. Fetch video URLs from a YouTube playlist
  2. Download the audio from these videos
  3. Convert the audio into MP3 format
  4. Optionally, organise the downloaded MP3 into directories named af the video's author.

Key Features

Fetch URLs from a YT playlist: Simply paste in the YT playlist and it retrieves all video URLS from that playlist into the download box for you. (My initial app only had manual entry for all URL's) - So much nicer if you don't really care from random playlists etc)

How It Works

User Interface

The app's user interface is created using Tkinter, a standard Python library for GUI development. Here are the main components of the interface:

YouTube URLs Textbox: A large text box where you can enter YouTube URLs (one per line).
Download Button: A button to start the download and conversion process.
Save to Author Folder Checkbox: A checkbox that, when checked, saves the MP3 files in folders named after the video's author.
Playlist URL Textbox: A smaller text box to input a YouTube playlist URL.
Fetch URLs Button: A button to fetch all video URLs from the provided playlist URL.
Progress Bar: A progress bar to show the download and conversion progress.
Functional Flow

Functional Flow

Fetching URLs from a Playlist:

Input a playlist URL and click the "Fetch URLs" button.
The app makes a request to the playlist URL and extracts video URLs using regex.
The extracted URLs are displayed in the "YouTube URLs" textbox and saved to a file.
Downloading and Converting Videos:

Input individual YouTube video URLs directly into the "YouTube URLs" textbox or use the fetched URLs from the playlist.
Click the "Download MP3s" button to start the process.
The app downloads the audio stream of each video using pytube.
The audio is then converted to MP3 format using pydub.
If the "Save to Author Folder" checkbox is checked, the MP3 files are saved in a folder named after the video’s author.
Progress Tracking:

The progress bar updates with each completed download, giving visual feedback on the operation's progress.

Code Highlights

Init and Setup
class YouTubeToMP3Downloader:
    def __init__(self, root):
        self.root = root
        self.root.title("YouTube to MP3 Downloader")
        self.root.geometry("768x800")
        self.url_label = tk.Label(root, text="YouTube URLs (one per line):")
        self.url_label.pack(pady=10)
        self.url_text = tk.Text(root, height=15, width=250)
        self.url_text.pack(pady=5)
        self.download_button = tk.Button(root, text="Download MP3s", command=self.download_mp3s)
        self.download_button.pack(pady=20)
        self.save_to_author_folder_var = tk.BooleanVar()
        self.save_to_author_folder_checkbox = tk.Checkbutton(root, text="Save to Author Folder", variable=self.save_to_author_folder_var)
        self.save_to_author_folder_checkbox.pack()
        self.directory_label = tk.Label(root, text="Playlist Directory:")
        self.directory_label.pack(pady=10)
        self.directory_text = tk.Text(root, height=1, width=50)
        self.directory_text.pack(pady=5)
        self.playlist_label = tk.Label(root, text="Playlist URL:")
        self.playlist_label.pack(pady=20)
        self.playlist_url = tk.Text(root, height=5, width=25)
        self.playlist_url.pack(pady=20)
        fetch_button = tk.Button(root, text="Fetch URLs", command=self.get_playlist_urls)
        fetch_button.pack(pady=10)
        self.progress_bar = ttk.Progressbar(root, orient='horizontal', length=400, mode='determinate')
        self.progress_bar.pack(pady=20)

Download and convert function

async def download_and_convert_mp3(self, url, checkbox, progress_bar, step):
    try:
        yt = YouTube(url)
        video = yt.streams.filter(only_audio=True).first()
        output_file = video.download(filename='temp_audio.mp4')
        base, _ = os.path.splitext(output_file)
        mp3_file = base + '.mp3'
        audio = AudioSegment.from_file(output_file)
        audio.export(mp3_file, format="mp3")
        video_title = yt.author + '-' + yt.title
        os.rename(mp3_file, f"{video_title}.mp3")
        os.remove(output_file)
        if self.directory_text.get("1.0", tk.END).strip():
            author_folder = self.directory_text.get("1.0", tk.END).strip()
        else:
            if checkbox:
                author_folder = yt.author
                if not os.path.exists(author_folder):
                    os.makedirs(author_folder)
                shutil.move(f"{video_title}.mp3", author_folder)
        return f"MP3 downloaded and saved as {video_title}.mp3"
    except Exception as e:
        return f"Error processing {url}: {e}"
    finally:
        progress_bar.step(step)
        progress_bar.update_idletasks()

Complete Python File 'pyratetube.py'

import tkinter as tk
from tkinter import ttk, messagebox
from pytube import YouTube
from pydub import AudioSegment
import sys
import os
import asyncio
import shutil
import re
import requests

class YouTubeToMP3Downloader:
    def download_mp3_with_author(self, youtube_url, target_directory):
        yt = YouTube(youtube_url)
        author = yt.author

        
        if self.directory_text.get("1.0", tk.END).strip():
            author_folder = self.directory_text.get("1.0", tk.END).strip()
        else:
            author_folder = os.path.join(target_directory, author)
        if not os.path.exists(author_folder):
            os.makedirs(author_folder)

        mp3_file_path = yt.streams.filter(only_audio=True).first().download(output_path=author_folder)
        print(f"MP3 file downloaded and saved to: {mp3_file_path}")

    def __init__(self, root):
        def on_checkbox_click():
            print("Checkbox state:", self.save_to_author_folder_var.get())

        def extract_hrefs_from_content(content):
            href_pattern = re.compile(r'"playlistVideoRenderer":\{.*?"navigationEndpoint":\{.*?"webCommandMetadata":\{"url":"(\/watch\?v=[^"]*?)".*?\}.*?\}.*?\}', re.DOTALL)
            hrefs = href_pattern.findall(content)
            return hrefs

        def get_playlist_videos(url):
            response = requests.get(url)
            return response.content.decode('utf-8')

        def get_playlist_urls():
            playlist_url = self.playlist_url.get("1.0", tk.END).strip()
            if not playlist_url:
                messagebox.showerror("Error", "Please enter a playlist URL")
                return

            playlist_videos = get_playlist_videos(playlist_url)
            hrefs = extract_hrefs_from_content(playlist_videos)

            self.url_text.delete('1.0', tk.END)
            with open('urls.txt', 'w', encoding='utf-8') as file:
                for href in hrefs:
                    url = 'https://www.youtube.com' + href
                    print(url)
                    self.url_text.insert(tk.END, url + '\n')
                    file.write(url + '\n')

        def resource_path(relative_path):
            try:
                base_path = sys._MEIPASS
            except Exception:
                base_path = os.path.abspath(".")
            return os.path.join(base_path, relative_path)

        self.root = root
        self.root.title("YouTube to MP3 Downloader")
        self.root.geometry("768x800")
        self.root.pack_propagate(0)
        self.root.wm_attributes("-topmost", True)

        self.url_label = tk.Label(root, text="YouTube URLs (one per line):")
        self.url_label.pack(pady=10)

        self.url_text = tk.Text(root, height=15, width=250)
        self.url_text.pack(pady=5)

        self.download_button = tk.Button(root, text="Download MP3s", command=self.download_mp3s)
        self.download_button.pack(pady=20)

        self.save_to_author_folder_var = tk.BooleanVar()
        self.save_to_author_folder_checkbox = tk.Checkbutton(root, text="Save to Author Folder", variable=self.save_to_author_folder_var, command=on_checkbox_click)
        self.save_to_author_folder_checkbox.pack()

        self.directory_label = tk.Label(root, text="Playlist Directory:")
        self.directory_label.pack(pady=10)

        self.directory_text = tk.Text(root, height=1, width=50)
        self.directory_text.pack(pady=5)

        self.playlist_label = tk.Label(root, text="Playlist URL:")
        self.playlist_label.pack(pady=20)

        self.playlist_url = tk.Text(root, height=5, width=25)
        self.playlist_url.pack(pady=20)


        fetch_button = tk.Button(root, text="Fetch URLs", command=get_playlist_urls)
        fetch_button.pack(pady=10)

        self.progress_bar = ttk.Progressbar(root, orient='horizontal', length=400, mode='determinate')
        self.progress_bar.pack(pady=20)

    async def download_and_convert_mp3(self, url, checkbox, progress_bar, step):
        try:
            yt = YouTube(url)
            video = yt.streams.filter(only_audio=True).first()
            if not video:
                return f"No audio stream available for {url}"

            output_file = video.download(filename='temp_audio.mp4')
            if not output_file:
                return f"Failed to download video {url}"

            base, _ = os.path.splitext(output_file)
            mp3_file = base + '.mp3'

            audio = AudioSegment.from_file(output_file)
            audio.export(mp3_file, format="mp3")

            video_title = yt.author + '-' + yt.title
            os.rename(mp3_file, f"{video_title}.mp3")
            os.remove(output_file)


            if self.directory_text.get("1.0", tk.END).strip():
                author_folder = self.directory_text.get("1.0", tk.END).strip()
            else:
                if checkbox:
                    author_folder = yt.author
                    if not os.path.exists(author_folder):
                        os.makedirs(author_folder)
                    shutil.move(f"{video_title}.mp3", author_folder)

            return f"MP3 downloaded and saved as {video_title}.mp3"
        except Exception as e:
            return f"Error processing {url}: {e}"
        finally:
            progress_bar.step(step)
            progress_bar.update_idletasks()

    async def download_mp3s_async(self):
        urls = self.url_text.get("1.0", tk.END).splitlines()
        if not urls:
            messagebox.showerror("Error", "Please enter YouTube URLs")
            return

        total_tasks = len(urls)
        if total_tasks == 0:
            messagebox.showerror("Error", "No URLs to process")
            return

        self.progress_bar['value'] = 0
        step = 100 / total_tasks

        tasks = [self.download_and_convert_mp3(url, self.save_to_author_folder_var.get(), self.progress_bar, step) for url in urls]
        results = await asyncio.gather(*tasks)

        messagebox.showinfo("Download Results", "\n".join(results))

    def download_mp3s(self):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        loop.run_until_complete(self.download_mp3s_async())

if __name__ == "__main__":
    root = tk.Tk()
    app = YouTubeToMP3Downloader(root)
    root.mainloop()

Steps

1. Create virtualenv  'python -m venv ./venv
2. Activate venv './venv/Scripts/Activate'
3. Install dependancies 'pip install -r requirements.txt'
4. You need to download the FFMPEG .exe's that I cannot supply with the exe (https://github.com/FFmpeg/FFmpeg) And these need to be in the same Directory as the exe
5. Run this as a batch script to rebuild. (pyinstaller --name PyrateTube --noconsole --onefile --windowed --icon=PyrateTube.ico --debug=all pyratetube.py)

Conclusion

I know there are plenty of online MP3 downloaders out there but I really just wanted to give something easy to my dad to be able to enter a couple of links or a playlist and let it do it for him! The big benefit is ability to organise by author/folder and its already on his computer, rather than having to do individual ones.

Ideas

  • I created a Flask implementation of this same app which to be honest can be better UI and hosted on web/docker image etc than Tkinter but just harder to supply to him than a simple packaged exe

image.png

What is everyone else creating, I'd love to know! :)