1+ #!/usr/bin/env python3
2+ import customtkinter as ctk
3+ import yt_dlp
4+ import threading
5+ import os
6+
7+ ctk .set_appearance_mode ("dark" )
8+ ctk .set_default_color_theme ("blue" )
9+
10+ DOWNLOAD_DIR = os .path .expanduser ("~/Downloads" )
11+
12+ class YTConverterApp (ctk .CTk ):
13+ def __init__ (self ):
14+ super ().__init__ ()
15+ self .title ("YT Converter" )
16+ self .geometry ("520x420" )
17+ self .resizable (False , False )
18+
19+ # Title
20+ ctk .CTkLabel (self , text = "YouTube Converter" , font = ctk .CTkFont (size = 22 , weight = "bold" )).pack (pady = (24 , 4 ))
21+ ctk .CTkLabel (self , text = "Download videos or audio from YouTube" , text_color = "gray" ).pack ()
22+
23+ # URL input
24+ self .url_entry = ctk .CTkEntry (self , placeholder_text = "Paste YouTube URL here..." , width = 420 , height = 40 )
25+ self .url_entry .pack (pady = 20 )
26+
27+ # Format frame
28+ fmt_frame = ctk .CTkFrame (self , fg_color = "transparent" )
29+ fmt_frame .pack ()
30+ ctk .CTkLabel (fmt_frame , text = "Format:" ).pack (side = "left" , padx = (0 , 12 ))
31+ self .format_var = ctk .StringVar (value = "mp3" )
32+ ctk .CTkRadioButton (fmt_frame , text = "MP3 (Audio)" , variable = self .format_var , value = "mp3" ).pack (side = "left" , padx = 8 )
33+ ctk .CTkRadioButton (fmt_frame , text = "MP4 (Video)" , variable = self .format_var , value = "mp4" ).pack (side = "left" , padx = 8 )
34+
35+ # Quality frame
36+ qual_frame = ctk .CTkFrame (self , fg_color = "transparent" )
37+ qual_frame .pack (pady = 8 )
38+ ctk .CTkLabel (qual_frame , text = "Quality:" ).pack (side = "left" , padx = (0 , 12 ))
39+ self .quality_var = ctk .StringVar (value = "best" )
40+ ctk .CTkRadioButton (qual_frame , text = "Best" , variable = self .quality_var , value = "best" ).pack (side = "left" , padx = 8 )
41+ ctk .CTkRadioButton (qual_frame , text = "720p" , variable = self .quality_var , value = "720" ).pack (side = "left" , padx = 8 )
42+ ctk .CTkRadioButton (qual_frame , text = "480p" , variable = self .quality_var , value = "480" ).pack (side = "left" , padx = 8 )
43+
44+ # Progress bar
45+ self .progress = ctk .CTkProgressBar (self , width = 420 )
46+ self .progress .pack (pady = 16 )
47+ self .progress .set (0 )
48+
49+ # Status
50+ self .status = ctk .CTkLabel (self , text = "Ready" , text_color = "gray" )
51+ self .status .pack ()
52+
53+ # Button
54+ self .btn = ctk .CTkButton (self , text = "⬇ Convert & Download" , width = 220 , height = 44 ,
55+ font = ctk .CTkFont (size = 14 , weight = "bold" ), command = self .start_download )
56+ self .btn .pack (pady = 18 )
57+
58+ # Output folder label
59+ ctk .CTkLabel (self , text = f"Saves to: { DOWNLOAD_DIR } " , text_color = "gray" ,
60+ font = ctk .CTkFont (size = 11 )).pack ()
61+
62+ def start_download (self ):
63+ url = self .url_entry .get ().strip ()
64+ if not url :
65+ self .status .configure (text = "⚠️ Please enter a URL" , text_color = "orange" )
66+ return
67+ self .btn .configure (state = "disabled" , text = "Downloading..." )
68+ self .progress .set (0 )
69+ self .status .configure (text = "Starting..." , text_color = "gray" )
70+ threading .Thread (target = self .download , args = (url ,), daemon = True ).start ()
71+
72+ def download (self , url ):
73+ fmt = self .format_var .get ()
74+ quality = self .quality_var .get ()
75+
76+ def progress_hook (d ):
77+ if d ['status' ] == 'downloading' :
78+ total = d .get ('total_bytes' ) or d .get ('total_bytes_estimate' , 0 )
79+ downloaded = d .get ('downloaded_bytes' , 0 )
80+ if total :
81+ pct = downloaded / total
82+ self .progress .set (pct )
83+ self .status .configure (text = f"Downloading... { int (pct * 100 )} %" , text_color = "gray" )
84+ elif d ['status' ] == 'finished' :
85+ self .status .configure (text = "Processing..." , text_color = "gray" )
86+
87+ if fmt == "mp3" :
88+ ydl_opts = {
89+ 'format' : 'bestaudio/best' ,
90+ 'outtmpl' : os .path .join (DOWNLOAD_DIR , '%(title)s.%(ext)s' ),
91+ 'progress_hooks' : [progress_hook ],
92+ 'postprocessors' : [{'key' : 'FFmpegExtractAudio' , 'preferredcodec' : 'mp3' , 'preferredquality' : '192' }],
93+ }
94+ else :
95+ fmt_str = 'bestvideo+bestaudio/best' if quality == 'best' else f'bestvideo[height<={ quality } ]+bestaudio/best'
96+ ydl_opts = {
97+ 'format' : fmt_str ,
98+ 'outtmpl' : os .path .join (DOWNLOAD_DIR , '%(title)s.%(ext)s' ),
99+ 'progress_hooks' : [progress_hook ],
100+ 'merge_output_format' : 'mp4' ,
101+ }
102+
103+ try :
104+ with yt_dlp .YoutubeDL (ydl_opts ) as ydl :
105+ ydl .download ([url ])
106+ self .progress .set (1 )
107+ self .status .configure (text = "✅ Done! File saved to ~/Downloads" , text_color = "#4CAF50" )
108+ except Exception as e :
109+ self .status .configure (text = f"❌ Error: { str (e )[:60 ]} " , text_color = "#f44336" )
110+ finally :
111+ self .btn .configure (state = "normal" , text = "⬇ Convert & Download" )
112+
113+ if __name__ == "__main__" :
114+ app = YTConverterApp ()
115+ app .mainloop ()
0 commit comments