Skip to content

Commit 3048bd1

Browse files
authored
Add files via upload
1 parent 03bb440 commit 3048bd1

File tree

4 files changed

+208
-0
lines changed

4 files changed

+208
-0
lines changed

orange.ico

32.2 KB
Binary file not shown.

orange.png

12.3 KB
Loading

orangyt.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import customtkinter as ctk
2+
import tkinter as tk
3+
import sys
4+
import os, threading, yt_dlp
5+
from urllib.request import build_opener, install_opener
6+
7+
opener = build_opener()
8+
opener.addheaders = [
9+
(
10+
"User-Agent",
11+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
12+
)
13+
]
14+
install_opener(opener)
15+
16+
# ---------------------------------------------------------------------------
17+
# Helper to load window icon (works for normal run & PyInstaller bundle)
18+
# ---------------------------------------------------------------------------
19+
20+
def load_icon(filename: str) -> tk.PhotoImage:
21+
"""Return a Tk PhotoImage for the given filename.
22+
The path is resolved correctly when bundled with PyInstaller."""
23+
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
24+
return tk.PhotoImage(file=os.path.join(base_path, filename))
25+
ctk.set_appearance_mode("dark")
26+
ctk.set_default_color_theme("blue")
27+
class OrangYT(ctk.CTk):
28+
def __init__(self):
29+
super().__init__()
30+
self.title("OrangYT Downloader")
31+
self.geometry("600x450")
32+
self.orange_colour = "#ff6b00"
33+
self.configure(fg_color=("gray90", "gray16"))
34+
# Set application icon
35+
self.icon = load_icon("orange.png") # keep reference to avoid GC
36+
self.iconphoto(False, self.icon)
37+
self.url_label = ctk.CTkLabel(self, text="Enter YouTube URL:", text_color=self.orange_colour)
38+
self.url_label.pack(pady=10)
39+
self.url_entry = ctk.CTkEntry(self, width=400, border_color=self.orange_colour, fg_color=("gray95", "gray20"))
40+
self.url_entry.pack(pady=5)
41+
self.format_frame = ctk.CTkFrame(self, fg_color="transparent")
42+
self.format_frame.pack(pady=20)
43+
self.format_var = ctk.StringVar(value="video")
44+
self.video_radio = ctk.CTkRadioButton(
45+
self.format_frame,
46+
text="Video",
47+
variable=self.format_var,
48+
value="video",
49+
command=self._update_format_options,
50+
fg_color=self.orange_colour,
51+
border_color=self.orange_colour,
52+
)
53+
self.video_radio.pack(side="left", padx=10)
54+
self.audio_radio = ctk.CTkRadioButton(
55+
self.format_frame,
56+
text="Audio",
57+
variable=self.format_var,
58+
value="audio",
59+
command=self._update_format_options,
60+
fg_color=self.orange_colour,
61+
border_color=self.orange_colour,
62+
)
63+
self.audio_radio.pack(side="left", padx=10)
64+
self.format_options_frame = ctk.CTkFrame(self, fg_color="transparent")
65+
self.format_options_frame.pack(pady=10)
66+
self.format_option_var = ctk.StringVar()
67+
self.format_menu = ctk.CTkOptionMenu(
68+
self.format_options_frame,
69+
variable=self.format_option_var,
70+
fg_color=self.orange_colour,
71+
button_color=self.orange_colour,
72+
button_hover_color=self.orange_colour,
73+
dropdown_hover_color=self.orange_colour,
74+
)
75+
self.format_menu.pack(pady=10)
76+
self.download_button = ctk.CTkButton(
77+
self,
78+
text="Download",
79+
command=self._start_download_thread,
80+
fg_color=self.orange_colour,
81+
hover_color=self.orange_colour,
82+
height=40,
83+
)
84+
self.download_button.pack(pady=20)
85+
self.progress_label = ctk.CTkLabel(self, text="", text_color=self.orange_colour)
86+
self.progress_label.pack(pady=10)
87+
# Available video formats (removed webm to avoid unwanted container)
88+
self.video_formats = ["mp4", "mkv"]
89+
self.audio_formats = ["mp3", "flac", "ogg", "wav", "m4a"]
90+
self._update_format_options()
91+
def _update_format_options(self):
92+
formats = self.video_formats if self.format_var.get() == "video" else self.audio_formats
93+
# Update dropdown values correctly using configure (required by customtkinter)
94+
self.format_menu.configure(values=formats)
95+
# Keep current selection if still valid, otherwise set to first option
96+
if self.format_option_var.get() not in formats:
97+
self.format_option_var.set(formats[0])
98+
99+
def _start_download_thread(self):
100+
self.download_button.configure(state="disabled")
101+
self.progress_label.configure(text="Starting download…")
102+
threading.Thread(target=self._download, daemon=True).start()
103+
# -------------------------------------------------------------------
104+
# core download logic using yt-dlp
105+
# -------------------------------------------------------------------
106+
def _download(self):
107+
try:
108+
url = self.url_entry.get().strip()
109+
if not url:
110+
raise ValueError("Please enter a YouTube URL")
111+
if "youtu.be" in url and not url.startswith("https://www.youtube.com/watch?v="):
112+
video_id = url.split("/")[-1]
113+
url = f"https://www.youtube.com/watch?v={video_id}"
114+
if not url.startswith(("http://", "https://")):
115+
url = "https://" + url
116+
format_type = self.format_var.get()
117+
selected_format = self.format_option_var.get()
118+
def hook(d):
119+
status = d.get("status")
120+
if status == "downloading":
121+
downloaded = d.get("downloaded_bytes", 0)
122+
total = d.get("total_bytes") or d.get("total_bytes_estimate")
123+
if total:
124+
pct_float = downloaded / total * 100
125+
mb_downloaded = downloaded / 1024 / 1024
126+
mb_total = total / 1024 / 1024
127+
self.progress_label.configure(
128+
text=f"Downloading: {pct_float:.1f}% (" \
129+
f"{mb_downloaded:.1f}/{mb_total:.1f} MB)"
130+
)
131+
else:
132+
# Fallback to percent string if total unknown
133+
pct = d.get("_percent_str", "0% ").strip().rstrip("%")
134+
try:
135+
pct_float = float(pct)
136+
self.progress_label.configure(text=f"Downloading: {pct_float:.1f}%")
137+
except ValueError:
138+
pass
139+
elif status == "finished":
140+
self.progress_label.configure(text="Post-processing…")
141+
ydl_opts = {
142+
"quiet": True,
143+
"progress_hooks": [hook],
144+
# Correct template placeholders to include actual title and extension
145+
"outtmpl": "%(title)s.%(ext)s",
146+
# Remove intermediary files created during post-processing
147+
"keepvideo": False,
148+
}
149+
if format_type == "video":
150+
# Select streams that match chosen container when possible
151+
if selected_format == "mp4":
152+
# Prefer MP4 video+audio streams to avoid later conversion
153+
ydl_opts["format"] = (
154+
"bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"
155+
)
156+
else:
157+
# Grab best quality, we'll convert afterwards
158+
ydl_opts["format"] = "bestvideo+bestaudio/best"
159+
160+
# Always run video converter so final container matches choice
161+
ydl_opts["merge_output_format"] = selected_format # ensure final container
162+
ydl_opts["postprocessors"] = [
163+
{
164+
"key": "FFmpegVideoConvertor",
165+
"preferedformat": selected_format,
166+
}
167+
]
168+
else:
169+
ydl_opts["format"] = "bestaudio/best"
170+
# Map GUI selection to correct ffmpeg codec name if necessary
171+
codec_map = {
172+
"ogg": "vorbis", # OGG container uses the Vorbis codec
173+
}
174+
ydl_opts["postprocessors"] = [
175+
{
176+
"key": "FFmpegExtractAudio",
177+
"preferredcodec": codec_map.get(selected_format, selected_format),
178+
"preferredquality": "192",
179+
}
180+
]
181+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
182+
info = ydl.extract_info(url, download=True)
183+
output_path = ydl.prepare_filename(info)
184+
185+
# --- cleanup of any leftover temporary files (e.g., webm, m4a) ---
186+
base_title = os.path.splitext(output_path)[0]
187+
for ext in ("webm", "m4a", "mp4", "mkv"):
188+
tmp_path = f"{base_title}.{ext}"
189+
# keep the final chosen file, delete others if present
190+
if ext != selected_format and os.path.exists(tmp_path):
191+
try:
192+
os.remove(tmp_path)
193+
except OSError:
194+
pass
195+
196+
self.progress_label.configure(
197+
text=f"Download complete!\nSaved as: {os.path.basename(base_title + '.' + selected_format)}"
198+
)
199+
except Exception as exc:
200+
self.progress_label.configure(text=f"Error: {exc}")
201+
finally:
202+
self.download_button.configure(state="normal")
203+
if __name__ == "__main__":
204+
app = OrangYT()
205+
app.mainloop()

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
yt-dlp
2+
moviepy
3+
customtkinter

0 commit comments

Comments
 (0)