Skip to content

Commit adcf820

Browse files
authored
Create YouTube-video-downloader.py
1 parent a760f58 commit adcf820

1 file changed

Lines changed: 179 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import os
2+
import threading
3+
import subprocess
4+
import tkinter as tk
5+
from tkinter import messagebox
6+
from PIL import Image, ImageTk
7+
import requests
8+
import io
9+
import ttkbootstrap as tb
10+
from ttkbootstrap.constants import *
11+
12+
# ---------------- HELPERS ---------------- #
13+
def format_size(bytes_size):
14+
if not bytes_size:
15+
return "N/A"
16+
try:
17+
bytes_size = int(bytes_size)
18+
except:
19+
return "N/A"
20+
for unit in ['B','KB','MB','GB','TB']:
21+
if bytes_size < 1024:
22+
return f"{bytes_size:.1f} {unit}"
23+
bytes_size /= 1024
24+
return f"{bytes_size:.1f} PB"
25+
26+
def run_command(command):
27+
try:
28+
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
29+
output_lines = []
30+
for line in proc.stdout:
31+
output_lines.append(line.strip())
32+
proc.wait()
33+
return output_lines
34+
except Exception as e:
35+
messagebox.showerror("Error", str(e))
36+
return []
37+
38+
def download_with_progress(video_url, fmt_code, progress_var):
39+
"""Download selected format and update progress bar"""
40+
downloads_folder = "downloads"
41+
os.makedirs(downloads_folder, exist_ok=True)
42+
output_template = os.path.join(downloads_folder, "%(title)s.%(ext)s")
43+
command = [
44+
"yt-dlp", "-f", fmt_code, "-o", output_template, "--newline", video_url
45+
]
46+
try:
47+
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
48+
for line in proc.stdout:
49+
if "%" in line:
50+
# Extract progress %
51+
try:
52+
percent = float(line.split("%")[0].split()[-1])
53+
progress_var.set(percent)
54+
except:
55+
pass
56+
proc.wait()
57+
progress_var.set(100)
58+
messagebox.showinfo("Downloaded", f"Video saved to {downloads_folder}")
59+
except Exception as e:
60+
messagebox.showerror("Error", str(e))
61+
62+
def parse_formats(output_lines):
63+
"""Return video/audio formats sorted high->low"""
64+
formats = []
65+
for line in output_lines:
66+
if not line or line.startswith("format code"):
67+
continue
68+
parts = line.split()
69+
if len(parts) < 3:
70+
continue
71+
fmt_code = parts[0]
72+
res = parts[1]
73+
type_ = parts[2]
74+
if "video" not in type_.lower() and "audio" not in type_.lower():
75+
continue
76+
size = "-"
77+
if len(parts) > 3 and parts[-1].endswith(("KiB","MiB","GiB")):
78+
size = parts[-1]
79+
sort_key = 0
80+
if "video" in type_.lower() and res != "audio":
81+
try: sort_key = int(res.replace("p",""))
82+
except: sort_key=0
83+
formats.append({"fmt_code": fmt_code, "res": res, "type": type_, "size": size, "sort_key": sort_key})
84+
formats.sort(key=lambda x: x["sort_key"], reverse=True)
85+
return formats
86+
87+
def fetch_thumbnail(video_url):
88+
"""Fetch video thumbnail"""
89+
try:
90+
cmd = ["yt-dlp", "--get-thumbnail", video_url]
91+
result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
92+
url = result.stdout.strip()
93+
if not url:
94+
return None
95+
resp = requests.get(url)
96+
img = Image.open(io.BytesIO(resp.content))
97+
img.thumbnail((320, 180))
98+
return ImageTk.PhotoImage(img)
99+
except:
100+
return None
101+
102+
# ---------------- GUI FUNCTIONS ---------------- #
103+
def fetch_formats_gui():
104+
video_url = url_entry.get().strip()
105+
if not video_url:
106+
messagebox.showwarning("Input Needed", "Please enter a YouTube URL.")
107+
return
108+
fetching_label.config(text="Fetching formats...")
109+
110+
def worker():
111+
# Clear previous
112+
for widget in list_frame.winfo_children():
113+
widget.destroy()
114+
# Fetch thumbnail
115+
thumb = fetch_thumbnail(video_url)
116+
if thumb:
117+
thumb_label.config(image=thumb)
118+
thumb_label.image = thumb
119+
120+
output_lines = run_command(["yt-dlp", "-F", video_url])
121+
if not output_lines:
122+
fetching_label.config(text="")
123+
messagebox.showerror("Error", "Failed to fetch formats.")
124+
return
125+
formats = parse_formats(output_lines)
126+
127+
# Headers
128+
headers = ["Format", "Resolution", "Type", "Size", "Download"]
129+
for c,h in enumerate(headers):
130+
tb.Label(list_frame, text=h, font=("Segoe UI",10,"bold")).grid(row=0,column=c,padx=5,pady=2)
131+
# Add rows
132+
for r_idx, fmt in enumerate(formats,start=1):
133+
bg_color = "#2c2f33" if r_idx % 2==0 else "#23272a"
134+
tb.Label(list_frame, text=fmt["fmt_code"], background=bg_color).grid(row=r_idx,column=0,padx=2,sticky="w")
135+
tb.Label(list_frame, text=fmt["res"], background=bg_color).grid(row=r_idx,column=1,padx=2,sticky="w")
136+
tb.Label(list_frame, text=fmt["type"], background=bg_color).grid(row=r_idx,column=2,padx=2,sticky="w")
137+
tb.Label(list_frame, text=fmt["size"], background=bg_color).grid(row=r_idx,column=3,padx=2,sticky="w")
138+
progress_var = tk.DoubleVar()
139+
progress = tb.Progressbar(list_frame, variable=progress_var, bootstyle="info-striped", length=120)
140+
progress.grid(row=r_idx,column=4,padx=2)
141+
btn = tb.Button(list_frame, text="⬇ Download", bootstyle="success",
142+
command=lambda f=fmt["fmt_code"], pv=progress_var: threading.Thread(target=download_with_progress,args=(video_url,f,pv),daemon=True).start())
143+
btn.grid(row=r_idx,column=5,padx=5)
144+
fetching_label.config(text="")
145+
146+
threading.Thread(target=worker, daemon=True).start()
147+
148+
# ---------------- GUI ---------------- #
149+
app = tb.Window(title="Professional YouTube Downloader", themename="darkly", size=(1000,650))
150+
151+
# URL entry
152+
top_frame = tb.Frame(app,padding=10)
153+
top_frame.pack(fill=tk.X)
154+
tb.Label(top_frame,text="YouTube URL:", font=("Segoe UI",12)).pack(side=tk.LEFT)
155+
url_entry = tb.Entry(top_frame,font=("Segoe UI",12))
156+
url_entry.pack(side=tk.LEFT,fill=tk.X,expand=True,padx=5)
157+
tb.Button(top_frame,text="Fetch Formats",bootstyle="primary", command=fetch_formats_gui).pack(side=tk.LEFT)
158+
159+
fetching_label = tb.Label(app,text="",font=("Segoe UI",10),foreground="yellow")
160+
fetching_label.pack(pady=2)
161+
162+
# Thumbnail
163+
thumb_label = tb.Label(app)
164+
thumb_label.pack(pady=5)
165+
166+
# List Frame with Scroll
167+
canvas_frame = tb.Frame(app)
168+
canvas_frame.pack(fill=tk.BOTH,expand=True,pady=10)
169+
canvas = tk.Canvas(canvas_frame)
170+
scrollbar = tk.Scrollbar(canvas_frame,orient="vertical",command=canvas.yview)
171+
scrollable_frame = tb.Frame(canvas)
172+
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
173+
canvas.create_window((0,0),window=scrollable_frame,anchor="nw")
174+
canvas.configure(yscrollcommand=scrollbar.set)
175+
canvas.pack(side="left",fill="both",expand=True)
176+
scrollbar.pack(side="right",fill="y")
177+
list_frame = scrollable_frame
178+
179+
app.mainloop()

0 commit comments

Comments
 (0)