├── YDICO.ico
├── theme_config.json
├── images
├── home_dark.png
├── home_light.png
├── search_results.png
├── video_downloader.png
├── advanced_settings.png
├── results_downloader.png
└── playlist_downloader.png
├── requirements.txt
├── README.md
└── main.py
/YDICO.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/YDICO.ico
--------------------------------------------------------------------------------
/theme_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bg_theme": "System",
3 | "default_color": "blue"
4 | }
--------------------------------------------------------------------------------
/images/home_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/home_dark.png
--------------------------------------------------------------------------------
/images/home_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/home_light.png
--------------------------------------------------------------------------------
/images/search_results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/search_results.png
--------------------------------------------------------------------------------
/images/video_downloader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/video_downloader.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytube
2 | pytubefix
3 | customtkinter
4 | youtube_transcript_api
5 | pillow
6 | requests
7 |
--------------------------------------------------------------------------------
/images/advanced_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/advanced_settings.png
--------------------------------------------------------------------------------
/images/results_downloader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/results_downloader.png
--------------------------------------------------------------------------------
/images/playlist_downloader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyman007/YouTube-Downloader/HEAD/images/playlist_downloader.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # **YouTube Downloader**
6 |
7 |
8 |
9 | > **Simple & modern YouTube Downloader to download videos, playlists, subtitles and search queries.**
10 |
11 |

12 |
13 |
14 | ## Features
15 |
16 | - 🔗 Download videos by URL.
17 | - 🗒️ Download playlists. With an option to select which videos to download.
18 | - 💬 Download the attached subtitles. Or translate it to your prefered language.
19 | - 🔎 Search keywords and download videos from the search results.
20 | - 🔉 Download audio only
21 | - 🔖 Selectable qualities.
22 | - ⚙️ Advanced quality settings. (Change format, codec, bitrate, etc...)
23 | - 🖼️ Download thumbnails.
24 | - ⏯️ Supports resumability.
25 | - 📂 Change the download path.
26 | - 🌄 Simple & modern GUI.
27 | - 🌃 Customize your theme and default color.
28 |
29 |
30 | ## Screenshots
31 |
32 |
33 |
34 | | Home Page
35 |
36 |
37 |
38 |
39 |
40 |
41 | | Home Page (Light)
42 |
43 |
44 |
45 |
46 |
47 |
48 | | Advanced Quality Settings Page
49 |
50 |
51 |
52 |
53 |
54 |
55 | | Video Downloader Page
56 |
57 |
58 |
59 |
60 |
61 |
62 | | Playlist Downloader Page
63 |
64 |
65 |
66 |
67 |
68 |
69 | | Search Results Page
70 |
71 |
72 |
73 |
74 |
75 |
76 | | Results Downloader Page
77 |
78 |
79 |
80 |
81 | ## Installation Guide
82 | ### **For Windows:**
83 | #### **Quick Install (Recommended):**
84 | Download the exe for windows from the latest [release](https://github.com/MAyman007/YouTube-Downloader/releases).
85 |
86 | ---
87 |
88 | #### **Build From Source:**
89 |
90 |
91 | - Install python and git, then make sure both are added to your PATH.
92 |
93 | - Download FFmpeg and either:
94 |
95 | - Add it to your PATH, or
96 | - Place
ffmpeg.exe in the same folder as the script.
97 |
98 |
99 | - Git-clone this repo & change directory
100 |
101 | ```
102 | git clone https://github.com/MAyman007/YouTube-Downloader.git
103 |
104 | cd YouTube-Downloader
105 | ```
106 | - Create a virtual environment & activate it (optional)
107 |
108 | ```
109 | python -m venv venv
110 | venv\Scripts\Activate.ps1 (or venv\Scripts\activate.bat)
111 | ```
112 | - Install modules using pip
113 |
114 | ```
115 | pip install -r requirements.txt
116 | ```
117 | - Run the .py file!
118 |
119 | ```
120 | py main.py
121 | ```
122 |
123 |
124 | ### **For Linux:**
125 | #### **Quick Install (Recommended):**
126 | Download the prebuilt binary for Linux from the latest
127 | release
128 | and install
129 | ffmpeg
130 | (sudo apt install ffmpeg if you're on a Debian-Based distro).
131 |
132 | ---
133 |
134 | #### **Build From Source:**
135 |
136 | - Install the following packages:
137 |
138 |
139 | - Debian-Based distros (Debian, Ubuntu, Mint, etc.):
140 |
141 | ```
142 | sudo apt update
143 | sudo apt install -y python3 python3-pip python3-tk git ffmpeg
144 | ```
145 |
146 | - Arch-Based Distros (Arch, Manjaro, EndeavourOS, etc.):
147 |
148 | ```
149 | sudo pacman -Syu python tk git ffmpeg
150 | ```
151 |
152 | - Fedora / RHEL-Based Distros:
153 |
154 | ```
155 | sudo dnf install python3 python3-pip python3-tkinter git ffmpeg
156 | ```
157 |
158 | - openSUSE:
159 |
160 | ```
161 | sudo zypper install python3 python3-pip python3-tk git-core ffmpeg
162 | ```
163 |
164 |
165 |
166 |
167 | - Git-clone this repo & change directory
168 |
169 | ```
170 | git clone https://github.com/MAyman007/YouTube-Downloader.git
171 |
172 | cd YouTube-Downloader
173 | ```
174 |
175 | - Create a virtual environment & activate it
176 |
177 | ```
178 | python -m venv venv
179 | source venv/bin/activate
180 | ```
181 |
182 | -
183 | Install modules using pip
184 |
185 | ```
186 | pip install -r requirements.txt
187 | ```
188 |
189 | -
190 | Run the .py file!
191 |
192 | ```
193 | python3 main.py
194 | ```
195 |
196 |
197 |
198 | ## Support
199 |
200 | Have questions, feedback, or issues? open an [issue](https://github.com/MAyman007/YouTube-Downloader/issues)
201 |
202 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import re
2 | from tkinter import *
3 | from tkinter import filedialog, messagebox
4 | import customtkinter
5 | import pytube
6 | from pytubefix import YouTube, Playlist, Search, extract, request
7 | import pytubefix.request
8 | from youtube_transcript_api import YouTubeTranscriptApi
9 | from youtube_transcript_api.formatters import SRTFormatter
10 | import threading
11 | from threading import Thread
12 | import json
13 | from PIL import Image
14 | import urllib.request
15 | import os
16 | import io
17 | import reprlib
18 | import time
19 | import subprocess
20 | import webbrowser
21 | import shlex
22 | from sys import platform
23 | import requests
24 |
25 |
26 | # Get config prefences from JSON
27 |
28 | def get_bg_theme():
29 | try:
30 | with open("theme_config.json", "r") as f:
31 | theme = json.load(f)
32 | return theme["bg_theme"]
33 | except FileNotFoundError:
34 | with open("_internal/theme_config.json", "r") as f:
35 | theme = json.load(f)
36 | return theme["bg_theme"]
37 | def get_default_color():
38 | try:
39 | with open("theme_config.json", "r") as f:
40 | theme = json.load(f)
41 | return theme["default_color"]
42 | except FileNotFoundError:
43 | with open("_internal/theme_config.json", "r") as f:
44 | theme = json.load(f)
45 | return theme["default_color"]
46 |
47 | # Set themes
48 | customtkinter.set_appearance_mode(get_bg_theme())
49 | customtkinter.set_default_color_theme(get_default_color())
50 |
51 | # On closing
52 | def onClosing():
53 | # if messagebox.askokcancel("Quit", "Do you want to quit?"):
54 | root.destroy()
55 |
56 | # Create form
57 | root = customtkinter.CTk()
58 | width = 700
59 | height = 460
60 | x = (root.winfo_screenwidth() // 2) - (width // 2)
61 | y = (root.winfo_screenheight() // 2) - (height // 2)
62 | root.geometry(fr"{width}x{height}+{x}+{y}") # Centers the window
63 | root.resizable(False, False)
64 | if platform == "linux" or platform == "linux2": pass # Linux
65 | else:
66 | try: root.iconbitmap("YDICO.ico") # Windows
67 | except TclError: root.iconbitmap("_internal/YDICO.ico") # Windows
68 | root.title("YouTube Downloader")
69 | customtkinter.CTkLabel(root, text = "YouTube Downloader", font = ("arial bold", 45)).place(x = 140 , y = 20)
70 |
71 | # Link Entry Copy
72 | def linkCopy():
73 | start = link_entry.index("sel.first")
74 | end = link_entry.index("sel.last")
75 | to_copy = link_entry.get()[start:end]
76 | root.clipboard_append(to_copy)
77 |
78 | # Link Entry Cut
79 | def linkCut():
80 | start = link_entry.index("sel.first")
81 | end = link_entry.index("sel.last")
82 | to_copy = link_entry.get()[start:end]
83 | root.clipboard_append(to_copy) # Get text from clipboard
84 | try: # Delete the selected text
85 | start = link_entry.index("sel.first")
86 | end = link_entry.index("sel.last")
87 | link_entry.delete(start, end)
88 | except TclError:
89 | pass # Nothing was selected, so paste won't delete
90 |
91 | # Link Entry Paste
92 | def linkPaste():
93 | clipboard = root.clipboard_get() # Get text from clipboard
94 | clipboard = clipboard.replace("\n", "\\n")
95 | try: # delete the selected text, if any
96 | start = link_entry.index("sel.first")
97 | end = link_entry.index("sel.last")
98 | link_entry.delete(start, end)
99 | except TclError:
100 | pass # Nothing was selected, so paste won't delete
101 | link_entry.insert("insert", clipboard) # Insert the modified clipboard contents
102 |
103 | # Right-Click menu
104 | m = Menu(root, tearoff = 0)
105 | m.add_command(label ="Cut", font = ("arial", 11), command = linkCut)
106 | m.add_command(label ="Copy", font = ("arial", 11), command = linkCopy)
107 | m.add_command(label ="Paste", font = ("arial", 11), command = linkPaste)
108 | def linkRightClickMenu(event):
109 | try: m.tk_popup(event.x_root, event.y_root)
110 | finally: m.grab_release()
111 |
112 | # Search Entry Copy
113 | def searchCopy():
114 | start = link_entry.index("sel.first")
115 | end = link_entry.index("sel.last")
116 | to_copy = link_entry.get()[start:end]
117 | root.clipboard_append(to_copy)
118 |
119 | # Search Entry Cut
120 | def searchCut():
121 | start = search_entry.index("sel.first")
122 | end = search_entry.index("sel.last")
123 | to_copy = search_entry.get()[start:end]
124 | root.clipboard_append(to_copy) # Get text from clipboard
125 | try: # Delete the selected text
126 | start = search_entry.index("sel.first")
127 | end = search_entry.index("sel.last")
128 | search_entry.delete(start, end)
129 | except TclError:
130 | pass # Nothing was selected, so paste won't delete
131 |
132 | # Search Entry Paste
133 | def searchPaste():
134 | clipboard = root.clipboard_get() # Get text from clipboard
135 | clipboard = clipboard.replace("\n", "\\n")
136 | try: # Delete the selected text, if any
137 | start = search_entry.index("sel.first")
138 | end = search_entry.index("sel.last")
139 | search_entry.delete(start, end)
140 | except TclError:
141 | pass # Nothing was selected, so paste won't delete
142 | search_entry.insert("insert", clipboard) # Insert the modified clipboard contents
143 |
144 | # Right-Click menu
145 | m2 = Menu(root, tearoff = 0)
146 | m2.add_command(label ="Cut", font = ("arial", 11), command = searchCut)
147 | m2.add_command(label ="Copy", font = ("arial", 11), command = searchCopy)
148 | m2.add_command(label ="Paste", font = ("arial", 11), command = searchPaste)
149 | def searchRightClickMenu(event):
150 | try: m2.tk_popup(event.x_root, event.y_root)
151 | finally: m2.grab_release()
152 |
153 | # Paste link widgets
154 | link_var = StringVar()
155 | customtkinter.CTkLabel(root, text = "Paste Your URL Here", font = ("arial bold", 25)).place(x = 105 , y = 120)
156 | link_entry = customtkinter.CTkEntry(root, width = 345, textvariable = link_var, corner_radius = 20)
157 | link_entry.place(x = 100 , y = 160)
158 | link_entry.bind("", linkRightClickMenu)
159 |
160 | # Type keywords widgets
161 | search_var = StringVar()
162 | customtkinter.CTkLabel(root, text = "Type Your Keywords Here", font = ("arial bold", 25)).place(x = 105 , y = 220)
163 | search_entry = customtkinter.CTkEntry(root, width = 345, textvariable = search_var, corner_radius = 20)
164 | search_entry.place(x = 100 , y = 260)
165 | search_entry.bind("", searchRightClickMenu)
166 |
167 | def check_youtube_link(link):
168 | video_pattern = re.compile(r"(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)[\w\-]+")
169 | playlist_pattern = re.compile(r"(https?://)?(www\.)?youtube\.com/playlist\?list=[\w\-]+")
170 | if playlist_pattern.match(link):
171 | return "Playlist Link"
172 | elif video_pattern.match(link):
173 | return "Video Link"
174 | else:
175 | return "Invalid YouTube Link"
176 |
177 | # Quality selections for downloads
178 | quality_var = IntVar()
179 | def downloadQualitySelect(quality):
180 | url_text = link_var.get()
181 | url_validaty = check_youtube_link(url_text)
182 | if url_validaty == "Invalid YouTube Link":
183 | whenError()
184 | return messagebox.showerror(title = "URL is invalid", message = "Please enter a vaild URL.")
185 | if quality == "Video: 1080p": quality_var.set(137)
186 | elif quality == "Video: 720p": quality_var.set(22)
187 | elif quality == "Video: 480p": quality_var.set(135)
188 | elif quality == "Video: 360p": quality_var.set(18)
189 | elif quality == "Video: 240p": quality_var.set(133)
190 | elif quality == "Video: 144p": quality_var.set(160)
191 | elif quality == "Audio: 160kbps": quality_var.set(251)
192 | elif quality == "Audio: 128kbps": quality_var.set(140)
193 | elif quality == "Audio: 70kbps": quality_var.set(250)
194 | elif quality == "Audio: 50kbps": quality_var.set(249)
195 | else: quality_var.set(0)
196 | global link
197 | link = link_var.get()
198 | if url_validaty == "Playlist Link": threading.Thread(target = PlaylistWindow).start()
199 | else: threading.Thread(target = DownlaodWindow).start()
200 | loading_optionmenu = download_optionmenu
201 | threading.Thread(target = Loading, args = (loading_optionmenu,)).start()
202 | download_quality_list = ["Video: 1080p", "Video: 720p", "Video: 480p", "Video: 360p", "Video: 240p", "Video: 144p", "Audio: 160kbps", "Audio: 128kbps", "Audio: 70kbps", "Audio: 50kbps"]
203 | download_optionmenu = customtkinter.CTkOptionMenu(root, width = 175, height = 35, font = ("arial bold", 25), values = download_quality_list, command = downloadQualitySelect, corner_radius = 15)
204 | download_optionmenu.place(x = 460 , y = 155)
205 | download_optionmenu.set("Download")
206 |
207 | # Quality selections for search
208 | quality_var = IntVar()
209 | def searchQualitySelect(quality):
210 | search_text = search_var.get()
211 | if search_text == "":
212 | whenError()
213 | return messagebox.showerror(title = "Entry is Empty", message = "Please type something.")
214 | elif len(search_text) < 3:
215 | whenError()
216 | return messagebox.showerror(title = "Characters Not Enough", message = "Please type at least 3 characters.")
217 | if quality == "Video: 1080p": quality_var.set(137)
218 | elif quality == "Video: 720p": quality_var.set(22)
219 | elif quality == "Video: 480p": quality_var.set(135)
220 | elif quality == "Video: 360p": quality_var.set(18)
221 | elif quality == "Video: 240p": quality_var.set(133)
222 | elif quality == "Video: 144p": quality_var.set(160)
223 | elif quality == "Audio: 160kbps": quality_var.set(251)
224 | elif quality == "Audio: 128kbps": quality_var.set(140)
225 | elif quality == "Audio: 70kbps": quality_var.set(250)
226 | elif quality == "Audio: 50kbps": quality_var.set(249)
227 | else: quality_var.set(0)
228 | global search
229 | search = search_var.get()
230 | loading_optionmenu = search_optionmenu
231 | search_optionmenu.configure(corner_radius = 15)
232 | threading.Thread(target = SearchWindow).start()
233 | threading.Thread(target = Loading, args = (loading_optionmenu,)).start()
234 | search_quality_list = ["Video: 1080p", "Video: 720p", "Video: 480p", "Video: 360p", "Video: 240p", "Video: 144p", "Audio: 160kbps", "Audio: 128kbps", "Audio: 70kbps", "Audio: 50kbps"]
235 | search_optionmenu = customtkinter.CTkOptionMenu(root, width = 175, height = 35, font = ("arial bold", 25), values = search_quality_list, command = searchQualitySelect, corner_radius = 35)
236 | search_optionmenu.place(x = 460 , y = 255)
237 | search_optionmenu.set("Search")
238 |
239 | # Appearance theme
240 | def changeTheme(color):
241 | color = color.lower()
242 | themes_list = ["system", "dark", "light"]
243 | if color in themes_list:
244 | customtkinter.set_appearance_mode(color)
245 | to_change = "bg_theme"
246 | else:
247 | customtkinter.set_default_color_theme(color)
248 | customtkinter.CTkLabel(root, text = "(Restart to take full effect)", font = ("arial", 12)).place(x = 242 , y = 415)
249 | to_change = "default_color"
250 | try:
251 | with open("theme_config.json", "r", encoding="utf8") as f:
252 | theme = json.load(f)
253 | with open("theme_config.json", "w", encoding="utf8") as f:
254 | theme[to_change] = color
255 | json.dump(theme, f, sort_keys = True, indent = 4, ensure_ascii = False)
256 | except FileNotFoundError:
257 | with open("_internal/theme_config.json", "r", encoding="utf8") as f:
258 | theme = json.load(f)
259 | with open("_internal/theme_config.json", "w", encoding="utf8") as f:
260 | theme[to_change] = color
261 | json.dump(theme, f, sort_keys = True, indent = 4, ensure_ascii = False)
262 | customtkinter.CTkLabel(root, text = "Appearance Settings", font = ("arial bold", 19)).place(x = 34 , y = 340)
263 | customtkinter.CTkLabel(root, text = "Theme Mode: ", font = ("arial", 15)).place(x = 27 , y = 375)
264 | themes_menu = customtkinter.CTkOptionMenu(root, values = ["System", "Dark", "Light"], width = 110, command = changeTheme, corner_radius = 15)
265 | themes_menu.place(x = 127 , y = 375)
266 | themes_menu.set(get_bg_theme().title())
267 | customtkinter.CTkLabel(root, text = "Default Color: ", font = ("arial", 15)).place(x = 27 , y = 415)
268 | defaultcolor_menu = customtkinter.CTkOptionMenu(root, values = ["Blue", "Dark-blue", "Green"], width = 110, command = changeTheme, corner_radius = 15)
269 | defaultcolor_menu.place(x = 127 , y = 415)
270 | defaultcolor_menu.set(get_default_color().title())
271 |
272 | # Loading labels dots loop
273 | ploading_counter_var = StringVar()
274 | customtkinter.CTkLabel(root, textvariable = ploading_counter_var, font = ("arial", 22)).place(x = 530 , y = 208)
275 | def Loading(loading_optionmenu):
276 | loading_optionmenu.set("Loading")
277 | time.sleep(0.5)
278 | while True:
279 | if loading_optionmenu.get() == "Loading":
280 | loading_optionmenu.set("Loading.")
281 | time.sleep(0.5)
282 | else: break
283 | if loading_optionmenu.get() == "Loading.":
284 | loading_optionmenu.set("Loading..")
285 | time.sleep(0.5)
286 | else: break
287 | if loading_optionmenu.get() == "Loading..":
288 | loading_optionmenu.set("Loading...")
289 | time.sleep(0.5)
290 | else: break
291 | if loading_optionmenu.get() == "Loading...":
292 | loading_optionmenu.set("Loading")
293 | time.sleep(0.5)
294 | else: break
295 |
296 | # Downloading labels dots loop
297 | def Downloading(downloading_var):
298 | if downloading_var.get() == "Converting":
299 | while True:
300 | if downloading_var.get() == "Converting":
301 | downloading_var.set("Converting.")
302 | time.sleep(0.5)
303 | else: break
304 | if downloading_var.get() == "Converting.":
305 | downloading_var.set("Converting..")
306 | time.sleep(0.5)
307 | else: break
308 | if downloading_var.get() == "Converting..":
309 | downloading_var.set("Converting...")
310 | time.sleep(0.5)
311 | else: break
312 | if downloading_var.get() == "Converting...":
313 | downloading_var.set("Converting")
314 | time.sleep(0.5)
315 | else: break
316 | elif downloading_var.get() == "Merging":
317 | while True:
318 | if downloading_var.get() == "Merging":
319 | downloading_var.set("Merging.")
320 | time.sleep(0.5)
321 | else: break
322 | if downloading_var.get() == "Merging.":
323 | downloading_var.set("Merging..")
324 | time.sleep(0.5)
325 | else: break
326 | if downloading_var.get() == "Merging..":
327 | downloading_var.set("Merging...")
328 | time.sleep(0.5)
329 | else: break
330 | if downloading_var.get() == "Merging...":
331 | downloading_var.set("Merging")
332 | time.sleep(0.5)
333 | else: break
334 | elif downloading_var.get() == "Downloading audio":
335 | while True:
336 | if downloading_var.get() == "Downloading audio":
337 | downloading_var.set("Downloading audio.")
338 | time.sleep(0.5)
339 | else: break
340 | if downloading_var.get() == "Downloading audio.":
341 | downloading_var.set("Downloading audio..")
342 | time.sleep(0.5)
343 | else: break
344 | if downloading_var.get() == "Downloading audio..":
345 | downloading_var.set("Downloading audio...")
346 | time.sleep(0.5)
347 | else: break
348 | if downloading_var.get() == "Downloading audio...":
349 | downloading_var.set("Downloading audio")
350 | time.sleep(0.5)
351 | else: break
352 | else:
353 | downloading_var.set("Downloading")
354 | time.sleep(0.5)
355 | while True:
356 | if downloading_var.get() == "Downloading":
357 | downloading_var.set("Downloading.")
358 | time.sleep(0.5)
359 | else: break
360 | if downloading_var.get() == "Downloading.":
361 | downloading_var.set("Downloading..")
362 | time.sleep(0.5)
363 | else: break
364 | if downloading_var.get() == "Downloading..":
365 | downloading_var.set("Downloading...")
366 | time.sleep(0.5)
367 | else: break
368 | if downloading_var.get() == "Downloading...":
369 | downloading_var.set("Downloading")
370 | time.sleep(0.5)
371 | else: break
372 | dont_change = ["Canceled", "Paused", "Finished", " "]
373 | while True:
374 | time.sleep(1)
375 | if downloading_var.get() in dont_change: continue
376 | else: Downloading(downloading_var)
377 |
378 | # Integer -> time format
379 | def to_hms(s):
380 | m, s = divmod(s, 60)
381 | h, m = divmod(m, 60)
382 | return "{}:{:0>2}:{:0>2}".format(h, m, s)
383 |
384 | # Clean file names
385 | def clean_filename(name):
386 | forbidden_chars = "*\\/\"'.|?:<>"
387 | filename = ("".join([x if x not in forbidden_chars else " " for x in name])).replace(" ", " ").strip()
388 | if len(filename) >= 176:
389 | filename = filename[:170] + "..."
390 | return filename
391 |
392 | # When error happens return everything to normal in root
393 | def whenError():
394 | download_optionmenu.configure(state = "normal")
395 | search_optionmenu.configure(state = "normal")
396 | themes_menu.configure(state = "normal")
397 | defaultcolor_menu.configure(state = "normal")
398 | download_optionmenu.set("Download")
399 | search_optionmenu.set("Search")
400 | search_optionmenu.configure(corner_radius = 35)
401 | ploading_counter_var.set("")
402 | adv_quailty_button = customtkinter.CTkButton(root, text = "Advanced Quality Settings", width = 175, font = ("arial bold", 15), command = AdvancedWindow, corner_radius = 20)
403 | adv_quailty_button.place(x = 460 , y = 375)
404 | about_button = customtkinter.CTkButton(root, text = "About Developer", width = 175, font = ("arial bold", 15), command = AboutWindow, corner_radius = 20)
405 | about_button.place(x = 460 , y = 415)
406 | link_entry.configure(state = "normal")
407 | search_entry.configure(state = "normal")
408 |
409 | # When opens a new window from home page
410 | def whenOpening():
411 | download_optionmenu.configure(state = "disabled")
412 | search_optionmenu.configure(state = "disabled")
413 | themes_menu.configure(state = "disabled")
414 | defaultcolor_menu.configure(state = "disabled")
415 | ploading_counter_var.set("")
416 | adv_quailty_button = customtkinter.CTkButton(root, text = "Advanced Quality Settings", width = 175, font = ("arial bold", 15), state = "disabled", corner_radius = 20)
417 | adv_quailty_button.place(x = 460 , y = 375)
418 | about_button = customtkinter.CTkButton(root, text = "About Developer", width = 175, font = ("arial bold", 15), state = "disabled", corner_radius = 20)
419 | about_button.place(x = 460 , y = 415)
420 | link_entry.configure(state = "disabled")
421 | search_entry.configure(state = "disabled")
422 |
423 |
424 | # Advanced Settings Window
425 | def AdvancedWindow():
426 | global advWindow
427 | try:
428 | advWindow.deiconify()
429 | root.withdraw()
430 | except:
431 | # Form creating
432 | def onClosing():
433 | advWindow.destroy()
434 | root.deiconify()
435 | advWindow = customtkinter.CTkToplevel() # Toplevel object which will be treated as a new window
436 | advWindow.withdraw()
437 | advWindow.title("Advanced Quality Settings")
438 | width = 700
439 | height = 460
440 | x = (advWindow.winfo_screenwidth() // 2) - (width // 2)
441 | y = (advWindow.winfo_screenheight() // 2) - (height // 2)
442 | advWindow.geometry(fr"{width}x{height}+{x}+{y}")
443 | advWindow.maxsize(700, 460)
444 | advWindow.minsize(700, 460)
445 | if platform == "linux" or platform == "linux2": pass
446 | else:
447 | try: advWindow.iconbitmap("YDICO.ico") # Windows
448 | except TclError: advWindow.iconbitmap("_internal/YDICO.ico") # Windows
449 | advWindow.protocol("WM_DELETE_WINDOW", onClosing)
450 |
451 | # CRF slider function
452 | def crfSlider(num):
453 | if num == 23: crf_var.set(fr"{int(num)} (Default)")
454 | elif num == 0: crf_var.set(fr"{int(num)} (Loseless Quality)")
455 | elif num == 51: crf_var.set(fr"{int(num)} (Lowest Quality)")
456 | else: crf_var.set(int(num))
457 |
458 | # Radiobuttons function
459 | def radioDisableNormal():
460 | if video_crf_or_bitrate.get() == "crfr":
461 | crf_slider.configure(state = "normal")
462 | bitrate_entry.configure(state = "disabled")
463 | bitrate_entry.configure(border_color = "#565B5E")
464 | else:
465 | bitrate_entry.configure(state = "normal")
466 | crf_slider.configure(state = "disabled")
467 | if audio_quality_or_bitrate.get() == "bitrate":
468 | abitrate_combobox.configure(state = "readonly")
469 | aquality_combobox.configure(state = "disabled")
470 | else:
471 | aquality_combobox.configure(state = "readonly")
472 | abitrate_combobox.configure(state = "disabled")
473 |
474 | # Widgets vars
475 | video_crf_or_bitrate = StringVar()
476 | video_crf_or_bitrate.set("crfr")
477 | crf_var = StringVar()
478 | crf_var.set("23 (Default)")
479 | audio_quality_or_bitrate = StringVar()
480 | audio_quality_or_bitrate.set("bitrate")
481 | bitrate_entry_var = StringVar()
482 | # Widgets lists
483 | profile_combobox_list = ["High", "Main (Default)", "Baseline"] # -profile [selected option]
484 | tune_combobox_list = ["Film", "Animation", "Grain", "Still Image", "Fast Decode", "Zero Latency", "None (Default)"] # -tune [selected option]
485 | preset_combobox_list = ["Ultrafast", "Superfast", "Veryfast", "Faster", "Fast", "Medium (Default)", "Slow"] # -preset [prselected optioneset]
486 | format_combobox_list = ["MP4 (Default)", "M4A", "MKV"]
487 | codec_combobox_list = ["H.264 (Default)", "H.265", "AV1", "MPEG-4"]
488 | fps_combobox_list = ["5", "10", "15", "20", "23.976", "24", "30 (Default)", "40", "45", "50", "60"]
489 | aformat_combobox_list = ["MP3 (Default)", "WAV", "AAC", "OPUS", "FLAC"]
490 | abitrate_combobox_list = ["320", "192", "160", "128", "96", "70", "50"]
491 | aquality_combobox_list = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
492 | # Widgets placing
493 | customtkinter.CTkLabel(advWindow, text = "Video Settings", font = ("arial bold italic", 30)).place(x = 8 , y = 13)
494 | customtkinter.CTkLabel(advWindow, text = "Format:", font = ("arial bold", 20)).place(x = 20 , y = 55)
495 | format_combobox = customtkinter.CTkComboBox(advWindow, width = 133, height = 26, values = format_combobox_list, corner_radius = 15, state = "readonly")
496 | format_combobox._entry.configure(readonlybackground = format_combobox._apply_appearance_mode(format_combobox._fg_color))
497 | format_combobox.set("MP4 (Default)")
498 | format_combobox.place(x = 97 , y = 55)
499 | customtkinter.CTkLabel(advWindow, text = "Encoder Tune:", font = ("arial bold", 20)).place(x = 360 , y = 125)
500 | tune_combobox = customtkinter.CTkComboBox(advWindow, width = 137, height = 26, values = tune_combobox_list, corner_radius = 15, state = "readonly")
501 | tune_combobox._entry.configure(readonlybackground = tune_combobox._apply_appearance_mode(tune_combobox._fg_color))
502 | tune_combobox.set("None (Default)")
503 | tune_combobox.place(x = 505 , y =125)
504 | customtkinter.CTkLabel(advWindow, text = "Encoder Profile:", font = ("arial bold", 20)).place(x = 360 , y = 90)
505 | profile_combobox = customtkinter.CTkComboBox(advWindow, width = 137, height = 26, values = profile_combobox_list, corner_radius = 15, state = "readonly")
506 | profile_combobox._entry.configure(readonlybackground = profile_combobox._apply_appearance_mode(profile_combobox._fg_color))
507 | profile_combobox.set("Main (Default)")
508 | profile_combobox.place(x = 518 , y = 90)
509 | customtkinter.CTkLabel(advWindow, text = "Encoder Preset:", font = ("arial bold", 20)).place(x = 360 , y = 55)
510 | preset_combobox = customtkinter.CTkComboBox(advWindow, width = 150, height = 26, values = preset_combobox_list, corner_radius = 15, state = "readonly")
511 | preset_combobox._entry.configure(readonlybackground = preset_combobox._apply_appearance_mode(preset_combobox._fg_color))
512 | preset_combobox.set("Medium (Default)")
513 | preset_combobox.place(x = 519 , y = 55)
514 | customtkinter.CTkLabel(advWindow, text = "Codec:", font = ("arial bold", 20)).place(x = 20 , y = 90)
515 | codec_combobox = customtkinter.CTkComboBox(advWindow, width = 138, height = 26, values = codec_combobox_list, corner_radius = 15, state = "readonly")
516 | codec_combobox.place(x = 93 , y = 90)
517 | codec_combobox._entry.configure(readonlybackground = codec_combobox._apply_appearance_mode(codec_combobox._fg_color))
518 | codec_combobox.set("H.264 (Default)")
519 | customtkinter.CTkLabel(advWindow, text = "Framerate (FPS):", font = ("arial bold", 20)).place(x = 20 , y = 125)
520 | fps_combobox = customtkinter.CTkComboBox(advWindow, width = 120, height = 26, values = fps_combobox_list, corner_radius = 15, state = "readonly")
521 | fps_combobox.place(x = 185 , y = 125)
522 | fps_combobox._entry.configure(readonlybackground = fps_combobox._apply_appearance_mode(fps_combobox._fg_color))
523 | fps_combobox.set("30 (Default)")
524 | crf_radiobutton = customtkinter.CTkRadioButton(advWindow, text = "Constant Quality:", font = ("arial bold", 20), variable = video_crf_or_bitrate, value = "crfr", command = radioDisableNormal)
525 | crf_radiobutton.place(x = 20 , y = 163)
526 | customtkinter.CTkLabel(advWindow, textvariable = crf_var, font = ("arial", 17)).place(x = 520 , y = 161)
527 | crf_slider = customtkinter.CTkSlider(advWindow, corner_radius = 15, width = 300, from_ = 0, to = 51, number_of_steps = 51, command = crfSlider)
528 | crf_slider.place(x = 212 , y = 167)
529 | crf_slider.set(23)
530 | bitrate_radiobutton = customtkinter.CTkRadioButton(advWindow, text = "Total Bitrate (kbps):", font = ("arial bold", 20), variable = video_crf_or_bitrate, value = "bitrate", command = radioDisableNormal)
531 | bitrate_radiobutton.place(x = 20 , y = 198)
532 | bitrate_entry = customtkinter.CTkEntry(advWindow, textvariable = bitrate_entry_var, width = 100, height = 26, corner_radius = 15, state = "disabled")
533 | bitrate_entry.place(x = 238 , y = 198)
534 | customtkinter.CTkLabel(advWindow, text = "Audio Settings", font = ("arial bold italic", 30)).place(x = 8 , y = 238)
535 | customtkinter.CTkLabel(advWindow, text = "Format:", font = ("arial bold", 20)).place(x = 20 , y = 280)
536 | aformat_combobox = customtkinter.CTkComboBox(advWindow, width = 133, height = 26, values = aformat_combobox_list, corner_radius = 15, state = "readonly")
537 | aformat_combobox._entry.configure(readonlybackground = aformat_combobox._apply_appearance_mode(aformat_combobox._fg_color))
538 | aformat_combobox.set("MP3 (Default)")
539 | aformat_combobox.place(x = 96 , y = 280)
540 | abitrate_radiobutton = customtkinter.CTkRadioButton(advWindow, text = "Bitrate:", font = ("arial bold", 20), variable = audio_quality_or_bitrate, value = "bitrate", command = radioDisableNormal)
541 | abitrate_radiobutton.place(x = 20 , y = 318)
542 | abitrate_combobox = customtkinter.CTkComboBox(advWindow, width = 80, height = 26, values = abitrate_combobox_list, corner_radius = 15, state = "readonly")
543 | abitrate_combobox._entry.configure(readonlybackground = abitrate_combobox._apply_appearance_mode(abitrate_combobox._fg_color))
544 | abitrate_combobox.set("320")
545 | abitrate_combobox.place(x = 121 , y = 316)
546 | aquality_radiobutton = customtkinter.CTkRadioButton(advWindow, text = "Quality:", font = ("arial bold", 20), variable = audio_quality_or_bitrate, value = "quality", command = radioDisableNormal)
547 | aquality_radiobutton.place(x = 20 , y = 353)
548 | aquality_combobox = customtkinter.CTkComboBox(advWindow, width = 90, height = 26, values = aquality_combobox_list, corner_radius = 15, state = "disabled")
549 | aquality_combobox._entry.configure(readonlybackground = aquality_combobox._apply_appearance_mode(aquality_combobox._fg_color))
550 | aquality_combobox.place(x = 125 , y = 351)
551 |
552 | # Switch stuff
553 | global switch_value
554 | switch_value = "video and audio"
555 | def switchFunction():
556 | global switch_value
557 | if switch_value == "video and audio":
558 | switch_value = "audio only"
559 | format_combobox.configure(state = "disabled")
560 | codec_combobox.configure(state = "disabled")
561 | fps_combobox.configure(state = "disabled")
562 | crf_slider.configure(state = "disabled")
563 | bitrate_entry.configure(state = "disabled")
564 | crf_radiobutton.configure(state = "disabled")
565 | bitrate_radiobutton.configure(state = "disabled")
566 | preset_combobox.configure(state = "disabled")
567 | tune_combobox.configure(state = "disabled")
568 | profile_combobox.configure(state = "disabled")
569 | elif switch_value == "audio only":
570 | switch_value = "video and audio"
571 | format_combobox.configure(state = "readonly")
572 | codec_combobox.configure(state = "readonly")
573 | fps_combobox.configure(state = "readonly")
574 | if video_crf_or_bitrate.get() == "crfr": crf_slider.configure(state = "normal")
575 | else: bitrate_entry.configure(state = "normal")
576 | crf_radiobutton.configure(state = "normal")
577 | bitrate_radiobutton.configure(state = "normal")
578 | preset_combobox.configure(state = "normal")
579 | tune_combobox.configure(state = "normal")
580 | profile_combobox.configure(state = "normal")
581 |
582 | # Placing the switch and labels
583 | customtkinter.CTkLabel(advWindow, text = "Audio Only", font = ("arial", 15)).place(x = 27 , y = 417)
584 | switch = customtkinter.CTkSwitch(advWindow, text = "", command = switchFunction)
585 | switch.place(x = 110 , y = 420)
586 | switch.select()
587 | customtkinter.CTkLabel(advWindow, text = "Video & Audio", font = ("arial", 15)).place(x = 154 , y = 417)
588 |
589 | # Cancel
590 | def cancelButton():
591 | advWindow.destroy()
592 | root.deiconify()
593 |
594 | # Reset everything
595 | def resetButton():
596 | switch.select()
597 | if switch_value == "audio only": switchFunction()
598 | crf_slider.configure(state = "normal")
599 | bitrate_entry.configure(state = "disabled")
600 | abitrate_combobox.configure(state = "normal")
601 | aquality_combobox.configure(state = "disabled")
602 | format_combobox.set("MP4 (Default)")
603 | codec_combobox.set("H.264 (Default)")
604 | fps_combobox.set("30 (Default)")
605 | preset_combobox.set("Medium (Default)")
606 | tune_combobox.set("None (Default)")
607 | profile_combobox.set("Main (Default)")
608 | video_crf_or_bitrate.set("crfr")
609 | crfSlider(23)
610 | bitrate_entry_var.set("")
611 | aformat_combobox.set("MP3 (Default)")
612 | audio_quality_or_bitrate.set("bitrate")
613 | abitrate_combobox.set("320")
614 | aquality_combobox.set("")
615 |
616 | # Save the ffmpeg command
617 | def okButton():
618 | global ffmpeg_command, advanced_quality_settings, advanced_extention, fps
619 | fps = "30"
620 | ffmpeg_command = 'ffmpeg -i "input"'
621 | format_combobox.configure(border_color = "#565B5E")
622 | bitrate_entry.configure(border_color = "#565B5E")
623 | aformat_combobox.configure(border_color = "#565B5E")
624 | if switch_value == "video and audio":
625 | if format_combobox.get() == "MP4 (Default)": advanced_extention = "mp4"
626 | else: advanced_extention = format_combobox.get().lower()
627 | if codec_combobox.get() == "H.264 (Default)": codec = "libx264"
628 | elif codec_combobox.get() == "H.265": codec = "libx265"
629 | elif codec_combobox.get() == "AV1": codec = "libaom-av1"
630 | elif codec_combobox.get() == "MPEG-4": codec = "mpeg4"
631 | ffmpeg_command = ffmpeg_command + f' -c:v {codec}'
632 | if fps_combobox.get() == "30 (Default)": fps = "30"
633 | else: fps = fps_combobox.get()
634 | ffmpeg_command = ffmpeg_command + f' -filter:v fps=fps={fps}'
635 | if aformat_combobox.get() == "MP3 (Default)": format_codec = "libmp3lame"
636 | elif aformat_combobox.get() == "AAC": format_codec = "aac"
637 | elif aformat_combobox.get() == "OPUS": format_codec = "libopus"
638 | elif aformat_combobox.get() == "FLAC": format_codec = "flac"
639 | if format_combobox.get() == "M4A" and aformat_combobox.get() == "FLAC":
640 | format_combobox.configure(border_color = "red")
641 | aformat_combobox.configure(border_color = "red")
642 | return messagebox.showerror(title = "Formats Not Compatible", message = "M4A Container doesn't support FLAC format.")
643 | if profile_combobox.get() == "Main (Default)": profile = "main"
644 | else: profile = profile_combobox.get().lower()
645 | ffmpeg_command = ffmpeg_command + f' -profile {profile}'
646 | if preset_combobox.get() == "Medium (Default)": preset = "medium"
647 | else: preset = preset_combobox.get().lower()
648 | ffmpeg_command = ffmpeg_command + f' -preset {preset}'
649 | if tune_combobox.get() == "None (Default)":
650 | pass
651 | else:
652 | tune = tune_combobox.get().lower().replace(" ", "")
653 | ffmpeg_command = ffmpeg_command + f' -tune {tune}'
654 | if video_crf_or_bitrate.get() == "crfr":
655 | if crf_slider.get() == "23 (Default)": crf = "23"
656 | elif crf_slider.get() == "0 (Loseless Quality)": crf = "0"
657 | elif crf_slider.get() == "51 (Highest Quality)": crf = "51"
658 | else: crf = crf_slider.get()
659 | ffmpeg_command = ffmpeg_command + f' -crf {crf}'
660 | else:
661 | try:
662 | if int(bitrate_entry.get()) not in range(100, 50001):
663 | bitrate_entry.configure(border_color = "red")
664 | return messagebox.showerror(title = "Wrong Video Bitrate", message = "Please select a valid video bitrate (from 100 to 50000).")
665 | except ValueError:
666 | bitrate_entry.configure(border_color = "red")
667 | return messagebox.showerror(title = "Wrong Video Bitrate", message = "Please select a valid video bitrate (from 100 to 50000).")
668 | else: fps = fps_combobox.get()
669 | bitrate = bitrate_entry.get() + "K"
670 | ffmpeg_command = ffmpeg_command + f' -b:v {bitrate}'
671 | advanced_quality_settings = "video"
672 | else:
673 | advanced_quality_settings = "audio"
674 | if aformat_combobox.get() == "MP3 (Default)":
675 | format_codec = "libmp3lame"
676 | advanced_extention = "mp3"
677 | elif aformat_combobox.get() == "WAV":
678 | format_codec = "pcm_s32le"
679 | advanced_extention = "wav"
680 | elif aformat_combobox.get() == "AAC":
681 | format_codec = "aac"
682 | advanced_extention = "aac"
683 | elif aformat_combobox.get() == "OPUS":
684 | format_codec = "libopus"
685 | advanced_extention = "opus"
686 | elif aformat_combobox.get() == "FLAC":
687 | format_codec = "flac"
688 | advanced_extention = "flac"
689 | ffmpeg_command = ffmpeg_command + f' -c:a {format_codec}'
690 | if audio_quality_or_bitrate.get() == "bitrate":
691 | abitrate = abitrate_combobox.get() + "K"
692 | ffmpeg_command = ffmpeg_command + f' -b:a {abitrate}'
693 | else:
694 | ffmpeg_command = ffmpeg_command + f' -q:a {aquality_combobox.get()}'
695 | ffmpeg_command = ffmpeg_command + ' -progress pipe:1 "output"'
696 | print(ffmpeg_command)
697 | print(advanced_quality_settings)
698 | print(advanced_extention)
699 | advWindow.withdraw()
700 | root.deiconify()
701 | customtkinter.CTkLabel(root, text = "(Advanced Quality Settings will apply on your next downloads)", font = ("arial", 12)).place(x = 360 , y = 340)
702 |
703 | # Placing the buttons
704 | customtkinter.CTkButton(advWindow, text = "OK", font = ("arial bold", 20), width = 120, corner_radius = 20, command = okButton).place(x = 565 , y = 415)
705 | customtkinter.CTkButton(advWindow, text = "Reset", font = ("arial bold", 20), width = 120, corner_radius = 20, command = resetButton).place(x = 435 , y = 415)
706 | customtkinter.CTkButton(advWindow, text = "Cancel", font = ("arial bold", 20), width = 120, corner_radius = 20, command = cancelButton).place(x = 305 , y = 415)
707 | root.withdraw()
708 | advWindow.deiconify()
709 |
710 |
711 |
712 | # Advanced Settings Window
713 | def AboutWindow():
714 | global abtWindow
715 | def onClosing():
716 | abtWindow.destroy()
717 | root.deiconify()
718 |
719 | try:
720 | abtWindow.deiconify()
721 | root.withdraw()
722 | except:
723 | # Form creating
724 | abtWindow = customtkinter.CTkToplevel() # Toplevel object which will be treated as a new window
725 | abtWindow.withdraw()
726 | abtWindow.title("About Developer")
727 | width = 700
728 | height = 460
729 | x = (abtWindow.winfo_screenwidth() // 2) - (width // 2)
730 | y = (abtWindow.winfo_screenheight() // 2) - (height // 2)
731 | abtWindow.geometry(fr"{width}x{height}+{x}+{y}")
732 | abtWindow.maxsize(700, 460)
733 | abtWindow.minsize(700, 460)
734 | if platform == "linux" or platform == "linux2": pass
735 | else:
736 | try: abtWindow.iconbitmap("YDICO.ico") # Windows
737 | except TclError: abtWindow.iconbitmap("_internal/YDICO.ico") # Windows
738 | abtWindow.protocol("WM_DELETE_WINDOW", onClosing)
739 |
740 | # Back to home button
741 | back_button = customtkinter.CTkButton(abtWindow, text = "Back To Home", font = ("arial bold", 20), command = onClosing, corner_radius = 20)
742 | back_button.place(x = 20 , y = 420)
743 |
744 | # About info
745 | customtkinter.CTkLabel(abtWindow, text = "Developer:", font = ("arial bold", 30)).place(x = 20 , y = 15)
746 | customtkinter.CTkLabel(abtWindow, text = "Mohamed Ayman", font = ("arial bold", 25)).place(x = 185 , y = 20)
747 |
748 | customtkinter.CTkLabel(abtWindow, text = "Email:", font = ("arial bold", 30)).place(x = 20 , y = 65)
749 | customtkinter.CTkLabel(abtWindow, text = "mohamedayman011324@gmail.com", font = ("arial bold", 25)).place(x = 117 , y = 70)
750 |
751 | customtkinter.CTkLabel(abtWindow, text = "Github:", font = ("arial bold", 30)).place(x = 20 , y = 115)
752 | github_label = customtkinter.CTkLabel(abtWindow, text = "github.com/mayman007", font = ("arial bold", 25), text_color="blue", cursor="hand2")
753 | github_label.place(x = 137 , y = 120)
754 | github_label.bind("", lambda e: webbrowser.open_new("http://github.com/mayman007"))
755 |
756 | customtkinter.CTkLabel(abtWindow, text = "Website:", font = ("arial bold", 30)).place(x = 20 , y = 165)
757 | website_label = customtkinter.CTkLabel(abtWindow, text = "mohamedayman.pages.dev", font = ("arial bold", 25), text_color="blue", cursor="hand2")
758 | website_label.place(x = 152 , y = 170)
759 | website_label.bind("", lambda e: webbrowser.open_new("http://mohamedayman.pages.dev"))
760 |
761 | root.withdraw()
762 | abtWindow.deiconify()
763 |
764 |
765 | # Advanced settings button
766 | advanced_quality_settings = "no"
767 | adv_quailty_button = customtkinter.CTkButton(root, text = "Advanced Quality Settings", width = 175, font = ("arial bold", 15), command = AdvancedWindow, corner_radius = 20)
768 | adv_quailty_button.place(x = 460 , y = 375)
769 |
770 | # About button
771 | about_button = customtkinter.CTkButton(root, text = "About Developer", width = 175, font = ("arial bold", 15), command = AboutWindow, corner_radius = 20)
772 | about_button.place(x = 460 , y = 415)
773 |
774 |
775 | # Conversion function
776 | def Conversion(input, ext, seconds):
777 | global ffmpeg_command
778 | output = input.replace(fr").{ext}", fr"_advanced_settings_applied).{advanced_extention}")
779 | ffmpeg_command = ffmpeg_command.replace("input", input)
780 | ffmpeg_command = ffmpeg_command.replace("output", output)
781 | # Progress reader function
782 | def progress_reader(procs, q):
783 | while True:
784 | if procs.poll() is not None: break # Break if FFmpeg sun-process is closed
785 | progress_text = procs.stdout.readline() # Read line from the pipe
786 | # Break the loop if progress_text is None (when pipe is closed).
787 | if progress_text is None: break
788 | progress_text = progress_text.decode("utf-8") # Convert bytes array to strings
789 | # Look for "frame=xx"
790 | if progress_text.startswith("frame="):
791 | frame = int(progress_text.partition('=')[-1]) # Get the frame number
792 | q[0] = frame # Store the last sample
793 | # Count number of frames
794 | tot_n_frames = seconds * float(fps)
795 | # Execute FFmpeg as sub-process with stdout as a pipe
796 | # Redirect progress to stdout using -progress pipe:1 arguments
797 | process = subprocess.Popen(shlex.split(ffmpeg_command), stdout=subprocess.PIPE)
798 | q = [0] # We don't really need to use a Queue - use a list of size 1
799 | progress_reader_thread = Thread(target=progress_reader, args=(process, q)) # Initialize progress reader thread
800 | progress_reader_thread.start() # Start the thread
801 | while True:
802 | if process.poll() is not None: break # Break if FFmpeg sun-process is closed
803 | time.sleep(1) # Sleep 1 second (do some work...)
804 | n_frame = q[0] # Read last element from progress_reader - current encoded frame
805 | progress_percent = (n_frame/tot_n_frames)*100 # Convert to percentage.
806 | print(f'Progress [%]: {progress_percent:.2f} ') # Print the progress
807 | if ext == "mp4": converting_percentage_var.set(f'{progress_percent:.2f}%') # Show the progress
808 | else: pass # For some reson, progress doesn't get printed when it's an audio
809 | process.stdout.close() # Close stdin pipe.
810 | progress_reader_thread.join() # Join thread
811 | process.wait() # Wait for FFmpeg sub-process to finish
812 | ffmpeg_command = ffmpeg_command.replace(input, "input")
813 | ffmpeg_command = ffmpeg_command.replace(output, "output")
814 |
815 |
816 | # Download window
817 | def DownlaodWindow():
818 | # Starting
819 | def VideoStart():
820 | threading.Thread(target = Downloading, args = (downloading_var,)).start()
821 | threading.Thread(target = VideoDownloader).start()
822 |
823 | # Back home
824 | def backHome():
825 | download = downloading_var.get()
826 | non_downloading_list = ["", "Finished", "Canceled"]
827 | if download in non_downloading_list:
828 | pass
829 | else:
830 | msg_box = messagebox.askquestion(title = "Cancel Download",
831 | message = "Going back to home will cancel the current download.\n\nDo you wish to continue?",
832 | icon = "warning")
833 | if msg_box == "yes": pass
834 | else: return
835 | newWindow.destroy()
836 | root.deiconify()
837 |
838 | # Set path
839 | if platform == "linux" or platform == "linux2": path = fr"/home/{os.getlogin()}/Downloads"
840 | else: path = fr"C:/Users\{os.getlogin()}\Downloads"
841 | global directory
842 | directory = os.path.realpath(path) # Deafult path in case the user didn't choose
843 | def BrowseDir(): # Path function
844 | global directory2
845 | directory2 = filedialog.askdirectory()
846 | path_var.set(directory2)
847 |
848 | # When an error happens
849 | def whenVideoError():
850 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
851 | toggle_button.place(x = 550 , y = 347)
852 | cancel_button = customtkinter.CTkButton(newWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
853 | cancel_button.place(x = 595 , y = 347)
854 | download_button = customtkinter.CTkButton(newWindow, text = "Download", font = ("arial bold", 25), command = VideoStart)
855 | download_button.place(x = 540 , y = 306)
856 | path_button = customtkinter.CTkButton(newWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, command = BrowseDir, corner_radius = 20)
857 | path_button.place(x = 430 , y = 347)
858 | lang_choose.configure(state = "normal")
859 | try: adv_checkbox.configure(state = "normal")
860 | except: pass
861 | downloading_var.set(" ")
862 | progress_label.configure(text_color = "#DCE4EE")
863 | progress_size_label.configure(text_color = "#DCE4EE")
864 |
865 | # Captions download
866 | def CaptionsDownload():
867 | lang = lang_choose.get()
868 | if lang.lower() == "none":
869 | return
870 | elif lang.lower() == "arabic":
871 | lang = "ar"
872 | elif lang.lower() == "english":
873 | for transcript in transcript_list:
874 | if transcript.language_code == "en-US":
875 | lang = "en-US"
876 | break
877 | elif transcript.language_code == "en-UK":
878 | lang = "en-UK"
879 | break
880 | else:
881 | lang = "en"
882 | try: # Get the subtitle directly if it's there
883 | final = YouTubeTranscriptApi.get_transcript(video_id = video_id, languages = [lang])
884 | print("got one already there")
885 | sub = "subtitle"
886 | except: # If not then translate it
887 | translated = "no"
888 | en_list = ["en", "en-US","en-UK"]
889 | for transcript in transcript_list:
890 | if transcript.language_code in en_list: # Translate from English if it's there
891 | final = transcript.translate(lang).fetch()
892 | print(fr"translated from {transcript.language_code}")
893 | sub = "translated_subtitle"
894 | translated = "yes"
895 | if translated == "no": # Avoid translating twice
896 | final = transcript.translate(lang).fetch()
897 | print(fr"translated {transcript.language_code}")
898 | sub = "translated_subtitle"
899 | translated = "yes"
900 | else:
901 | pass
902 | formatter = SRTFormatter()
903 | srt_formatted = formatter.format_transcript(final)
904 | try:
905 | with open(fr"{directory2}/{clean_filename(url.title)}_{sub}_{lang}.srt", "w", encoding = "utf-8") as srt_file:
906 | srt_file.write(srt_formatted)
907 | except NameError:
908 | with open(fr"{directory}/{clean_filename(url.title)}_{sub}_{lang}.srt", "w", encoding = "utf-8") as srt_file:
909 | srt_file.write(srt_formatted)
910 |
911 | # Advanced checker
912 | def advancedChecker():
913 | global advanced_quality_settings
914 | if advanced_quality_settings == "no": advanced_quality_settings = "yes"
915 | elif advanced_quality_settings == "yes": advanced_quality_settings = "no"
916 |
917 | # Pause/Resume function
918 | def toggle_download():
919 | global is_paused
920 | is_paused = not is_paused
921 | if is_paused:
922 | toggle_button = customtkinter.CTkButton(newWindow, text = "▶️", font = ("arial", 15), fg_color = "grey14", hover_color = "gray10", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
923 | toggle_button.place(x = 550 , y = 347)
924 | downloading_var.set("Paused")
925 | else:
926 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", hover_color = "gray10", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
927 | toggle_button.place(x = 550 , y = 347)
928 | downloading_var.set("Downloading")
929 |
930 | # Cancel function
931 | def cancel_download():
932 | global is_cancelled
933 | is_cancelled = True
934 |
935 | # Open folder in file explorer when download is finished
936 | def openFile():
937 | try:
938 | try:
939 | dir2 = os.path.normpath(directory2)
940 | if platform == "linux" or platform == "linux2": subprocess.Popen(dir2)
941 | else: subprocess.Popen(f'explorer "{dir2}"')
942 | except NameError:
943 | if platform == "linux" or platform == "linux2": subprocess.Popen(directory)
944 | else: subprocess.Popen(f'explorer "{directory}"')
945 | except PermissionError:
946 | try: messagebox.showerror(title = "Permission Denied", message = fr"I do not have permission to open '{dir2}'")
947 | except NameError: messagebox.showerror(title = "Permission Denied", message = fr"I do not have permission to open '{directory}'")
948 |
949 | # One Video Downloader
950 | def VideoDownloader(event = None):
951 | # Preperations
952 | global is_paused, is_cancelled
953 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", hover_color = "gray10", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
954 | toggle_button.place(x = 550 , y = 347)
955 | cancel_button = customtkinter.CTkButton(newWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", hover_color = "red4", width = 80, height = 26, command = cancel_download, corner_radius = 20)
956 | cancel_button.place(x = 595 , y = 347)
957 | download_button = customtkinter.CTkButton(newWindow, text = "Download", font = ("arial bold", 25), state = "disabled", corner_radius = 20)
958 | download_button.place(x = 540 , y = 306)
959 | path_button = customtkinter.CTkButton(newWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", width = 5, state = "disabled", corner_radius = 20)
960 | path_button.place(x = 430 , y = 347)
961 | lang_choose.configure(state = "disabled")
962 | try: adv_checkbox.configure(state = "disabled")
963 | except: pass
964 | audio_tags_list = ["251" , "140" , "250" , "249"]
965 | non_progressive_list = ["137" , "22", "135" , "133", "160"]
966 | # Download subtitles if selected
967 | if caps == "yes": CaptionsDownload()
968 | else: pass
969 | # Progress stuff
970 | pytubefix.request.default_range_size = 2097152 # 2MB chunk size (update progress every 2MB)
971 | progress_label.configure(text_color = "green")
972 | progress_size_label.configure(text_color = "LightBlue")
973 |
974 | # If the quality is non progressive video (1080p, 480p, 240p and 144p)
975 | if quality in non_progressive_list:
976 | if quality == "137": video = url.streams.filter(res = "1080p").first()
977 | elif quality == "22": video = url.streams.filter(res = "720p").first()
978 | elif quality == "135": video = url.streams.filter(res = "480p").first()
979 | elif quality == "133": video = url.streams.filter(res = "240p").first()
980 | elif quality == "160": video = url.streams.filter(res = "144p").first()
981 | audio = url.streams.get_by_itag(251)
982 | size = video.filesize + audio.filesize
983 | try:
984 | vname = fr"{directory2}/{clean_filename(url.title)}_video.mp4"
985 | aname = fr"{directory2}/{clean_filename(url.title)}_audio.mp3"
986 | except NameError:
987 | vname = fr"{directory}/{clean_filename(url.title)}_video.mp4"
988 | aname = fr"{directory}/{clean_filename(url.title)}_audio.mp3"
989 | # Downlaod video
990 | try:
991 | with open(vname, "wb") as f:
992 | is_paused = is_cancelled = False
993 | video = request.stream(video.url) # Get an iterable stream
994 | downloaded = 0
995 | while True:
996 | if is_cancelled:
997 | downloading_var.set("Canceled")
998 | break
999 | if is_paused:
1000 | time.sleep(0.1)
1001 | continue
1002 | try: chunk = next(video, None) # Get next chunk of video
1003 | except Exception as e:
1004 | print(e)
1005 | return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
1006 | if chunk:
1007 | f.write(chunk)
1008 | # Update progress
1009 | downloaded += len(chunk)
1010 | remaining = size - downloaded
1011 | bytes_downloaded = size - remaining
1012 | percentage_of_completion = bytes_downloaded / size * 100
1013 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
1014 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
1015 | progressbar.set(percentage_of_completion/100)
1016 | else:
1017 | # When finished
1018 | break
1019 | except PermissionError:
1020 | whenVideoError()
1021 | path = vname.replace(fr"/{clean_filename(url.title)}_video.mp4", "")
1022 | return messagebox.showerror(title = "Permission Error", message = fr"I don't have permission to access '{path}'. Change the path or run me as administrator.")
1023 | except TclError:
1024 | whenVideoError()
1025 | path = vname.replace(fr"/{clean_filename(url.title)}_video.mp4", "")
1026 | return messagebox.showerror(title = "Folder Not Found", message = fr"'{path}' is not found. Change the path to an existing folder.")
1027 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1028 | toggle_button.place(x = 550 , y = 347)
1029 | cancel_button = customtkinter.CTkButton(newWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1030 | cancel_button.place(x = 595 , y = 347)
1031 | if is_cancelled:
1032 | pass
1033 | else: # Download audio
1034 | downloading_var.set("Downloading audio")
1035 | with open(aname, "wb") as f:
1036 | is_paused = is_cancelled = False
1037 | video = request.stream(audio.url) # Get an iterable stream
1038 | while True:
1039 | if is_paused:
1040 | time.sleep(0.1)
1041 | continue
1042 | try: chunk = next(video, None) # Get next chunk of video
1043 | except Exception as e:
1044 | print(e)
1045 | return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
1046 | if chunk:
1047 | f.write(chunk) # Download the chunk into the file
1048 | # Update progress
1049 | downloaded += len(chunk)
1050 | remaining = size - downloaded
1051 | bytes_downloaded = size - remaining
1052 | percentage_of_completion = bytes_downloaded / size * 100
1053 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
1054 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
1055 | progressbar.set(percentage_of_completion/100)
1056 | else:
1057 | # When finished
1058 | break
1059 | # Merge video and audio
1060 | downloading_var.set("Merging")
1061 | final_name = vname.replace("_video", fr"_({quality_string})")
1062 | cmd = f'ffmpeg -y -i "{aname}" -i "{vname}" -c copy "{final_name}"'
1063 | subprocess.call(cmd, shell=True)
1064 | os.remove(vname)
1065 | os.remove(aname)
1066 | # Convert if there is a conversion
1067 | if advanced_checker == "yes":
1068 | downloading_var.set("Converting")
1069 | Conversion(final_name, "mp4", url.length)
1070 | # Finished
1071 | newWindow.bell()
1072 | downloading_var.set("Finished")
1073 | converting_percentage_var.set("")
1074 | customtkinter.CTkButton(newWindow, text = "Open in File Explorer", font = ("arial bold", 20), command = openFile, corner_radius = 20).place(x = 460 , y = 420)
1075 | global convert_count
1076 | convert_count = 0
1077 |
1078 | # If the quality is 720p or 360p or audio
1079 | else:
1080 | video = url.streams.get_by_itag(quality)
1081 | size = video.filesize
1082 | if quality in audio_tags_list: ext = "mp3"
1083 | else: ext = "mp4"
1084 | try: vname = fr"{directory2}/{clean_filename(url.title)}_({quality_string}).{ext}"
1085 | except NameError: vname = fr"{directory}/{clean_filename(url.title)}_({quality_string}).{ext}"
1086 | try:
1087 | with open(vname, "wb") as f:
1088 | is_paused = is_cancelled = False
1089 | video = request.stream(video.url) # get an iterable stream
1090 | downloaded = 0
1091 | while True:
1092 | if is_cancelled:
1093 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1094 | toggle_button.place(x = 550 , y = 347)
1095 | cancel_button = customtkinter.CTkButton(newWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1096 | cancel_button.place(x = 595 , y = 347)
1097 | downloading_var.set("Canceled")
1098 | break
1099 | if is_paused:
1100 | time.sleep(0.5)
1101 | continue
1102 | try: chunk = next(video, None) # Get next chunk of video
1103 | except: return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
1104 | if chunk:
1105 | f.write(chunk) # Downlaod the chunk into the file
1106 | # Update progress
1107 | downloaded += len(chunk)
1108 | remaining = size - downloaded
1109 | bytes_downloaded = size - remaining
1110 | percentage_of_completion = bytes_downloaded / size * 100
1111 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
1112 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
1113 | progressbar.set(percentage_of_completion/100)
1114 | else:
1115 | # When finished
1116 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1117 | toggle_button.place(x = 550 , y = 347)
1118 | cancel_button = customtkinter.CTkButton(newWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1119 | cancel_button.place(x = 595 , y = 347)
1120 | # Convert if there is a conversion
1121 | if advanced_checker == "yes":
1122 | downloading_var.set("Converting")
1123 | Conversion(vname, ext, url.length)
1124 | newWindow.bell()
1125 | downloading_var.set("Finished")
1126 | converting_percentage_var.set("")
1127 | customtkinter.CTkButton(newWindow, text = "Open in File Explorer", font = ("arial bold", 20), command = openFile, corner_radius = 20).place(x = 460 , y = 420)
1128 | break
1129 | except PermissionError:
1130 | whenVideoError()
1131 | path = vname.replace(fr"/{clean_filename(url.title)}_({quality_string}).{ext}", "")
1132 | return messagebox.showerror(title = "Permission Error", message = fr"I don't have permission to access '{path}'. Change the path or run me as administrator.")
1133 | except FileNotFoundError:
1134 | whenVideoError()
1135 | path = vname.replace(fr"/{clean_filename(url.title)}_({quality_string}).{ext}", "")
1136 | return messagebox.showerror(title = "Folder Not Found", message = fr"'{path}' is not found. Change the path to an existing folder.")
1137 | if is_cancelled:
1138 | msg_box = messagebox.askquestion(title = "Delete Canceled File", message = fr"Do you want to delete '{vname}'?")
1139 | if msg_box == "yes": os.remove(vname)
1140 |
1141 |
1142 | whenOpening() # Disable widgets in root to load
1143 | try: # Get url object
1144 | global url
1145 | url = YouTube(link)
1146 | quality = str(quality_var.get())
1147 | if not "youtu" in link:
1148 | raise pytube.exceptions.RegexMatchError
1149 | except pytube.exceptions.RegexMatchError:
1150 | whenError()
1151 | return messagebox.showerror(title = "Link Not Valid", message = "Please enter a valid video link.")
1152 | global video
1153 | global size
1154 | try: # Get video and size
1155 | if quality == "0":
1156 | whenError()
1157 | return messagebox.showerror(title = "Format Not Selected", message = "Please select a format to download.")
1158 | elif quality == "137":
1159 | video = url.streams.filter(res = "1080p").first()
1160 | audio = url.streams.get_by_itag(251)
1161 | size = video.filesize + audio.filesize
1162 | elif quality == "22":
1163 | video = url.streams.filter(res = "720p").first()
1164 | audio = url.streams.get_by_itag(251)
1165 | size = video.filesize + audio.filesize
1166 | elif quality == "135":
1167 | video = url.streams.filter(res = "480p").first()
1168 | audio = url.streams.get_by_itag(251)
1169 | size = video.filesize + audio.filesize
1170 | elif quality == "133":
1171 | video = url.streams.filter(res = "240p").first()
1172 | audio = url.streams.get_by_itag(251)
1173 | size = video.filesize + audio.filesize
1174 | elif quality == "160":
1175 | video = url.streams.filter(res = "144p").first()
1176 | audio = url.streams.get_by_itag(251)
1177 | size = video.filesize + audio.filesize
1178 | else:
1179 | video = url.streams.get_by_itag(quality) # 1080p, 720, 360p, *audio
1180 | size = video.filesize
1181 | size_string = fr"{round(size/1024/1024, 2)} MB"
1182 | except urllib.error.URLError as e:
1183 | print(e)
1184 | whenError()
1185 | return messagebox.showerror(title = "Not Connected", message = "Please check your internet connection.")
1186 | except KeyError as e:
1187 | print(fr"KeyError: {e}")
1188 | whenError()
1189 | return messagebox.showerror(title = "Something Went Wrong", message = fr"I can't retrieve '{url.title}' at the moment. Change the selected quality or try again later.")
1190 | except AttributeError as e:
1191 | print(fr"AttributeError: {e}")
1192 | whenError()
1193 | return messagebox.showerror(title = "Quality Not Available",
1194 | message = fr"I can't retrieve '{url.title}' in the quality that you chose. Change the selected quality or try again later.")
1195 | except pytube.exceptions.LiveStreamError as e:
1196 | print(e)
1197 | whenError()
1198 | return messagebox.showerror(title = "Video is Live", message = "Can't download a live video.")
1199 | except pytube.exceptions.AgeRestrictedError as e:
1200 | print(e)
1201 | whenError()
1202 | return messagebox.showerror(title = "Age Restricted", message = "This video is age restricted.")
1203 | except pytube.exceptions.MembersOnly as e:
1204 | print(e)
1205 | whenError()
1206 | return messagebox.showerror(title = "Members Only", message = "This video is members only.")
1207 | except pytube.exceptions.VideoPrivate as e:
1208 | print(e)
1209 | whenError()
1210 | return messagebox.showerror(title = "Private Video", message = "This video is private.")
1211 | except pytube.exceptions.VideoRegionBlocked as e:
1212 | print(e)
1213 | whenError()
1214 | return messagebox.showerror(title = "Region Blocked", message = "This video is region blocked.")
1215 | except pytube.exceptions.VideoUnavailable as e:
1216 | print(e)
1217 | whenError()
1218 | return messagebox.showerror(title = "Video Unavailable", message = "This video is unavailable.")
1219 |
1220 | # Get transcripts
1221 | try:
1222 | video_id = extract.video_id(link)
1223 | transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
1224 | lang_choose_state = "readonly"
1225 | caps = "yes"
1226 | except:
1227 | lang_choose_state = "disabled"
1228 | caps = "no"
1229 |
1230 | # Getting video thumbnail
1231 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg").read()
1232 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
1233 | photo = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (270 , 150))
1234 |
1235 | # Video labels info
1236 | r = reprlib.Repr()
1237 | r.maxlist = 5 # max elements displayed for lists
1238 | r.maxstring = 50 # max characters displayed for strings
1239 | date_format = "%d/%m/%Y"
1240 | if quality == "160": quality_string = "144p"
1241 | elif quality == "133": quality_string = "240p"
1242 | elif quality == "18": quality_string = "360p"
1243 | elif quality == "135": quality_string = "480p"
1244 | elif quality == "22": quality_string = "720p"
1245 | elif quality == "137": quality_string = "1080p"
1246 | elif quality == "249": quality_string = "50kbps"
1247 | elif quality == "250": quality_string = "70kbps"
1248 | elif quality == "140": quality_string = "128kbps"
1249 | elif quality == "251": quality_string = "160kbps"
1250 |
1251 | # Video form creating
1252 | newWindow = customtkinter.CTkToplevel() # Toplevel object which will be treated as a new window
1253 | newWindow.withdraw()
1254 | newWindow.title("Video Downloader")
1255 | width = 700
1256 | height = 460
1257 | x = (newWindow.winfo_screenwidth() // 2) - (width // 2)
1258 | y = (newWindow.winfo_screenheight() // 2) - (height // 2)
1259 | newWindow.geometry(fr"{width}x{height}+{x}+{y}")
1260 | newWindow.maxsize(700, 460)
1261 | newWindow.minsize(700, 460)
1262 | if platform == "linux" or platform == "linux2": pass
1263 | else:
1264 | try: newWindow.iconbitmap("YDICO.ico") # Windows
1265 | except TclError: newWindow.iconbitmap("_internal/YDICO.ico") # Windows
1266 | newWindow.protocol("WM_DELETE_WINDOW", onClosing)
1267 | # newWindow.bind("", VideoDownloader)
1268 |
1269 | # Downloading label
1270 | downloading_var = StringVar()
1271 | customtkinter.CTkLabel(newWindow, textvariable = downloading_var, font = ("arial", 25)).place(x = 265 , y = 418)
1272 | global converting_percentage_var
1273 | converting_percentage_var = StringVar()
1274 | customtkinter.CTkLabel(newWindow, textvariable = converting_percentage_var, font = ("arial", 22)).place(x = 410 , y = 418)
1275 |
1276 | # Video labels
1277 | customtkinter.CTkLabel(newWindow, text = "", image = photo).pack()
1278 | customtkinter.CTkLabel(newWindow, text = "Video Title:", font = ("arial bold", 20)).place(x = 20 , y = 165)
1279 | customtkinter.CTkLabel(newWindow, text = r.repr(url.title), font = ("arial", 20)).place(x = 132 , y = 165)
1280 | customtkinter.CTkLabel(newWindow, text = "Channel:", font = ("arial bold", 20)).place(x = 20 , y = 195)
1281 | customtkinter.CTkLabel(newWindow, text = r.repr(url.author), font = ("arial", 20)).place(x = 108 , y = 195)
1282 | customtkinter.CTkLabel(newWindow, text = "Publish Date:", font = ("arial bold", 20)).place(x = 20 , y = 225)
1283 | customtkinter.CTkLabel(newWindow, text = url.publish_date.strftime(date_format), font = ("arial", 20)).place(x = 152 , y = 225)
1284 | customtkinter.CTkLabel(newWindow, text = "Length:", font = ("arial bold", 20)).place(x = 20 , y = 255)
1285 | customtkinter.CTkLabel(newWindow, text = to_hms(url.length), font = ("arial", 20)).place(x = 97 , y = 255)
1286 | customtkinter.CTkLabel(newWindow, text = "Quality:", font = ("arial bold", 20)).place(x = 20 , y = 285)
1287 | customtkinter.CTkLabel(newWindow, text = quality_string, font = ("arial", 20)).place(x = 97 , y = 285)
1288 | customtkinter.CTkLabel(newWindow, text = "File Size:", font = ("arial bold", 20)).place(x = 20 , y = 315)
1289 | customtkinter.CTkLabel(newWindow, text = size_string, font = ("arial", 20)).place(x = 112 , y = 315)
1290 |
1291 | # Get thumbnail
1292 | def download_thumbnail():
1293 | try:
1294 | try:
1295 | response = requests.get(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg")
1296 | response.raise_for_status()
1297 | except:
1298 | response = requests.get(f"https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg")
1299 | response.raise_for_status()
1300 | except requests.exceptions.ConnectionError:
1301 | return messagebox.showinfo(title = "Connection Error", message = fr"Check your internet connection and try again.")
1302 | thumb_dir = filedialog.askdirectory()
1303 | thumb_path = fr"{thumb_dir}/{clean_filename(url.title)}_thumbnail.png"
1304 | with open(thumb_path, 'wb') as file:
1305 | file.write(response.content)
1306 | messagebox.showinfo(title = "Thumbnail Downloaded", message = fr"Thumbnail has been downloaded successfully in '{thumb_dir}'")
1307 | thumbnail_button = customtkinter.CTkButton(newWindow, text = "Download Thumbnail", font = ("arial bold", 18), command = Thread(target = download_thumbnail).start, corner_radius = 20)
1308 | thumbnail_button.place(x = 480 , y = 260)
1309 |
1310 | # Path change
1311 | customtkinter.CTkLabel(newWindow, text = "Download Path:", font = ("arial bold", 20)).place(x = 20 , y = 345)
1312 | path_var = StringVar()
1313 | customtkinter.CTkEntry(newWindow, width = 245, textvariable = path_var, state = "disabled", height = 26, corner_radius = 20).place(x = 175 , y = 347)
1314 | try:
1315 | path_var.set(directory2)
1316 | except NameError:
1317 | path_var.set(directory)
1318 | path_button = customtkinter.CTkButton(newWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, height = 26, command = BrowseDir, corner_radius = 20)
1319 | path_button.place(x = 430 , y = 347)
1320 |
1321 | # Subtitle Combobox
1322 | customtkinter.CTkLabel(newWindow, text = "Subtitle:", font = ("arial bold", 20), corner_radius = 20).place(x = 325 , y = 315)
1323 | lang_choose = customtkinter.CTkComboBox(newWindow, width = 100, height = 26, values = ["None", "Arabic", "English"], state = lang_choose_state, corner_radius = 15)
1324 | lang_choose._entry.configure(readonlybackground = lang_choose._apply_appearance_mode(lang_choose._fg_color))
1325 | lang_choose.set("None")
1326 | lang_choose.place(x = 430 , y = 315)
1327 |
1328 | # Progress bar/labels
1329 | percentage_var = StringVar()
1330 | sizeprogress_var = StringVar()
1331 | progress_label = customtkinter.CTkLabel(newWindow, textvariable = percentage_var, font = ("arial", 22))
1332 | progress_label.place(x = 540 , y = 384)
1333 | progress_size_label = customtkinter.CTkLabel(newWindow, textvariable = sizeprogress_var, font = ("arial", 22))
1334 | progress_size_label.place(x = 624 , y = 384)
1335 | percentage_var.set("0.00%")
1336 | sizeprogress_var.set("0 MB")
1337 | progressbar = customtkinter.CTkProgressBar(newWindow, width = 505)
1338 | progressbar.place(x = 20 , y = 393)
1339 | progressbar.set(0)
1340 |
1341 | # Pause/Resume & Cancel buttons
1342 | toggle_button = customtkinter.CTkButton(newWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1343 | toggle_button.place(x = 550 , y = 347)
1344 | cancel_button = customtkinter.CTkButton(newWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1345 | cancel_button.place(x = 595 , y = 347)
1346 |
1347 | # Advanced quality settings check
1348 | global advanced_checker
1349 | advanced_checker = "no"
1350 | if not advanced_quality_settings == "no":
1351 | audio_quality_list = ["160kbps" , "128kbps" , "70kbps" , "50kbps"]
1352 | if advanced_quality_settings == "audio":
1353 | if quality_string in audio_quality_list:
1354 | adv_checkbox = customtkinter.CTkCheckBox(newWindow, text = "Apply Advanced Quality Settings", font = ("arial bold", 15), command = advancedChecker)
1355 | adv_checkbox.place(x = 410 , y = 225)
1356 | adv_checkbox.select()
1357 | advanced_checker = "yes"
1358 | else:
1359 | if not quality_string in audio_quality_list:
1360 | adv_checkbox = customtkinter.CTkCheckBox(newWindow, text = "Apply Advanced Quality Settings", font = ("arial bold", 15), command = advancedChecker)
1361 | adv_checkbox.place(x = 410 , y = 225)
1362 | adv_checkbox.select()
1363 | advanced_checker = "yes"
1364 |
1365 | # Download button
1366 | download_button = customtkinter.CTkButton(newWindow, text = "Download", font = ("arial bold", 25), command = VideoStart, corner_radius = 20)
1367 | download_button.place(x = 540 , y = 306)
1368 |
1369 | # Back to home button
1370 | back_button = customtkinter.CTkButton(newWindow, text = "Back To Home", font = ("arial bold", 20), command = backHome, corner_radius = 20)
1371 | back_button.place(x = 20 , y = 420)
1372 |
1373 | # Return to normal state in root
1374 | whenError()
1375 | root.withdraw()
1376 | newWindow.deiconify()
1377 |
1378 |
1379 | # Playlist window
1380 | def PlaylistWindow():
1381 | # Starting
1382 | def pVideoStart():
1383 | threading.Thread(target = Downloading, args = (downloading_var,)).start()
1384 | threading.Thread(target = PlaylistDownloader).start()
1385 |
1386 | # Back home
1387 | def backHome():
1388 | download = downloading_var.get()
1389 | non_downloading_list = ["", "Finished", "Canceled"]
1390 | if download in non_downloading_list:
1391 | pass
1392 | else:
1393 | msg_box = messagebox.askquestion(title = "Cancel Download",
1394 | message = "Going back to home will cancel the current download.\n\nDo you wish to continue?",
1395 | icon = "warning")
1396 | if msg_box == "yes": pass
1397 | else: return
1398 | pWindow.destroy()
1399 | root.deiconify()
1400 |
1401 | # Set path
1402 | if platform == "linux" or platform == "linux2": path = fr"/home/{os.getlogin()}/Downloads"
1403 | else: path = fr"C:/Users\{os.getlogin()}\Downloads"
1404 | global directory
1405 | directory = os.path.realpath(path) # Deafult path in case the user didn't choose
1406 | def pBrowseDir(): # Path function
1407 | global directory2
1408 | directory2 = filedialog.askdirectory()
1409 | ppath_var.set(directory2)
1410 |
1411 | # When an error happens
1412 | def whenPlaylistError():
1413 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1414 | toggle_button.place(x = 550 , y = 347)
1415 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1416 | cancel_button.place(x = 595 , y = 347)
1417 | download_button = customtkinter.CTkButton(pWindow, text = "Download", font = ("arial bold", 25), command = pVideoStart)
1418 | download_button.place(x = 540 , y = 306)
1419 | path_button = customtkinter.CTkButton(pWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, command = pBrowseDir, corner_radius = 20)
1420 | path_button.place(x = 430 , y = 347)
1421 | lang_choose.configure(state = "normal")
1422 | menubutton.configure(state = "normal")
1423 | try: adv_checkbox.configure(state = "normal")
1424 | except: pass
1425 | downloading_var.set(" ")
1426 | downloadcounter_var.set("")
1427 | progress_label.configure(text_color = "#DCE4EE")
1428 | progress_size_label.configure(text_color = "#DCE4EE")
1429 |
1430 | # Captions download
1431 | def pCaptionsDownload(vid_link, title):
1432 | if vid_link in vids_subs:
1433 | lang = lang_choose.get()
1434 | video_id = extract.video_id(vid_link)
1435 | transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
1436 | if lang.lower() == "none":
1437 | return
1438 | elif lang.lower() == "arabic":
1439 | lang = "ar"
1440 | elif lang.lower() == "english":
1441 | for transcript in transcript_list:
1442 | if transcript.language_code == "en-US":
1443 | lang = "en-US"
1444 | break
1445 | elif transcript.language_code == "en-UK":
1446 | lang = "en-UK"
1447 | break
1448 | else:
1449 | lang = "en"
1450 | try: # Get the subtitle directly if it's there
1451 | final = YouTubeTranscriptApi.get_transcript(video_id = video_id, languages = [lang])
1452 | print("got one already there")
1453 | sub = "subtitle"
1454 | except: # If not then translate it
1455 | translated = "no"
1456 | en_list = ["en", "en-US","en-UK"]
1457 | for transcript in transcript_list:
1458 | if transcript.language_code in en_list: # Translate from English if it's there
1459 | final = transcript.translate(lang).fetch()
1460 | print(fr"translated from {transcript.language_code}")
1461 | sub = "translated_subtitle"
1462 | translated = "yes"
1463 | if translated == "no": # Avoid translating twice
1464 | final = transcript.translate(lang).fetch()
1465 | print(fr"translated {transcript.language_code}")
1466 | sub = "translated_subtitle"
1467 | translated = "yes"
1468 | else:
1469 | pass
1470 | formatter = SRTFormatter()
1471 | srt_formatted = formatter.format_transcript(final)
1472 | try:
1473 | with open(fr"{directory2}/{clean_filename(title)}_{sub}_{lang}.srt", "w", encoding = "utf-8") as srt_file:
1474 | srt_file.write(srt_formatted)
1475 | except NameError:
1476 | with open(fr"{directory}/{clean_filename(title)}_{sub}_{lang}.srt", "w", encoding = "utf-8") as srt_file:
1477 | srt_file.write(srt_formatted)
1478 | else:
1479 | print(fr"{title} not in caption list")
1480 |
1481 | # Add/Remove videos from download list
1482 | def videoSelector(choice):
1483 | global vids_counter, psize, plength
1484 | if choice.startswith("✔️ "): # Remove from list
1485 | vids_list.remove(choice)
1486 | choice = choice.replace("✔️ ","")
1487 | vids_list.append(choice)
1488 | vids_counter = vids_counter - 1
1489 |
1490 | choice_list = choice.split(" | ")
1491 | time = choice_list[1].split(":")
1492 | h = int(time[0]) * 60 * 60
1493 | m = int(time[1]) * 60
1494 | s = int(time[2])
1495 | seconds = h + m + s
1496 | plength = plength - seconds
1497 | size_float = choice_list[2]
1498 | size_float = size_float.replace(" MB", "")
1499 | size_float = round(float(size_float)*1024*1024, 2)
1500 | psize = psize - size_float
1501 |
1502 | else: # Add to list
1503 | vids_list.remove(choice)
1504 | choice = "✔️ " + choice
1505 | vids_list.append(choice)
1506 | vids_counter = vids_counter + 1
1507 |
1508 | choice_list = choice.split(" | ")
1509 | time = choice_list[1].split(":")
1510 | h = int(time[0]) * 60 * 60
1511 | m = int(time[1]) * 60
1512 | s = int(time[2])
1513 | seconds = h + m + s
1514 | plength = plength + seconds
1515 | size_float = choice_list[2]
1516 | size_float = size_float.replace(" MB", "")
1517 | size_float = round(float(size_float)*1024*1024, 2)
1518 | psize = psize + size_float
1519 |
1520 | print("==================")
1521 | print(vids_list)
1522 | print(vids_counter)
1523 | menubutton.configure(values = vids_list)
1524 | menubutton.set("Open Videos Menu")
1525 | length_var.set(to_hms(plength))
1526 | size_var.set(fr"{round(psize/1024/1024, 2)} MB")
1527 | videos_var.set(vids_counter)
1528 |
1529 | # Advanced checker
1530 | def advancedChecker():
1531 | global advanced_quality_settings
1532 | if advanced_quality_settings == "no": advanced_quality_settings = "yes"
1533 | elif advanced_quality_settings == "yes": advanced_quality_settings = "no"
1534 |
1535 | # Pause/Resume function
1536 | def toggle_download():
1537 | global is_paused
1538 | is_paused = not is_paused
1539 | if is_paused:
1540 | toggle_button = customtkinter.CTkButton(pWindow, text = "▶️", font = ("arial", 15), fg_color = "grey14", hover_color = "gray10", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
1541 | toggle_button.place(x = 550 , y = 347)
1542 | downloading_var.set("Paused")
1543 | else:
1544 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
1545 | toggle_button.place(x = 550 , y = 347)
1546 | downloading_var.set("Downloading")
1547 |
1548 | # Cancel function
1549 | def cancel_download():
1550 | global is_cancelled
1551 | is_cancelled = True
1552 |
1553 | # Open folder in file explorer when download is finished
1554 | def openFile():
1555 | try:
1556 | try:
1557 | dir2 = os.path.normpath(directory2)
1558 | if platform == "linux" or platform == "linux2": subprocess.Popen(dir2)
1559 | else: subprocess.Popen(f'explorer "{dir2}"')
1560 | except NameError:
1561 | if platform == "linux" or platform == "linux2": subprocess.Popen(directory)
1562 | else: subprocess.Popen(f'explorer "{directory}"')
1563 | except PermissionError:
1564 | try: messagebox.showerror(title = "Permission Denied", message = fr"I do not have permission to open '{dir2}'")
1565 | except NameError: messagebox.showerror(title = "Permission Denied", message = fr"I do not have permission to open '{directory}'")
1566 |
1567 | # Download playlist
1568 | def PlaylistDownloader(event = None):
1569 | # Preperations
1570 | global is_paused, is_cancelled
1571 | is_paused = is_cancelled = False
1572 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
1573 | toggle_button.place(x = 550 , y = 347)
1574 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
1575 | cancel_button.place(x = 595 , y = 347)
1576 | download_button = customtkinter.CTkButton(pWindow, text = "Download", font = ("arial bold", 25), state = "disabled", corner_radius = 20)
1577 | download_button.place(x = 540 , y = 306)
1578 | path_button = customtkinter.CTkButton(pWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", width = 5, state = "disabled", corner_radius = 20)
1579 | path_button.place(x = 430 , y = 347)
1580 | lang_choose.configure(state = "disabled")
1581 | menubutton.configure(state = "disabled")
1582 | try: adv_checkbox.configure(state = "disabled")
1583 | except: pass
1584 | audio_tags_list = ["251" , "140" , "250" , "249"]
1585 | non_progressive_list = ["137" , "22", "135" , "133", "160"]
1586 | # Progress stuff
1587 | pytubefix.request.default_range_size = 2097152 # 2MB chunk size (update progress every 2MB)
1588 | progress_label.configure(text_color = "green")
1589 | progress_size_label.configure(text_color = "LightBlue")
1590 |
1591 | # Download playlist
1592 | if vids_counter == 0:
1593 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1594 | toggle_button.place(x = 550 , y = 347)
1595 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1596 | cancel_button.place(x = 595 , y = 347)
1597 | download_button = customtkinter.CTkButton(pWindow, text = "Download", font = ("arial bold", 25), command = pVideoStart, corner_radius = 20)
1598 | download_button.place(x = 540 , y = 306)
1599 | path_button = customtkinter.CTkButton(pWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, command = pBrowseDir, corner_radius = 20)
1600 | path_button.place(x = 430 , y = 347)
1601 | lang_choose.configure(state = "normal")
1602 | menubutton.configure(state = "normal")
1603 | downloading_var.set(" ")
1604 | return messagebox.showerror(title = "No Selected Video", message = "Please select at least one video.")
1605 | downloaded_counter = 0
1606 |
1607 | # If the quality is non progressive video (1080p, 480p, 240p and 144p)
1608 | if quality in non_progressive_list:
1609 | for url in urls_list:
1610 | if is_cancelled: break
1611 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
1612 | toggle_button.place(x = 550 , y = 347)
1613 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
1614 | cancel_button.place(x = 595 , y = 347)
1615 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg").read()
1616 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
1617 | photo = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (270 , 150))
1618 | if quality == "137": video = url.streams.filter(res = "1080p").first()
1619 | elif quality == "22": video = url.streams.filter(res = "720p").first()
1620 | elif quality == "135": video = url.streams.filter(res = "480p").first()
1621 | elif quality == "133": video = url.streams.filter(res = "240p").first()
1622 | elif quality == "160": video = url.streams.filter(res = "144p").first()
1623 | audio = url.streams.get_by_itag(251)
1624 | size = video.filesize + audio.filesize
1625 | if fr"✔️ {p.repr(clean_filename(url.title))} | {to_hms(url.length)} | {round(size/1024/1024, 2)} MB" in vids_list: pass
1626 | else: continue
1627 | # Download subtitles if selected
1628 | if caps == "yes": pCaptionsDownload(url.watch_url, url.title)
1629 | else: pass
1630 | ext = "mp4"
1631 | try:
1632 | vname = fr"{directory2}/{clean_filename(url.title)}_video.mp4"
1633 | aname = fr"{directory2}/{clean_filename(url.title)}_audio.mp3"
1634 | except NameError:
1635 | vname = fr"{directory}/{clean_filename(url.title)}_video.mp4"
1636 | aname = fr"{directory}/{clean_filename(url.title)}_audio.mp3"
1637 | # Downlaod video
1638 | try:
1639 | with open(vname, "wb") as f:
1640 | percentage_var.set(fr"0.00% ")
1641 | sizeprogress_var.set(fr"0 MB ")
1642 | downloading_var.set("Downloading")
1643 | converting_percentage_var.set("")
1644 | progressbar.set(0)
1645 | downloaded_counter = downloaded_counter + 1
1646 | downloadcounter_var.set(fr"{downloaded_counter}/{vids_counter}")
1647 | thumbnail.configure(image = photo)
1648 | downloaded = 0
1649 | is_paused = is_cancelled = False
1650 | video = request.stream(video.url) # Get an iterable stream
1651 | while True:
1652 | if is_cancelled:
1653 | downloading_var.set("Canceled")
1654 | break
1655 | if is_paused:
1656 | time.sleep(0.1)
1657 | continue
1658 | try: chunk = next(video, None) # Get next chunk of video
1659 | except Exception as e:
1660 | print(e)
1661 | return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
1662 | if chunk:
1663 | f.write(chunk)
1664 | # Update progress
1665 | downloaded += len(chunk)
1666 | remaining = size - downloaded
1667 | bytes_downloaded = size - remaining
1668 | percentage_of_completion = bytes_downloaded / size * 100
1669 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
1670 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
1671 | progressbar.set(percentage_of_completion/100)
1672 | else:
1673 | # When finished
1674 | break
1675 | except PermissionError:
1676 | whenPlaylistError()
1677 | path = vname.replace(fr"/{clean_filename(url.title)}_video.mp4", "")
1678 | return messagebox.showerror(title = "Permission Error", message = fr"I don't have permission to access '{path}'. Change the path or run me as administrator.")
1679 | except FileNotFoundError:
1680 | whenPlaylistError()
1681 | path = vname.replace(fr"/{clean_filename(url.title)}_video.mp4", "")
1682 | return messagebox.showerror(title = "Folder Not Found", message = fr"'{path}' is not found. Change the path to an existing folder.")
1683 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1684 | toggle_button.place(x = 550 , y = 347)
1685 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1686 | cancel_button.place(x = 595 , y = 347)
1687 | if is_cancelled:
1688 | pass
1689 | else: # Download audio
1690 | downloading_var.set("Downloading audio")
1691 | with open(aname, "wb") as f:
1692 | is_paused = is_cancelled = False
1693 | video = request.stream(audio.url) # Get an iterable stream
1694 | # downloaded = 0
1695 | while True:
1696 | if is_paused:
1697 | time.sleep(0.1)
1698 | continue
1699 | try: chunk = next(video, None) # Get next chunk of video
1700 | except Exception as e:
1701 | print(e)
1702 | return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
1703 | if chunk:
1704 | f.write(chunk) # Download the chunk into the file
1705 | downloaded += len(chunk)
1706 | remaining = size - downloaded
1707 | bytes_downloaded = size - remaining
1708 | percentage_of_completion = bytes_downloaded / size * 100
1709 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
1710 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
1711 | progressbar.set(percentage_of_completion/100)
1712 | else:
1713 | # When finished
1714 | break
1715 | # Merge video and audio
1716 | downloading_var.set("Merging")
1717 | final_name = vname.replace("_video", fr"_({quality_string})")
1718 | cmd = f'ffmpeg -y -i "{aname}" -r 30 -i "{vname}" -filter:a aresample=async=1 -c:a flac -c:v copy "{final_name}"'
1719 | subprocess.call(cmd, shell=True)
1720 | os.remove(vname)
1721 | os.remove(aname)
1722 | # Convert if there is a conversion
1723 | if advanced_checker == "yes":
1724 | downloading_var.set("Converting")
1725 | Conversion(final_name, "mp4", url.length)
1726 |
1727 | # If playlist is 720p, 360p, *audio
1728 | else:
1729 | for url in urls_list:
1730 | if is_cancelled: break
1731 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
1732 | toggle_button.place(x = 550 , y = 347)
1733 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
1734 | cancel_button.place(x = 595 , y = 347)
1735 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg").read()
1736 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
1737 | photo = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (270 , 150))
1738 | video = url.streams.get_by_itag(quality)
1739 | size = video.filesize
1740 | if fr"✔️ {p.repr(clean_filename(url.title))} | {to_hms(url.length)} | {round(size/1024/1024, 2)} MB" in vids_list: pass
1741 | else: continue
1742 | # Download subtitles if selected
1743 | if caps == "yes": pCaptionsDownload(url.watch_url, url.title)
1744 | else: pass
1745 | if quality in audio_tags_list: ext = "mp3"
1746 | else: ext = "mp4"
1747 | try: vname = fr"{directory2}/{clean_filename(url.title)}_({quality_string}).{ext}"
1748 | except NameError: vname = fr"{directory}/{clean_filename(url.title)}_({quality_string}).{ext}"
1749 | try:
1750 | with open(vname, "wb") as f:
1751 | percentage_var.set(fr"0.00% ")
1752 | sizeprogress_var.set(fr"0 MB ")
1753 | downloading_var.set("Downloading")
1754 | converting_percentage_var.set("")
1755 | progressbar.set(0)
1756 | downloaded_counter = downloaded_counter + 1
1757 | downloadcounter_var.set(fr"{downloaded_counter}/{vids_counter}")
1758 | thumbnail.configure(image = photo)
1759 | downloaded = 0
1760 | video = request.stream(video.url) # Get an iterable stream
1761 | while True:
1762 | if is_cancelled:
1763 | downloading_var.set("Canceled")
1764 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1765 | toggle_button.place(x = 550 , y = 347)
1766 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1767 | cancel_button.place(x = 595 , y = 347)
1768 | break
1769 | if is_paused:
1770 | time.sleep(0.5)
1771 | continue
1772 | try: chunk = next(video, None) # Get next chunk of video
1773 | except: return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
1774 | if chunk:
1775 | f.write(chunk) # Download the chunk into the file
1776 | # Update Progress
1777 | downloaded += len(chunk)
1778 | remaining = size - downloaded
1779 | bytes_downloaded = size - remaining
1780 | percentage_of_completion = bytes_downloaded / size * 100
1781 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
1782 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
1783 | progressbar.set(percentage_of_completion/100)
1784 | else:
1785 | # Convert if there is a conversion
1786 | if advanced_checker == "yes":
1787 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1788 | toggle_button.place(x = 550 , y = 347)
1789 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1790 | cancel_button.place(x = 595 , y = 347)
1791 | downloading_var.set("Converting")
1792 | Conversion(vname, ext, url.length)
1793 | break # No more data = Finished
1794 | except PermissionError:
1795 | whenPlaylistError()
1796 | path = vname.replace(fr"/{clean_filename(url.title)}_({quality_string}).{ext}", "")
1797 | return messagebox.showerror(title = "Permission Error", message = fr"I don't have permission to access '{path}'. Change the path or run me as administrator.")
1798 | except FileNotFoundError:
1799 | whenPlaylistError()
1800 | path = vname.replace(fr"/{clean_filename(url.title)}_({quality_string}).{ext}", "")
1801 | return messagebox.showerror(title = "Folder Not Found", message = fr"'{path}' is not found. Change the path to an existing folder.")
1802 |
1803 | # When finished
1804 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
1805 | toggle_button.place(x = 550 , y = 347)
1806 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
1807 | cancel_button.place(x = 595 , y = 347)
1808 | if is_cancelled:
1809 | msg_box = messagebox.askquestion(title = "Delete Canceled File", message = fr"Do you want to delete '{vname}'?")
1810 | if msg_box == "yes": os.remove(vname)
1811 | else:
1812 | downloadcounter_var.set("")
1813 | pWindow.bell()
1814 | downloading_var.set("Finished")
1815 | converting_percentage_var.set("")
1816 | customtkinter.CTkButton(pWindow, text = "Open in File Explorer", font = ("arial bold", 20), command = openFile, corner_radius = 20).place(x = 460 , y = 420)
1817 |
1818 |
1819 | # Get playlist info, get link and check errors
1820 | whenOpening()
1821 | try:
1822 | urls = Playlist(link)
1823 | quality = str(quality_var.get())
1824 | if not "youtu" in link or not "playlist" in link: raise KeyError()
1825 | except KeyError:
1826 | whenError()
1827 | return messagebox.showerror(title = "Link Not Valid", message = "Please enter a valid link.")
1828 | except pytube.exceptions.RegexMatchError:
1829 | whenError()
1830 | return messagebox.showerror(title = "Link Not Valid", message = "Please enter a valid link.")
1831 | if quality == "0":
1832 | whenError()
1833 | return messagebox.showerror(title = "Format Not Selected", message = "Please select a format to download.")
1834 | else:
1835 | not_supported_list = ["250" , "249"]
1836 | if quality in not_supported_list:
1837 | whenError()
1838 | return messagebox.showerror(title = "Not Supported", message = "Currently, we don't support downloading playlists in 50kbps or 70kbps.")
1839 | else:
1840 | global vids_counter, psize, plength
1841 | vids_list = []
1842 | urls_list = []
1843 | plength = 0
1844 | psize = 0
1845 | vids_counter = 0
1846 | vids_subs = []
1847 | pl_tst_counter = 0
1848 | p = reprlib.Repr()
1849 | p.maxstring = 40
1850 | for url in urls.videos:
1851 | pl_tst_counter = pl_tst_counter + 1
1852 | print(fr"================================")
1853 | print(fr"({pl_tst_counter}) loop started")
1854 | try:
1855 | if quality == "137":
1856 | video = url.streams.filter(res = "1080p").first()
1857 | audio = url.streams.get_by_itag(251)
1858 | size = video.filesize + audio.filesize
1859 | elif quality == "22":
1860 | video = url.streams.filter(res = "720p").first()
1861 | audio = url.streams.get_by_itag(251)
1862 | size = video.filesize + audio.filesize
1863 | elif quality == "135":
1864 | video = url.streams.filter(res = "480p").first()
1865 | audio = url.streams.get_by_itag(251)
1866 | size = video.filesize + audio.filesize
1867 | elif quality == "133":
1868 | video = url.streams.filter(res = "240p").first()
1869 | audio = url.streams.get_by_itag(251)
1870 | size = video.filesize + audio.filesize
1871 | elif quality == "160":
1872 | video = url.streams.filter(res = "144p").first()
1873 | audio = url.streams.get_by_itag(251)
1874 | size = video.filesize + audio.filesize
1875 | else:
1876 | video = url.streams.get_by_itag(quality) # 1080p, 720, 360p, *audio
1877 | size = video.filesize
1878 | print(fr"({pl_tst_counter}) got video item")
1879 | except urllib.error.URLError as e:
1880 | print(e)
1881 | whenError()
1882 | return messagebox.showerror(title = "Not Connected", message = "Please check your internet connection.")
1883 | except pytube.exceptions.LiveStreamError as e:
1884 | print(e)
1885 | messagebox.showwarning(title = "Video is Live", message = fr"I couldn't retrieve '{url.title}' because it's live.\nThe load of the playlist will continue.")
1886 | continue
1887 | except KeyError as e:
1888 | print(fr"KeyError: {e}")
1889 | messagebox.showwarning(title = "Something Went Wrong", message = fr"I can't retrieve '{url.title}' at the moment. Change the selected quality or try again later.\nThe load of the playlist will continue.")
1890 | continue
1891 | except AttributeError as e:
1892 | print(fr"AttributeError: {e}")
1893 | messagebox.showwarning(title = "Quality Not Available", message = fr"I can't retrieve '{url.title}' in the quality that you chose. Change the selected quality or try again later.\nThe load of the playlist will continue.")
1894 | continue
1895 | try:
1896 | video_id = extract.video_id(url.watch_url)
1897 | YouTubeTranscriptApi.list_transcripts(video_id)
1898 | vids_subs.append(url.watch_url)
1899 | print(fr"({pl_tst_counter}) found subtitle")
1900 | except:
1901 | print(fr"({pl_tst_counter}) no subtitle")
1902 | psize = psize + size
1903 | size_string = round(psize/1024/1024, 2)
1904 | plength = plength + url.length
1905 | vids_list.append(fr"✔️ {p.repr(clean_filename(url.title))} | {to_hms(url.length)} | {round(size/1024/1024, 2)} MB")
1906 | urls_list.append(url)
1907 | vids_counter = vids_counter + 1
1908 | ploading_counter_var.set(fr"({vids_counter})")
1909 | print(fr"({pl_tst_counter}) got size + length + added vid_option to list")
1910 | if vids_list == []: # If an exception occured on every video...
1911 | whenError()
1912 | return messagebox.showerror(title = "Something Went Wrong", message = fr"I can't retrieve this playlist at the moment. Change the selected quality or try again later.")
1913 |
1914 | # Getting playlist thumbnail
1915 | try:
1916 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg").read()
1917 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
1918 | photo = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (270 , 150))
1919 | except UnboundLocalError:
1920 | whenError()
1921 | return messagebox.showerror(title = "Playlist is Unavailable",
1922 | message = "The cause for this may be one of the following:\n- Playlist is private\n- Playlist is age-restricted\n- Playlist is region blocked\n- Playlist is members only")
1923 |
1924 | # Playlist labels info
1925 | r = reprlib.Repr()
1926 | r.maxstring = 60 # max characters displayed for strings
1927 | date_format = "%d/%m/%Y"
1928 | if quality == "160": quality_string = "144p"
1929 | elif quality == "133": quality_string = "240p"
1930 | elif quality == "18": quality_string = "360p"
1931 | elif quality == "135": quality_string = "480p"
1932 | elif quality == "22": quality_string = "720p"
1933 | elif quality == "137": quality_string = "1080p"
1934 | elif quality == "249": quality_string = "50kbps"
1935 | elif quality == "250": quality_string = "70kbps"
1936 | elif quality == "140": quality_string = "128kbps"
1937 | elif quality == "251": quality_string = "160kbps"
1938 |
1939 | # Playlist form creating
1940 | pWindow = customtkinter.CTkToplevel()
1941 | pWindow.withdraw()
1942 | pWindow.title("Playlist Downloader")
1943 | width = 700
1944 | height = 460
1945 | x = (pWindow.winfo_screenwidth() // 2) - (width // 2)
1946 | y = (pWindow.winfo_screenheight() // 2) - (height // 2)
1947 | pWindow.geometry(fr"{width}x{height}+{x}+{y}")
1948 | pWindow.maxsize(700, 460)
1949 | pWindow.minsize(700, 460)
1950 | if platform == "linux" or platform == "linux2": pass
1951 | else:
1952 | try: pWindow.iconbitmap("YDICO.ico") # Windows
1953 | except TclError: pWindow.iconbitmap("_internal/YDICO.ico") # Windows
1954 | pWindow.protocol("WM_DELETE_WINDOW", onClosing)
1955 | # pWindow.bind("", PlaylistDownloader)
1956 |
1957 | # Downloading label
1958 | downloading_var = StringVar()
1959 | customtkinter.CTkLabel(pWindow, textvariable = downloading_var, font = ("arial", 25)).place(x = 305 , y = 418)
1960 | downloadcounter_var = StringVar()
1961 | customtkinter.CTkLabel(pWindow, textvariable = downloadcounter_var, font = ("arial", 25), text_color = "LightBlue").place(rely = 1.0, relx = 1.0, x = -405, y = -13, anchor = SE)
1962 | global converting_percentage_var
1963 | converting_percentage_var = StringVar()
1964 | customtkinter.CTkLabel(pWindow, textvariable = converting_percentage_var, font = ("arial", 25)).place(x = 450 , y = 418)
1965 |
1966 | # Playlist labels
1967 | length_var = StringVar()
1968 | length_var.set(to_hms(plength))
1969 | size_var = StringVar()
1970 | size_var.set(fr"{size_string} MB")
1971 | videos_var = IntVar()
1972 | videos_var.set(vids_counter)
1973 | thumbnail = customtkinter.CTkLabel(pWindow, text = "", image = photo)
1974 | thumbnail.place(x = 220 , y = 0)
1975 | customtkinter.CTkLabel(pWindow, text = "Playlist Title:", font = ("arial bold", 20)).place(x = 20 , y = 165)
1976 | customtkinter.CTkLabel(pWindow, text = r.repr(urls.title), font = ("arial", 20)).place(x = 148 , y = 165)
1977 | customtkinter.CTkLabel(pWindow, text = "Channel:", font = ("arial bold", 20)).place(x = 20 , y = 195)
1978 | customtkinter.CTkLabel(pWindow, text = r.repr(url.author), font = ("arial", 20)).place(x = 108 , y = 195)
1979 | customtkinter.CTkLabel(pWindow, text = "Publish Date:", font = ("arial bold", 20)).place(x = 20 , y = 225)
1980 | customtkinter.CTkLabel(pWindow, text = url.publish_date.strftime(date_format), font = ("arial", 20)).place(x = 152 , y = 225)
1981 | customtkinter.CTkLabel(pWindow, text = "Total Length:", font = ("arial bold", 20)).place(x = 20 , y = 255)
1982 | customtkinter.CTkLabel(pWindow, textvariable = length_var, font = ("arial", 20)).place(x = 149 , y = 255)
1983 | customtkinter.CTkLabel(pWindow, text = "Quality:", font = ("arial bold", 20)).place(x = 20 , y = 285)
1984 | customtkinter.CTkLabel(pWindow, text = quality_string, font = ("arial", 20)).place(x = 97 , y = 285)
1985 | customtkinter.CTkLabel(pWindow, text = "Total Size:", font = ("arial bold", 20)).place(x = 20 , y = 315)
1986 | customtkinter.CTkLabel(pWindow, textvariable = size_var, font = ("arial", 20)).place(x = 126 , y = 315)
1987 | customtkinter.CTkLabel(pWindow, text = "Total Videos:", font = ("arial bold", 20)).place(x = 340 , y = 285)
1988 | customtkinter.CTkLabel(pWindow, textvariable = videos_var, font = ("arial", 20)).place(x = 470 , y = 285)
1989 |
1990 | # Get thumbnail
1991 | def download_thumbnail():
1992 | if messagebox.askokcancel(title = "Download Thumbnails", message = fr"Do you want to download the thumbnails of all the videos?"):
1993 | thumb_dir = filedialog.askdirectory()
1994 | try:
1995 | for url in urls_list:
1996 | # Below line doesn't work properly for some reason
1997 | # if fr"✔️ {p.repr(clean_filename(url.title))} | {to_hms(url.length)} | {round(size/1024/1024, 2)} MB" in vids_list:
1998 | try:
1999 | response = requests.get(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg")
2000 | response.raise_for_status()
2001 | except:
2002 | response = requests.get(f"https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg")
2003 | response.raise_for_status()
2004 | thumb_path = fr"{thumb_dir}/{clean_filename(url.title)}_thumbnail.png"
2005 | with open(thumb_path, 'wb') as file:
2006 | file.write(response.content)
2007 | except requests.exceptions.ConnectionError:
2008 | return messagebox.showinfo(title = "Connection Error", message = fr"Check your internet connection and try again.")
2009 | messagebox.showinfo(title = "Thumbnails Downloaded", message = fr"Thumbnails has been downloaded successfully in '{thumb_dir}'")
2010 | thumbnail_button = customtkinter.CTkButton(pWindow, text = "Download Thumbnail", font = ("arial bold", 18), command = Thread(target = download_thumbnail).start, corner_radius = 20)
2011 | thumbnail_button.place(x = 480 , y = 230)
2012 |
2013 | # Path change
2014 | customtkinter.CTkLabel(pWindow, text = "Download Path:", font = ("arial bold", 20)).place(x = 20 , y = 345)
2015 | ppath_var = StringVar()
2016 | customtkinter.CTkEntry(pWindow, width = 245, height = 26, textvariable = ppath_var, state = "disabled", corner_radius = 20).place(x = 175 , y = 347)
2017 | try:
2018 | ppath_var.set(directory2)
2019 | except NameError:
2020 | ppath_var.set(directory)
2021 | path_button = customtkinter.CTkButton(pWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, height = 26, command = pBrowseDir, corner_radius = 20)
2022 | path_button.place(x = 430 , y = 347)
2023 |
2024 | # Subtitle Combobox
2025 | if vids_subs == []:
2026 | lang_choose_state = "disabled"
2027 | caps = "no"
2028 | else:
2029 | lang_choose_state = "readonly"
2030 | caps = "yes"
2031 | print(vids_subs)
2032 | customtkinter.CTkLabel(pWindow, text = "Subtitle:", font = ("arial bold", 20)).place(x = 340 , y = 315)
2033 | lang_choose = customtkinter.CTkComboBox(pWindow, width = 100, height = 26, values = ["None", "Arabic", "English"], state = lang_choose_state, corner_radius = 15)
2034 | lang_choose._entry.configure(readonlybackground = lang_choose._apply_appearance_mode(lang_choose._fg_color))
2035 | lang_choose.set("None")
2036 | lang_choose.place(x = 430 , y = 315)
2037 |
2038 | # Video selector
2039 | menubutton = customtkinter.CTkOptionMenu(pWindow, font = ("arial", 14), values = vids_list, command = videoSelector, corner_radius = 13)
2040 | menubutton.set("Open Videos Menu")
2041 | menubutton.place(x = 525 , y = 270)
2042 |
2043 | # Progress bar/labels
2044 | percentage_var = StringVar()
2045 | sizeprogress_var = StringVar()
2046 | progress_label = customtkinter.CTkLabel(pWindow, textvariable = percentage_var, font = ("arial", 22))
2047 | progress_label.place(x = 540 , y = 384)
2048 | progress_size_label = customtkinter.CTkLabel(pWindow, textvariable = sizeprogress_var, font = ("arial", 22))
2049 | progress_size_label.place(x = 624 , y = 384)
2050 | percentage_var.set("0.00%")
2051 | sizeprogress_var.set("0 MB")
2052 | progressbar = customtkinter.CTkProgressBar(pWindow, width = 505)
2053 | progressbar.place(x = 20 , y = 395)
2054 | progressbar.set(0)
2055 |
2056 | # Pause/Resume & Cancel buttons
2057 | toggle_button = customtkinter.CTkButton(pWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
2058 | toggle_button.place(x = 550 , y = 347)
2059 | cancel_button = customtkinter.CTkButton(pWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
2060 | cancel_button.place(x = 595 , y = 347)
2061 |
2062 | # Advanced quality settings check
2063 | global advanced_checker
2064 | advanced_checker = "no"
2065 | if not advanced_quality_settings == "no":
2066 | audio_quality_list = ["160kbps" , "128kbps" , "70kbps" , "50kbps"]
2067 | if advanced_quality_settings == "audio":
2068 | if quality_string in audio_quality_list:
2069 | adv_checkbox = customtkinter.CTkCheckBox(pWindow, text = "Apply Advanced Quality Settings", font = ("arial bold", 15), command = advancedChecker)
2070 | adv_checkbox.place(x = 410 , y = 195)
2071 | adv_checkbox.select()
2072 | advanced_checker = "yes"
2073 | else:
2074 | if not quality_string in audio_quality_list:
2075 | adv_checkbox = customtkinter.CTkCheckBox(pWindow, text = "Apply Advanced Quality Settings", font = ("arial bold", 15), command = advancedChecker)
2076 | adv_checkbox.place(x = 410 , y = 195)
2077 | adv_checkbox.select()
2078 | advanced_checker = "yes"
2079 |
2080 | # Download button
2081 | download_button = customtkinter.CTkButton(pWindow, text = "Download", font = ("arial bold", 25), command = pVideoStart, corner_radius = 20)
2082 | download_button.place(x = 540 , y = 306)
2083 |
2084 | # Back to home button
2085 | back_button = customtkinter.CTkButton(pWindow, text = "Back To Home", font = ("arial bold", 20), command = backHome, corner_radius = 20)
2086 | back_button.place(x = 20 , y = 420)
2087 |
2088 | # Return to normal state in root
2089 | whenError()
2090 | root.withdraw()
2091 | pWindow.deiconify()
2092 |
2093 | # Search window
2094 | def SearchWindow():
2095 | # For length format
2096 | def to_hms(s):
2097 | m, s = divmod(s, 60)
2098 | h, m = divmod(m, 60)
2099 | return "{}:{:0>2}:{:0>2}".format(h, m, s)
2100 | # Disable root
2101 | whenOpening()
2102 | # Window creating
2103 | search_text = search_var.get()
2104 | quality = str(quality_var.get())
2105 | if quality == "0":
2106 | whenError()
2107 | return messagebox.showerror(title = "Format Not Selected", message = "Please select a format.")
2108 | sWindow = customtkinter.CTkToplevel()
2109 | sWindow.title(f'Search Results For "{search_text}"')
2110 | sWindow.withdraw()
2111 | # On closing
2112 | def onClosing():
2113 | sWindow.destroy()
2114 | root.deiconify()
2115 | width = 700
2116 | height = 460
2117 | x = (sWindow.winfo_screenwidth() // 2) - (width // 2)
2118 | y = (sWindow.winfo_screenheight() // 2) - (height // 2)
2119 | sWindow.geometry(fr"{width}x{height}+{x}+{y}")
2120 | sWindow.maxsize(700, 460)
2121 | sWindow.minsize(700, 460)
2122 | if platform == "linux" or platform == "linux2": pass
2123 | else:
2124 | try: sWindow.iconbitmap("YDICO.ico") # Windows
2125 | except TclError: sWindow.iconbitmap("_internal/YDICO.ico") # Windows
2126 | sWindow.withdraw()
2127 | sWindow.protocol("WM_DELETE_WINDOW", onClosing)
2128 | global to_download
2129 | to_download = []
2130 | def checkboxes():
2131 | global to_download
2132 | if cb_var1.get() == 1:
2133 | if search.results[0] not in to_download: to_download.append(search.results[0])
2134 | else:
2135 | if search.results[0] in to_download: to_download.remove(search.results[0])
2136 | if cb_var2.get() == 1:
2137 | if search.results[1] not in to_download: to_download.append(search.results[1])
2138 | else:
2139 | if search.results[1] in to_download: to_download.remove(search.results[1])
2140 | if cb_var3.get() == 1:
2141 | if search.results[2] not in to_download: to_download.append(search.results[2])
2142 | else:
2143 | if search.results[2] in to_download: to_download.remove(search.results[2])
2144 | if cb_var4.get() == 1:
2145 | if search.results[3] not in to_download: to_download.append(search.results[3])
2146 | else:
2147 | if search.results[3] in to_download: to_download.remove(search.results[3])
2148 |
2149 | if cb_var5.get() == 1:
2150 | if search.results[4] not in to_download: to_download.append(search.results[4])
2151 | else:
2152 | if search.results[4] in to_download: to_download.remove(search.results[4])
2153 | if cb_var6.get() == 1:
2154 | if search.results[5] not in to_download: to_download.append(search.results[5])
2155 | else:
2156 | if search.results[5] in to_download: to_download.remove(search.results[5])
2157 | if cb_var7.get() == 1:
2158 | if search.results[6] not in to_download: to_download.append(search.results[6])
2159 | else:
2160 | if search.results[6] in to_download: to_download.remove(search.results[6])
2161 | if cb_var8.get() == 1:
2162 | if search.results[7] not in to_download: to_download.append(search.results[7])
2163 | else:
2164 | if search.results[7] in to_download: to_download.remove(search.results[7])
2165 |
2166 | if cb_var9.get() == 1:
2167 | if search.results[8] not in to_download: to_download.append(search.results[8])
2168 | else:
2169 | if search.results[8] in to_download: to_download.remove(search.results[8])
2170 | if cb_var10.get() == 1:
2171 | if search.results[9] not in to_download: to_download.append(search.results[9])
2172 | else:
2173 | if search.results[9] in to_download: to_download.remove(search.results[9])
2174 | if cb_var11.get() == 1:
2175 | if search.results[10] not in to_download: to_download.append(search.results[10])
2176 | else:
2177 | if search.results[10] in to_download: to_download.remove(search.results[10])
2178 | if cb_var12.get() == 1:
2179 | if search.results[11] not in to_download: to_download.append(search.results[11])
2180 | else:
2181 | if search.results[11] in to_download: to_download.remove(search.results[11])
2182 |
2183 | if cb_var13.get() == 1:
2184 | if search.results[12] not in to_download: to_download.append(search.results[12])
2185 | else:
2186 | if search.results[12] in to_download: to_download.remove(search.results[12])
2187 | if cb_var14.get() == 1:
2188 | if search.results[13] not in to_download: to_download.append(search.results[13])
2189 | else:
2190 | if search.results[13] in to_download: to_download.remove(search.results[13])
2191 | if cb_var15.get() == 1:
2192 | if search.results[14] not in to_download: to_download.append(search.results[14])
2193 | else:
2194 | if search.results[14] in to_download: to_download.remove(search.results[14])
2195 | if cb_var16.get() == 1:
2196 | if search.results[15] not in to_download: to_download.append(search.results[15])
2197 | else:
2198 | if search.results[15] in to_download: to_download.remove(search.results[15])
2199 |
2200 | selected_counter.set(fr"{len(to_download)} Selected")
2201 | print("==========")
2202 | print(to_download)
2203 | print(len(to_download))
2204 |
2205 | # Checkboxes variables
2206 | cb_var1 = IntVar()
2207 | cb_var2 = IntVar()
2208 | cb_var3 = IntVar()
2209 | cb_var4 = IntVar()
2210 | cb_var5 = IntVar()
2211 | cb_var6 = IntVar()
2212 | cb_var7 = IntVar()
2213 | cb_var8 = IntVar()
2214 | cb_var9 = IntVar()
2215 | cb_var10 = IntVar()
2216 | cb_var11 = IntVar()
2217 | cb_var12 = IntVar()
2218 | cb_var13 = IntVar()
2219 | cb_var14 = IntVar()
2220 | cb_var15 = IntVar()
2221 | cb_var16 = IntVar()
2222 |
2223 | # Titles variables
2224 | t_var1 = StringVar()
2225 | t_var2 = StringVar()
2226 | t_var3 = StringVar()
2227 | t_var4 = StringVar()
2228 |
2229 | # Open in Youtube buttons
2230 | def openYouTube1():
2231 | if results_counter == 4: webbrowser.open(search.results[0].watch_url, new=1)
2232 | if results_counter == 8: webbrowser.open(search.results[4].watch_url, new=1)
2233 | if results_counter == 12: webbrowser.open(search.results[8].watch_url, new=1)
2234 | if results_counter == 16: webbrowser.open(search.results[12].watch_url, new=1)
2235 | def openYouTube2():
2236 | if results_counter == 4: webbrowser.open(search.results[1].watch_url, new=1)
2237 | if results_counter == 8: webbrowser.open(search.results[5].watch_url, new=1)
2238 | if results_counter == 12: webbrowser.open(search.results[9].watch_url, new=1)
2239 | if results_counter == 16: webbrowser.open(search.results[13].watch_url, new=1)
2240 | def openYouTube3():
2241 | if results_counter == 4: webbrowser.open(search.results[2].watch_url, new=1)
2242 | if results_counter == 8: webbrowser.open(search.results[6].watch_url, new=1)
2243 | if results_counter == 12: webbrowser.open(search.results[10].watch_url, new=1)
2244 | if results_counter == 16: webbrowser.open(search.results[14].watch_url, new=1)
2245 | def openYouTube4():
2246 | if results_counter == 4: webbrowser.open(search.results[3].watch_url, new=1)
2247 | if results_counter == 8: webbrowser.open(search.results[7].watch_url, new=1)
2248 | if results_counter == 12: webbrowser.open(search.results[11].watch_url, new=1)
2249 | if results_counter == 16: webbrowser.open(search.results[15].watch_url, new=1)
2250 |
2251 | # First video
2252 | cb1 = customtkinter.CTkCheckBox(sWindow, text = "", variable = cb_var1, command = checkboxes)
2253 | cb1.place(x = 30, y = 40)
2254 | img1 = customtkinter.CTkLabel(sWindow, text = "", image = "")
2255 | img1.place(x = 80, y = 10)
2256 | t1 = customtkinter.CTkLabel(sWindow, textvariable = t_var1, font = ("arial bold", 20))
2257 | t1.place(x = 250, y = 15)
2258 | a1 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 15))
2259 | a1.place(x = 255, y = 40)
2260 | l1 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2261 | l1.place(x = 260, y = 65)
2262 | v1 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2263 | v1.place(x = 320, y = 65)
2264 | b1 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube1, corner_radius = 20)
2265 | b1.place(x = 405, y = 63)
2266 | # s1 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 12))
2267 | # s1.place(x = 260, y = 65)
2268 |
2269 | # Second video
2270 | cb2 = customtkinter.CTkCheckBox(sWindow, text = "", variable = cb_var2, command = checkboxes)
2271 | cb2.place(x = 30, y = 140)
2272 | img2 = customtkinter.CTkLabel(sWindow, text = "", image = "")
2273 | img2.place(x = 80, y = 110)
2274 | t2 = customtkinter.CTkLabel(sWindow, textvariable = t_var2, font = ("arial bold", 20))
2275 | t2.place(x = 250, y = 115)
2276 | a2 = customtkinter.CTkLabel(sWindow, text = "Not Available", font = ("arial", 15))
2277 | a2.place(x = 255, y = 140)
2278 | l2 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2279 | l2.place(x = 260, y = 165)
2280 | v2 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2281 | v2.place(x = 320, y = 165)
2282 | b2 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube2, corner_radius = 20)
2283 | b2.place(x = 405, y = 163)
2284 | # s2 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 12))
2285 | # s2.place(x = 260, y = 165)
2286 |
2287 | # Third video
2288 | cb3 = customtkinter.CTkCheckBox(sWindow, text = "", variable = cb_var3, command = checkboxes)
2289 | cb3.place(x = 30, y = 240)
2290 | img3 = customtkinter.CTkLabel(sWindow, text = "", image = "")
2291 | img3.place(x = 80, y = 210)
2292 | t3 = customtkinter.CTkLabel(sWindow, textvariable = t_var3, font = ("arial bold", 20))
2293 | t3.place(x = 250, y = 215)
2294 | a3 = customtkinter.CTkLabel(sWindow, text = "Not Available", font = ("arial", 15))
2295 | a3.place(x = 255, y = 240)
2296 | l3 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2297 | l3.place(x = 260, y = 265)
2298 | v3 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2299 | v3.place(x = 320, y = 265)
2300 | b3 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube3, corner_radius = 20)
2301 | b3.place(x = 405, y = 263)
2302 | # s3 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 12))
2303 | # s3.place(x = 260, y = 265)
2304 |
2305 | # Fourth video
2306 | cb4 = customtkinter.CTkCheckBox(sWindow, text = "", variable = cb_var4, command = checkboxes)
2307 | cb4.place(x = 30, y = 340)
2308 | img4 = customtkinter.CTkLabel(sWindow, text = "", image = "")
2309 | img4.place(x = 80, y = 310)
2310 | t4 = customtkinter.CTkLabel(sWindow, textvariable = t_var4, font = ("arial bold", 20))
2311 | t4.place(x = 250, y = 315)
2312 | a4 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 15))
2313 | a4.place(x = 255, y = 340)
2314 | l4 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2315 | l4.place(x = 260, y = 365)
2316 | v4 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 14))
2317 | v4.place(x = 320, y = 365)
2318 | b4 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube4, corner_radius = 20)
2319 | b4.place(x = 405, y = 363)
2320 | # s4 = customtkinter.CTkLabel(sWindow, text = "", font = ("arial", 12))
2321 | # s4.place(x = 260, y = 365)
2322 |
2323 | # Loading
2324 | def Loading(button_var):
2325 | # Disabled widgets
2326 | cb1.configure(state = "disabled")
2327 | cb2.configure(state = "disabled")
2328 | cb3.configure(state = "disabled")
2329 | cb4.configure(state = "disabled")
2330 | b1 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", width = 0, state = "disabled", corner_radius = 20)
2331 | b1.place(x = 405, y = 63)
2332 | b2 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", width = 0, state = "disabled", corner_radius = 20)
2333 | b2.place(x = 405, y = 163)
2334 | b3 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", width = 0, state = "disabled", corner_radius = 20)
2335 | b3.place(x = 405, y = 263)
2336 | b4 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", width = 0, state = "disabled", corner_radius = 20)
2337 | b4.place(x = 405, y = 363)
2338 | pr_button.configure(state = "disabled")
2339 | dn_button.configure(state = "disabled")
2340 | nr_button.configure(state = "disabled")
2341 | # Loading label
2342 | button_var.set("Loading")
2343 | time.sleep(0.5)
2344 | while True:
2345 | if button_var.get() == "Loading":
2346 | button_var.set("Loading.")
2347 | time.sleep(0.5)
2348 | else: break
2349 | if button_var.get() == "Loading.":
2350 | button_var.set("Loading..")
2351 | time.sleep(0.5)
2352 | else: break
2353 | if button_var.get() == "Loading..":
2354 | button_var.set("Loading...")
2355 | time.sleep(0.5)
2356 | else: break
2357 | if button_var.get() == "Loading...":
2358 | button_var.set("Loading")
2359 | time.sleep(0.5)
2360 | else: break
2361 |
2362 | # Return to normal
2363 | def normalWidgets():
2364 | global results_counter
2365 | if t_var1.get() != "Video is Not Available": cb1.configure(state = "normal")
2366 | if t_var2.get() != "Video is Not Available": cb2.configure(state = "normal")
2367 | if t_var3.get() != "Video is Not Available": cb3.configure(state = "normal")
2368 | if t_var4.get() != "Video is Not Available": cb4.configure(state = "normal")
2369 | b1 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube1, corner_radius = 20)
2370 | b1.place(x = 405, y = 63)
2371 | b2 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube2, corner_radius = 20)
2372 | b2.place(x = 405, y = 163)
2373 | b3 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube3, corner_radius = 20)
2374 | b3.place(x = 405, y = 263)
2375 | b4 = customtkinter.CTkButton(sWindow, text = "Open in YouTube", font = ("arial bold", 12), fg_color = "red", hover_color = "red3", width = 0, command = openYouTube4, corner_radius = 20)
2376 | b4.place(x = 405, y = 363)
2377 | dn_button.configure(state = "normal")
2378 | dn_button_var.set("Download")
2379 | pr_button_var.set("Previous Results")
2380 | nr_button_var.set("Next Results")
2381 | if results_counter == 4:
2382 | pr_button.configure(state = "disabled")
2383 | nr_button.configure(state = "normal")
2384 | page_counter.set("Page 1/4")
2385 | elif results_counter == 8:
2386 | pr_button.configure(state = "normal")
2387 | nr_button.configure(state = "normal")
2388 | page_counter.set("Page 2/4")
2389 | elif results_counter == 12:
2390 | pr_button.configure(state = "normal")
2391 | nr_button.configure(state = "normal")
2392 | page_counter.set("Page 3/4")
2393 | elif results_counter == 16:
2394 | pr_button.configure(state = "normal")
2395 | nr_button.configure(state = "disabled")
2396 | page_counter.set("Page 4/4")
2397 |
2398 | # On buttons click
2399 | def onPrClick():
2400 | button_var = pr_button_var
2401 | threading.Thread(target=Loading, args = (button_var,)).start()
2402 | threading.Thread(target=deresults).start()
2403 | def onNrClick():
2404 | button_var = nr_button_var
2405 | threading.Thread(target=Loading, args = (button_var,)).start()
2406 | threading.Thread(target=results).start()
2407 |
2408 | # Previous results
2409 | def deresults():
2410 | global results_counter
2411 | if results_counter == 16: results_counter = 8
2412 | elif results_counter == 12: results_counter = 4
2413 | elif results_counter == 8: results_counter = 0
2414 | results()
2415 |
2416 | # Search
2417 | search = Search(search_text)
2418 | try:
2419 | print(len(search.results))
2420 | except urllib.error.URLError:
2421 | whenError()
2422 | return messagebox.showerror(title = "Not Connected", message = "Please check your internet connection.")
2423 | global results_counter
2424 | results_counter = 0
2425 | r = reprlib.Repr()
2426 | r.maxstring = 40
2427 | def results():
2428 | global results_counter
2429 | if results_counter == 0:
2430 | limit = 4
2431 | results_list = search.results[0:4]
2432 | elif results_counter == 4:
2433 | limit = 8
2434 | results_list = search.results[4:8]
2435 | elif results_counter == 8:
2436 | limit = 12
2437 | results_list = search.results[8:12]
2438 | elif results_counter == 12:
2439 | limit = 16
2440 | results_list = search.results[12:16]
2441 | for url in results_list:
2442 | first = [0, 4, 8, 12]
2443 | second = [1, 5, 9, 13]
2444 | third = [2, 6, 10, 14]
2445 | fourth = [3, 7, 11, 15]
2446 | if results_counter == limit: break
2447 | elif results_counter in first: url1 = url
2448 | elif results_counter in second: url2 = url
2449 | elif results_counter in third: url3 = url
2450 | elif results_counter in fourth: url4 = url
2451 | results_counter = results_counter + 1
2452 | print(fr"got {results_counter} results")
2453 |
2454 | # Get thumbnails
2455 | try:
2456 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url1.video_id}/maxresdefault.jpg").read()
2457 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
2458 | thumb1 = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (160 , 90))
2459 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url2.video_id}/maxresdefault.jpg").read()
2460 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
2461 | thumb2 = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (160 , 90))
2462 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url3.video_id}/maxresdefault.jpg").read()
2463 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
2464 | thumb3 = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (160 , 90))
2465 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url4.video_id}/maxresdefault.jpg").read()
2466 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
2467 | thumb4 = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (160 , 90))
2468 | except UnboundLocalError:
2469 | whenError()
2470 | sWindow.destroy()
2471 | root.deiconify()
2472 | return messagebox.showerror(title = "Something Went Wrong", message = "Try again later.")
2473 | except urllib.error.URLError:
2474 | normalWidgets()
2475 | return messagebox.showerror(title = "Internet Error", message = "Please check your internet connection.")
2476 |
2477 | # Configure checkboxes
2478 | if results_counter == 4:
2479 | cb1.configure(variable = cb_var1)
2480 | cb2.configure(variable = cb_var2)
2481 | cb3.configure(variable = cb_var3)
2482 | cb4.configure(variable = cb_var4)
2483 | elif results_counter == 8:
2484 | cb1.configure(variable = cb_var5)
2485 | cb2.configure(variable = cb_var6)
2486 | cb3.configure(variable = cb_var7)
2487 | cb4.configure(variable = cb_var8)
2488 | elif results_counter == 12:
2489 | cb1.configure(variable = cb_var9)
2490 | cb2.configure(variable = cb_var10)
2491 | cb3.configure(variable = cb_var11)
2492 | cb4.configure(variable = cb_var12)
2493 | elif results_counter == 16:
2494 | cb1.configure(variable = cb_var13)
2495 | cb2.configure(variable = cb_var14)
2496 | cb3.configure(variable = cb_var15)
2497 | cb4.configure(variable = cb_var16)
2498 |
2499 | # Configure first video in page
2500 | if url1.views > 1000000: views = fr"{int(url1.views/1000000)}M"
2501 | elif url1.views > 1000: views = fr"{int(url1.views/1000)}K"
2502 | else: views = int(url1.views/1000000)
2503 | t_var1.set(r.repr(url1.title))
2504 | a1.configure(text = r.repr(url1.author))
2505 | l1.configure(text = to_hms(url1.length))
2506 | v1.configure(text = fr"{views} Views")
2507 | img1.configure(image = thumb1)
2508 |
2509 | # Configure second video in page
2510 | if url2.views > 1000000: views = fr"{int(url2.views/1000000)}M"
2511 | elif url2.views > 1000: views = fr"{int(url2.views/1000)}K"
2512 | else: views = int(url2.views/1000000)
2513 | t_var2.set(r.repr(url2.title))
2514 | a2.configure(text = r.repr(url2.author))
2515 | l2.configure(text = to_hms(url2.length))
2516 | v2.configure(text = fr"{views} Views")
2517 | img2.configure(image = thumb2)
2518 |
2519 | # Configure third video in page
2520 | if url3.views > 1000000: views = fr"{int(url3.views/1000000)}M"
2521 | elif url3.views > 1000: views = fr"{int(url3.views/1000)}K"
2522 | else: views = int(url3.views/1000000)
2523 | t_var3.set(r.repr(url3.title))
2524 | a3.configure(text = r.repr(url3.author))
2525 | l3.configure(text = to_hms(url3.length))
2526 | v3.configure(text = fr"{views} Views")
2527 | img3.configure(image = thumb3)
2528 |
2529 | # Configure fourth video in page
2530 | if url4.views > 1000000: views = fr"{int(url4.views/1000000)}M"
2531 | elif url4.views > 1000: views = fr"{int(url4.views/1000)}K"
2532 | else: views = int(url4.views/1000000)
2533 | t_var4.set(r.repr(url4.title))
2534 | a4.configure(text = r.repr(url4.author))
2535 | l4.configure(text = to_hms(url4.length))
2536 | v4.configure(text = fr"{views} Views")
2537 | img4.configure(image = thumb4)
2538 |
2539 | # Returns widgets to right state
2540 | normalWidgets()
2541 |
2542 | # Proceed to downlaod page
2543 | def onDnClick():
2544 | if to_download == []:
2545 | return messagebox.showerror(title = "No Selected Video", message = "Please select at least one video to download.")
2546 | msg_box = messagebox.askquestion(title = "Proceed To Download", message = fr"Are you sure that you want to download {len(to_download)} video(s)")
2547 | if msg_box == "yes":
2548 | button_var = dn_button_var
2549 | threading.Thread(target = ResultsWindow, args = (to_download,)).start()
2550 | threading.Thread(target = Loading, args = (button_var,)).start()
2551 | else: return
2552 |
2553 | # Search download
2554 | def ResultsWindow(to_download):
2555 | # Starting
2556 | def pVideoStart():
2557 | threading.Thread(target = Downloading, args = (downloading_var,)).start()
2558 | threading.Thread(target = SearchDownloader).start()
2559 |
2560 | # Back home
2561 | def backHome():
2562 | download = downloading_var.get()
2563 | non_downloading_list = ["", "Finished", "Canceled"]
2564 | if download in non_downloading_list:
2565 | pass
2566 | else:
2567 | msg_box = messagebox.askquestion(title = "Cancel Download",
2568 | message = "Going back to home will cancel the current download.\n\nDo you wish to continue?",
2569 | icon = "warning")
2570 | if msg_box == "yes": pass
2571 | else: return
2572 | sDWindow.destroy()
2573 | root.deiconify()
2574 |
2575 | # Set path
2576 | if platform == "linux" or platform == "linux2": path = fr"/home/{os.getlogin()}/Downloads"
2577 | else: path = fr"C:/Users\{os.getlogin()}\Downloads"
2578 | global directory
2579 | directory = os.path.realpath(path) # Deafult path in case the user didn't choose
2580 | def pBrowseDir(): # Path function
2581 | global directory2
2582 | directory2 = filedialog.askdirectory()
2583 | ppath_var.set(directory2)
2584 |
2585 | # When an error happens
2586 | def whenResultsError():
2587 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
2588 | toggle_button.place(x = 550 , y = 347)
2589 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
2590 | cancel_button.place(x = 595 , y = 347)
2591 | download_button = customtkinter.CTkButton(sDWindow, text = "Download", font = ("arial bold", 25), command = pVideoStart, corner_radius = 20)
2592 | download_button.place(x = 540 , y = 306)
2593 | path_button = customtkinter.CTkButton(sDWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, command = pBrowseDir, corner_radius = 20)
2594 | path_button.place(x = 430 , y = 347)
2595 | lang_choose.configure(state = "normal")
2596 | try: adv_checkbox.configure(state = "normal")
2597 | except: pass
2598 | downloadcounter_var.set("")
2599 | downloading_var.set(" ")
2600 | progress_label.configure(text_color = "#DCE4EE")
2601 | progress_size_label.configure(text_color = "#DCE4EE")
2602 |
2603 | # Captions download
2604 | def sCaptionsDownload(vid_link, title):
2605 | if vid_link in vids_subs:
2606 | lang = lang_choose.get()
2607 | video_id = extract.video_id(vid_link)
2608 | transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
2609 | if lang.lower() == "none":
2610 | return
2611 | elif lang.lower() == "arabic":
2612 | lang = "ar"
2613 | elif lang.lower() == "english":
2614 | for transcript in transcript_list:
2615 | if transcript.language_code == "en-US":
2616 | lang = "en-US"
2617 | break
2618 | elif transcript.language_code == "en-UK":
2619 | lang = "en-UK"
2620 | break
2621 | else:
2622 | lang = "en"
2623 | try: # Get the subtitle directly if it's there
2624 | final = YouTubeTranscriptApi.get_transcript(video_id = video_id, languages = [lang])
2625 | print("got one already there")
2626 | sub = "subtitle"
2627 | except: # If not then translate it
2628 | translated = "no"
2629 | en_list = ["en", "en-US","en-UK"]
2630 | for transcript in transcript_list:
2631 | if transcript.language_code in en_list: # Translate from English if it's there
2632 | final = transcript.translate(lang).fetch()
2633 | print(fr"translated from {transcript.language_code}")
2634 | sub = "translated_subtitle"
2635 | translated = "yes"
2636 | if translated == "no": # Avoid translating twice
2637 | final = transcript.translate(lang).fetch()
2638 | print(fr"translated {transcript.language_code}")
2639 | sub = "translated_subtitle"
2640 | translated = "yes"
2641 | else:
2642 | pass
2643 | formatter = SRTFormatter()
2644 | srt_formatted = formatter.format_transcript(final)
2645 | try:
2646 | with open(fr"{directory2}/{clean_filename(title)}_{sub}_{lang}.srt", "w", encoding = "utf-8") as srt_file:
2647 | srt_file.write(srt_formatted)
2648 | except NameError:
2649 | with open(fr"{directory}/{clean_filename(title)}_{sub}_{lang}.srt", "w", encoding = "utf-8") as srt_file:
2650 | srt_file.write(srt_formatted)
2651 | else:
2652 | print(fr"{title} not in list")
2653 |
2654 | # Advanced checker
2655 | def advancedChecker():
2656 | global advanced_quality_settings
2657 | if advanced_quality_settings == "no": advanced_quality_settings = "yes"
2658 | elif advanced_quality_settings == "yes": advanced_quality_settings = "no"
2659 |
2660 | # Open folder in file explorer when download is finished
2661 | def openFile():
2662 | try:
2663 | try:
2664 | dir2 = os.path.normpath(directory2)
2665 | if platform == "linux" or platform == "linux2": subprocess.Popen(dir2)
2666 | else: subprocess.Popen(f'explorer "{dir2}"')
2667 | except NameError:
2668 | if platform == "linux" or platform == "linux2": subprocess.Popen(directory)
2669 | else: subprocess.Popen(f'explorer "{directory}"')
2670 | except PermissionError:
2671 | try: messagebox.showerror(title = "Permission Denied", message = fr"I do not have permission to open '{dir2}'")
2672 | except NameError: messagebox.showerror(title = "Permission Denied", message = fr"I do not have permission to open '{directory}'")
2673 |
2674 | # Pause/Resume function
2675 | def toggle_download():
2676 | global is_paused
2677 | is_paused = not is_paused
2678 | if is_paused:
2679 | toggle_button = customtkinter.CTkButton(sDWindow, text = "▶️", font = ("arial", 15), fg_color = "grey14", hover_color = "gray10", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
2680 | toggle_button.place(x = 550 , y = 347)
2681 | downloading_var.set("Paused")
2682 | else:
2683 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
2684 | toggle_button.place(x = 550 , y = 347)
2685 | downloading_var.set("Downloading")
2686 |
2687 | # Cancel function
2688 | def cancel_download():
2689 | global is_cancelled
2690 | is_cancelled = True
2691 |
2692 | # Download search
2693 | def SearchDownloader(event = None):
2694 | # Preperations
2695 | global is_paused, is_cancelled
2696 | is_paused = is_cancelled = False
2697 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
2698 | toggle_button.place(x = 550 , y = 347)
2699 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
2700 | cancel_button.place(x = 595 , y = 347)
2701 | download_button = customtkinter.CTkButton(sDWindow, text = "Download", font = ("arial bold", 25), state = "disabled", corner_radius = 20)
2702 | download_button.place(x = 540 , y = 306)
2703 | path_button = customtkinter.CTkButton(sDWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", width = 5, state = "disabled", corner_radius = 20)
2704 | path_button.place(x = 430 , y = 347)
2705 | audio_tags_list = ["251" , "140" , "250" , "249"]
2706 | non_progressive_list = ["137" , "22", "135" , "133", "160"]
2707 | downloaded_counter = 0
2708 | vids_counter = len(to_download)
2709 | r = reprlib.Repr()
2710 | r.maxstring = 27
2711 | # Progress stuff
2712 | pytubefix.request.default_range_size = 2097152 # 2MB chunk size (update progress every 2MB)
2713 | progress_label.configure(text_color = "green")
2714 | progress_size_label.configure(text_color = "LightBlue")
2715 | lang_choose.configure(state = "disabled")
2716 | try: adv_checkbox.configure(state = "disabled")
2717 | except: pass
2718 |
2719 | # If the quality is non progressive video (1080p, 480p, 240p and 144p)
2720 | if quality in non_progressive_list:
2721 | for url in to_download:
2722 | if is_cancelled: break
2723 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
2724 | toggle_button.place(x = 550 , y = 347)
2725 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
2726 | cancel_button.place(x = 595 , y = 347)
2727 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg").read()
2728 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
2729 | photo = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (270 , 150))
2730 | if quality == "137": video = url.streams.filter(res = "1080p").first()
2731 | elif quality == "22": video = url.streams.filter(res = "720p").first()
2732 | elif quality == "135": video = url.streams.filter(res = "480p").first()
2733 | elif quality == "133": video = url.streams.filter(res = "240p").first()
2734 | elif quality == "160": video = url.streams.filter(res = "144p").first()
2735 | audio = url.streams.get_by_itag(251)
2736 | size = video.filesize + audio.filesize
2737 | title_var.set(r.repr(url.title))
2738 | length_var.set(to_hms(url.length))
2739 | size_var.set(fr"{round(size/1024/1024, 2)} MB")
2740 | # Download subtitles if selected
2741 | if caps == "yes": sCaptionsDownload(url.watch_url, url.title)
2742 | else: pass
2743 | if quality in audio_tags_list: ext = "mp3"
2744 | else: ext = "mp4"
2745 | try:
2746 | vname = fr"{directory2}/{clean_filename(url.title)}_video.mp4"
2747 | aname = fr"{directory2}/{clean_filename(url.title)}_audio.mp3"
2748 | except NameError:
2749 | vname = fr"{directory}/{clean_filename(url.title)}_video.mp4"
2750 | aname = fr"{directory}/{clean_filename(url.title)}_audio.mp3"
2751 | # Downlaod video
2752 | try:
2753 | with open(vname, "wb") as f:
2754 | percentage_var.set(fr"0.00% ")
2755 | sizeprogress_var.set(fr"0 MB ")
2756 | downloading_var.set("Downloading")
2757 | converting_percentage_var.set("")
2758 | progressbar.set(0)
2759 | downloaded_counter = downloaded_counter + 1
2760 | downloadcounter_var.set(fr"{downloaded_counter}/{vids_counter}")
2761 | thumbnail.configure(image = photo)
2762 | video = request.stream(video.url) # Get an iterable stream
2763 | downloaded = 0
2764 | while True:
2765 | if is_cancelled:
2766 | downloading_var.set("Canceled")
2767 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
2768 | toggle_button.place(x = 550 , y = 347)
2769 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
2770 | cancel_button.place(x = 595 , y = 347)
2771 | break
2772 | if is_paused:
2773 | time.sleep(0.5)
2774 | continue
2775 | try: chunk = next(video, None) # Get next chunk of video
2776 | except: return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
2777 | if chunk:
2778 | f.write(chunk) # Download the chunk into the file
2779 | # Update Progress
2780 | downloaded += len(chunk)
2781 | remaining = size - downloaded
2782 | bytes_downloaded = size - remaining
2783 | percentage_of_completion = bytes_downloaded / size * 100
2784 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
2785 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
2786 | progressbar.set(percentage_of_completion/100)
2787 | else:
2788 | break # No more data = Finished
2789 | except PermissionError:
2790 | whenResultsError()
2791 | path = vname.replace(fr"/{clean_filename(url.title)}_video.mp4", "")
2792 | return messagebox.showerror(title = "Permission Error", message = fr"I don't have permission to access '{path}'. Change the path or run me as administrator.")
2793 | except FileNotFoundError:
2794 | whenResultsError()
2795 | path = vname.replace(fr"/{clean_filename(url.title)}_video.mp4", "")
2796 | return messagebox.showerror(title = "Folder Not Found", message = fr"'{path}' is not found. Change the path to an existing folder.")
2797 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
2798 | toggle_button.place(x = 550 , y = 347)
2799 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
2800 | cancel_button.place(x = 595 , y = 347)
2801 | if is_cancelled:
2802 | pass
2803 | else: # Download audio
2804 | downloading_var.set("Downloading audio")
2805 | with open(aname, "wb") as f:
2806 | is_paused = is_cancelled = False
2807 | video = request.stream(audio.url) # Get an iterable stream
2808 | while True:
2809 | if is_paused:
2810 | time.sleep(0.1)
2811 | continue
2812 | try: chunk = next(video, None) # Get next chunk of video
2813 | except Exception as e:
2814 | print(e)
2815 | return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
2816 | if chunk:
2817 | f.write(chunk) # Download the chunk into the file
2818 | # Update progress
2819 | downloaded += len(chunk)
2820 | remaining = size - downloaded
2821 | bytes_downloaded = size - remaining
2822 | percentage_of_completion = bytes_downloaded / size * 100
2823 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
2824 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
2825 | progressbar.set(percentage_of_completion/100)
2826 | else:
2827 | # When finished
2828 | break
2829 | # Merge video and audio
2830 | downloading_var.set("Merging")
2831 | final_name = vname.replace("_video", fr"_({quality_string})")
2832 | cmd = f'ffmpeg -y -i "{aname}" -r 30 -i "{vname}" -filter:a aresample=async=1 -c:a flac -c:v copy "{final_name}"'
2833 | subprocess.call(cmd, shell=True)
2834 | os.remove(vname)
2835 | os.remove(aname)
2836 | # Convert if there is a conversion
2837 | if advanced_checker == "yes":
2838 | downloading_var.set("Converting")
2839 | Conversion(final_name, "mp4", url.length)
2840 |
2841 | # Download to_download
2842 | else:
2843 | for url in to_download:
2844 | if is_cancelled: break
2845 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
2846 | toggle_button.place(x = 550 , y = 347)
2847 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
2848 | cancel_button.place(x = 595 , y = 347)
2849 | try: raw_data = urllib.request.urlopen(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg").read()
2850 | except: raw_data = urllib.request.urlopen("https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg").read()
2851 | photo = customtkinter.CTkImage(light_image = Image.open(io.BytesIO(raw_data)), dark_image = Image.open(io.BytesIO(raw_data)), size = (270 , 150))
2852 | video = url.streams.get_by_itag(quality)
2853 | title_var.set(r.repr(url.title))
2854 | length_var.set(to_hms(url.length))
2855 | size_var.set(fr"{round(video.filesize/1024/1024, 2)} MB")
2856 | # Download subtitles if selected
2857 | if caps == "yes": sCaptionsDownload(url.watch_url, url.title)
2858 | else: pass
2859 | if quality in audio_tags_list: ext = "mp3"
2860 | else: ext = "mp4"
2861 | size = video.filesize
2862 | try: vname = fr"{directory2}/{clean_filename(url.title)}_({quality_string}).{ext}"
2863 | except NameError: vname = fr"{directory}/{clean_filename(url.title)}_({quality_string}).{ext}"
2864 | try:
2865 | with open(vname, "wb") as f:
2866 | percentage_var.set(fr"0.00% ")
2867 | sizeprogress_var.set(fr"0 MB ")
2868 | downloading_var.set("Downloading")
2869 | converting_percentage_var.set("")
2870 | progressbar.set(0)
2871 | downloaded_counter = downloaded_counter + 1
2872 | downloadcounter_var.set(fr"{downloaded_counter}/{vids_counter}")
2873 | thumbnail.configure(image = photo)
2874 | video = request.stream(video.url) # Get an iterable stream
2875 | downloaded = 0
2876 | while True:
2877 | if is_cancelled:
2878 | downloading_var.set("Canceled")
2879 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
2880 | toggle_button.place(x = 550 , y = 347)
2881 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
2882 | cancel_button.place(x = 595 , y = 347)
2883 | break
2884 | if is_paused:
2885 | time.sleep(0.5)
2886 | continue
2887 | try: chunk = next(video, None) # Get next chunk of video
2888 | except: return messagebox.showerror(title = "Something Went Wrong", message = "Something went wrong, please try again.")
2889 | if chunk:
2890 | f.write(chunk) # Download the chunk into the file
2891 | # Update Progress
2892 | downloaded += len(chunk)
2893 | remaining = size - downloaded
2894 | bytes_downloaded = size - remaining
2895 | percentage_of_completion = bytes_downloaded / size * 100
2896 | percentage_var.set(fr"{round(percentage_of_completion, 2)}% ")
2897 | sizeprogress_var.set(fr"{int(bytes_downloaded / 1024 / 1024)} MB ")
2898 | progressbar.set(percentage_of_completion/100)
2899 | else:
2900 | # Convert if there is a conversion
2901 | if advanced_checker == "yes":
2902 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, command = toggle_download)
2903 | toggle_button.place(x = 550 , y = 347)
2904 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, command = cancel_download, corner_radius = 20)
2905 | cancel_button.place(x = 595 , y = 347)
2906 | downloading_var.set("Converting")
2907 | Conversion(vname, ext, url.length)
2908 | break # No more data = Finished
2909 | except PermissionError:
2910 | whenResultsError()
2911 | path = vname.replace(fr"/{clean_filename(url.title)}_({quality_string}).{ext}", "")
2912 | return messagebox.showerror(title = "Permission Error", message = fr"I don't have permission to access '{path}'. Change the path or run me as administrator.")
2913 | except FileNotFoundError:
2914 | whenResultsError()
2915 | path = vname.replace(fr"/{clean_filename(url.title)}_({quality_string}).{ext}", "")
2916 | return messagebox.showerror(title = "Folder Not Found", message = fr"'{path}' is not found. Change the path to an existing folder.")
2917 |
2918 | # When finished
2919 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
2920 | toggle_button.place(x = 550 , y = 347)
2921 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
2922 | cancel_button.place(x = 595 , y = 347)
2923 | if is_cancelled:
2924 | msg_box = messagebox.askquestion(title = "Delete Canceled File", message = fr"Do you want to delete '{vname}'?")
2925 | if msg_box == "yes": os.remove(vname)
2926 | else:
2927 | downloadcounter_var.set("")
2928 | converting_percentage_var.set("")
2929 | thumbnail.configure(image = "")
2930 | title_var.set("Finished")
2931 | length_var.set("Finished")
2932 | size_var.set("Finished")
2933 | downloading_var.set("Finished")
2934 | customtkinter.CTkButton(sDWindow, text = "Open in File Explorer", font = ("arial bold", 20), command = openFile, corner_radius = 20).place(x = 460 , y = 420)
2935 | sDWindow.bell()
2936 |
2937 | # Get to_download list info, get link and check errors
2938 | loaded_count = 0
2939 | total_size = 0
2940 | total_length = 0
2941 | vids_subs = []
2942 | for url in to_download:
2943 | try:
2944 | if quality == "137":
2945 | video = url.streams.filter(res = "1080p").first()
2946 | audio = url.streams.get_by_itag(251)
2947 | size = video.filesize + audio.filesize
2948 | elif quality == "22":
2949 | video = url.streams.filter(res = "720p").first()
2950 | audio = url.streams.get_by_itag(251)
2951 | size = video.filesize + audio.filesize
2952 | elif quality == "135":
2953 | video = url.streams.filter(res = "480p").first()
2954 | audio = url.streams.get_by_itag(251)
2955 | size = video.filesize + audio.filesize
2956 | elif quality == "133":
2957 | video = url.streams.filter(res = "240p").first()
2958 | audio = url.streams.get_by_itag(251)
2959 | size = video.filesize + audio.filesize
2960 | elif quality == "160":
2961 | video = url.streams.filter(res = "144p").first()
2962 | audio = url.streams.get_by_itag(251)
2963 | size = video.filesize + audio.filesize
2964 | else:
2965 | video = url.streams.get_by_itag(quality) # 1080p, 720, 360p, *audio
2966 | size = video.filesize
2967 | total_size = total_size + size
2968 | total_length = total_length + url.length
2969 | loaded_count += 1
2970 | loaded_counter.set(fr"({loaded_count})")
2971 | except urllib.error.URLError as e:
2972 | normalWidgets()
2973 | return messagebox.showerror(title = "Not Connected", message = "Please check your internet connection.")
2974 | except KeyError as e:
2975 | print(fr"KeyError: {e}")
2976 | normalWidgets()
2977 | return messagebox.showerror(title = "Something Went Wrong", message = fr"I can't fetch '{url.title}' at the moment. Change the selected quality or try again later.")
2978 | except AttributeError as e:
2979 | print(fr"AttributeError: {e}")
2980 | normalWidgets()
2981 | return messagebox.showerror(title = "Quality Not Available",
2982 | message = fr"I can't fetch '{url.title}' in the quality that you chose. Change the selected quality or try again later.")
2983 | except pytube.exceptions.LiveStreamError as e:
2984 | print(e)
2985 | normalWidgets()
2986 | return messagebox.showerror(title = "Video is Live", message = fr"I Can't fetch '{url.title}' because it's a live video.")
2987 | except pytube.exceptions.AgeRestrictedError as e:
2988 | print(e)
2989 | normalWidgets()
2990 | return messagebox.showerror(title = "Age Restricted", message = fr"'{url.title}' is age restricted.")
2991 | except pytube.exceptions.MembersOnly as e:
2992 | print(e)
2993 | normalWidgets()
2994 | return messagebox.showerror(title = "Members Only", message = fr"'{url.title}' is members only.")
2995 | except pytube.exceptions.VideoPrivate as e:
2996 | print(e)
2997 | normalWidgets()
2998 | return messagebox.showerror(title = "Private Video", message = fr"'{url.title}' is private.")
2999 | except pytube.exceptions.VideoRegionBlocked as e:
3000 | print(e)
3001 | normalWidgets()
3002 | return messagebox.showerror(title = "Region Blocked", message = fr"'{url.title}' is region blocked.")
3003 | except pytube.exceptions.VideoUnavailable as e:
3004 | print(e)
3005 | normalWidgets()
3006 | return messagebox.showerror(title = "Video Unavailable", message = fr"'{url.title}' is unavailable.")
3007 | try:
3008 | video_id = extract.video_id(url.watch_url)
3009 | YouTubeTranscriptApi.list_transcripts(video_id)
3010 | vids_subs.append(url.watch_url)
3011 | print(fr"({url.title}) found subtitle")
3012 | except:
3013 | print(fr"({url.title}) no subtitle")
3014 | pass
3015 |
3016 | # Selected labels prepare
3017 | if quality == "160": quality_string = "144p"
3018 | elif quality == "133": quality_string = "240p"
3019 | elif quality == "18": quality_string = "360p"
3020 | elif quality == "135": quality_string = "480p"
3021 | elif quality == "22": quality_string = "720p"
3022 | elif quality == "137": quality_string = "1080p"
3023 | elif quality == "249": quality_string = "50kbps"
3024 | elif quality == "250": quality_string = "70kbps"
3025 | elif quality == "140": quality_string = "128kbps"
3026 | elif quality == "251": quality_string = "160kbps"
3027 |
3028 | # Form creating
3029 | sDWindow = customtkinter.CTkToplevel()
3030 | sDWindow.withdraw()
3031 | sDWindow.title("Results Downloader")
3032 | width = 700
3033 | height = 460
3034 | x = (sDWindow.winfo_screenwidth() // 2) - (width // 2)
3035 | y = (sDWindow.winfo_screenheight() // 2) - (height // 2)
3036 | sDWindow.geometry(fr"{width}x{height}+{x}+{y}")
3037 | sDWindow.maxsize(700, 460)
3038 | sDWindow.minsize(700, 460)
3039 | def onClosing():
3040 | # if messagebox.askokcancel("Quit", "Do you want to quit?"):
3041 | sDWindow.destroy()
3042 | root.destroy()
3043 | sDWindow.protocol("WM_DELETE_WINDOW", onClosing)
3044 | if platform == "linux" or platform == "linux2": pass
3045 | else:
3046 | try: sDWindow.iconbitmap("YDICO.ico") # Windows
3047 | except TclError: sDWindow.iconbitmap("_internal/YDICO.ico") # Windows
3048 | # sDWindow.bind("", SearchDownloader)
3049 |
3050 | # Downloading label
3051 | downloading_var = StringVar()
3052 | customtkinter.CTkLabel(sDWindow, textvariable = downloading_var, font = ("arial", 25)).place(x = 305 , y = 418)
3053 | downloadcounter_var = StringVar()
3054 | customtkinter.CTkLabel(sDWindow, textvariable = downloadcounter_var, font = ("arial", 25), text_color = "LightBlue").place(rely = 1.0, relx = 1.0, x = -405, y = -13, anchor = SE)
3055 | global converting_percentage_var
3056 | converting_percentage_var = StringVar()
3057 | customtkinter.CTkLabel(sDWindow, textvariable = converting_percentage_var, font = ("arial", 25)).place(x = 450 , y = 418)
3058 |
3059 | # Search download labels
3060 | title_var = StringVar()
3061 | title_var.set("...")
3062 | length_var = StringVar()
3063 | length_var.set("...")
3064 | size_var = StringVar()
3065 | size_var.set("...")
3066 | thumbnail = customtkinter.CTkLabel(sDWindow, text = "", image = "")
3067 | thumbnail.place(x = 415 , y = 15)
3068 | customtkinter.CTkLabel(sDWindow, text = "Current Video Info", font = ("arial bold italic", 30)).place(x = 10 , y = 15)
3069 | customtkinter.CTkLabel(sDWindow, text = "Video Title:", font = ("arial bold", 20)).place(x = 20 , y = 55)
3070 | customtkinter.CTkLabel(sDWindow, textvariable = title_var, font = ("arial", 20)).place(x = 132 , y = 55)
3071 | customtkinter.CTkLabel(sDWindow, text = "Length:", font = ("arial bold", 20)).place(x = 20 , y = 90)
3072 | customtkinter.CTkLabel(sDWindow, textvariable = length_var, font = ("arial", 20)).place(x = 97 , y = 90)
3073 | customtkinter.CTkLabel(sDWindow, text = "File size:", font = ("arial bold", 20)).place(x = 20 , y = 125)
3074 | customtkinter.CTkLabel(sDWindow, textvariable = size_var, font = ("arial", 20)).place(x = 111 , y = 125)
3075 |
3076 | customtkinter.CTkLabel(sDWindow, text = "Total Videos Info", font = ("arial bold italic", 30)).place(x = 10 , y = 165)
3077 | customtkinter.CTkLabel(sDWindow, text = "Total Videos:", font = ("arial bold", 20)).place(x = 20 , y = 205)
3078 | customtkinter.CTkLabel(sDWindow, text = len(to_download), font = ("arial", 20)).place(x = 151 , y = 205)
3079 | customtkinter.CTkLabel(sDWindow, text = "Total Length:", font = ("arial bold", 20)).place(x = 20 , y = 240)
3080 | customtkinter.CTkLabel(sDWindow, text = fr"{to_hms(total_length)}", font = ("arial", 20)).place(x = 150 , y = 240)
3081 | customtkinter.CTkLabel(sDWindow, text = "Quality:", font = ("arial bold", 20)).place(x = 20 , y = 275)
3082 | customtkinter.CTkLabel(sDWindow, text = quality_string, font = ("arial", 20)).place(x = 96 , y = 275)
3083 | customtkinter.CTkLabel(sDWindow, text = "Total Size:", font = ("arial bold", 20)).place(x = 20 , y = 310)
3084 | customtkinter.CTkLabel(sDWindow, text = fr"{round(total_size/1024/1024, 2)} MB", font = ("arial", 20)).place(x = 123 , y = 310)
3085 |
3086 | # Get thumbnail
3087 | def download_thumbnail():
3088 | if messagebox.askokcancel(title = "Download Thumbnails", message = fr"Do you want to download the thumbnails of all the selected videos?"):
3089 | thumb_dir = filedialog.askdirectory()
3090 | try:
3091 | for url in to_download:
3092 | try:
3093 | response = requests.get(f"https://img.youtube.com/vi/{url.video_id}/maxresdefault.jpg")
3094 | response.raise_for_status()
3095 | except:
3096 | response = requests.get(f"https://img.youtube.com/vi/dmg36v0zYbM/maxresdefault.jpg")
3097 | response.raise_for_status()
3098 | thumb_path = fr"{thumb_dir}/{clean_filename(url.title)}_thumbnail.png"
3099 | with open(thumb_path, 'wb') as file:
3100 | file.write(response.content)
3101 | except requests.exceptions.ConnectionError:
3102 | return messagebox.showinfo(title = "Connection Error", message = fr"Check your internet connection and try again.")
3103 | messagebox.showinfo(title = "Thumbnails Downloaded", message = fr"Thumbnails has been downloaded successfully in '{thumb_dir}'")
3104 | thumbnail_button = customtkinter.CTkButton(sDWindow, text = "Download Thumbnail", font = ("arial bold", 18), command = Thread(target = download_thumbnail).start, corner_radius = 20)
3105 | thumbnail_button.place(x = 480 , y = 260)
3106 |
3107 | # Path change
3108 | customtkinter.CTkLabel(sDWindow, text = "Download Path:", font = ("arial bold", 20)).place(x = 20 , y = 345)
3109 | ppath_var = StringVar()
3110 | customtkinter.CTkEntry(sDWindow, width = 245, height = 26, textvariable = ppath_var, state = "disabled", corner_radius = 20).place(x = 175 , y = 347)
3111 | try:
3112 | ppath_var.set(directory2)
3113 | except NameError:
3114 | ppath_var.set(directory)
3115 | path_button = customtkinter.CTkButton(sDWindow, text = "Change Path", font = ("arial bold", 12), fg_color = "dim grey", hover_color = "gray25", width = 5, height = 26, command = pBrowseDir, corner_radius = 20)
3116 | path_button.place(x = 430 , y = 347)
3117 |
3118 | # Subtitle Combobox
3119 | if vids_subs == []:
3120 | lang_choose_state = "disabled"
3121 | caps = "no"
3122 | else:
3123 | lang_choose_state = "readonly"
3124 | caps = "yes"
3125 | print(vids_subs)
3126 | customtkinter.CTkLabel(sDWindow, text = "Subtitle:", font = ("arial bold", 20)).place(x = 340 , y = 315)
3127 | lang_choose = customtkinter.CTkComboBox(sDWindow, width = 100, height = 26, values = ["None", "Arabic", "English"], state = lang_choose_state, corner_radius = 15)
3128 | lang_choose._entry.configure(readonlybackground = lang_choose._apply_appearance_mode(lang_choose._fg_color))
3129 | lang_choose.set("None")
3130 | lang_choose.place(x = 430 , y = 315)
3131 |
3132 | # Progress bar/labels
3133 | percentage_var = StringVar()
3134 | sizeprogress_var = StringVar()
3135 | progress_label = customtkinter.CTkLabel(sDWindow, textvariable = percentage_var, font = ("arial", 22))
3136 | progress_label.place(x = 540 , y = 384)
3137 | progress_size_label = customtkinter.CTkLabel(sDWindow, textvariable = sizeprogress_var, font = ("arial", 22))
3138 | progress_size_label.place(x = 624 , y = 384)
3139 | percentage_var.set("0.00%")
3140 | sizeprogress_var.set("0 MB")
3141 | progressbar = customtkinter.CTkProgressBar(sDWindow, width = 505)
3142 | progressbar.place(x = 20 , y = 395)
3143 | progressbar.set(0)
3144 |
3145 | # Pause/Resume & Cancel buttons
3146 | toggle_button = customtkinter.CTkButton(sDWindow, text = "⏸️", font = ("arial", 15), fg_color = "grey14", text_color = "CadetBlue1", width = 5, height = 26, state = "disabled")
3147 | toggle_button.place(x = 550 , y = 347)
3148 | cancel_button = customtkinter.CTkButton(sDWindow, text = "Cancel", font = ("arial bold", 12), fg_color = "red2", width = 80, height = 26, state = "disabled", corner_radius = 20)
3149 | cancel_button.place(x = 595 , y = 347)
3150 |
3151 | # Advanced quality settings check
3152 | global advanced_checker
3153 | advanced_checker = "no"
3154 | if not advanced_quality_settings == "no":
3155 | audio_quality_list = ["160kbps" , "128kbps" , "70kbps" , "50kbps"]
3156 | if advanced_quality_settings == "audio":
3157 | if quality_string in audio_quality_list:
3158 | adv_checkbox = customtkinter.CTkCheckBox(sDWindow, text = "Apply Advanced Quality Settings", font = ("arial bold", 15), command = advancedChecker)
3159 | adv_checkbox.place(x = 410 , y = 225)
3160 | adv_checkbox.select()
3161 | advanced_checker = "yes"
3162 | else:
3163 | if not quality_string in audio_quality_list:
3164 | adv_checkbox = customtkinter.CTkCheckBox(sDWindow, text = "Apply Advanced Quality Settings", font = ("arial bold", 15), command = advancedChecker)
3165 | adv_checkbox.place(x = 410 , y = 225)
3166 | adv_checkbox.select()
3167 | advanced_checker = "yes"
3168 |
3169 | # Download button
3170 | download_button = customtkinter.CTkButton(sDWindow, text = "Download", font = ("arial bold", 25), command = pVideoStart, corner_radius = 20)
3171 | download_button.place(x = 540 , y = 306)
3172 |
3173 | # Back to home button
3174 | back_button = customtkinter.CTkButton(sDWindow, text = "Back To Home", font = ("arial bold", 20), command = backHome, corner_radius = 20)
3175 | back_button.place(x = 20 , y = 420)
3176 |
3177 | # Return to normal state in root
3178 | whenError()
3179 | sWindow.destroy()
3180 | sDWindow.deiconify()
3181 |
3182 | # Footer Buttons & label
3183 | page_counter = StringVar()
3184 | page_counter.set("Page 1/4")
3185 | selected_counter = StringVar()
3186 | selected_counter.set("0 Selected")
3187 | loaded_counter = StringVar()
3188 | loaded_counter.set("")
3189 | pr_button_var = StringVar()
3190 | pr_button_var.set("Previous Results")
3191 | nr_button_var = StringVar()
3192 | nr_button_var.set("Next Results")
3193 | pr_button = customtkinter.CTkButton(sWindow, textvariable = pr_button_var, font = ("arial bold", 15), command = onPrClick, corner_radius = 20)
3194 | pr_button.place(x = 20, y = 420)
3195 | customtkinter.CTkLabel(sWindow, textvariable = selected_counter, font = ("arial bold", 15)).place(x = 193, y = 420)
3196 | dn_button_var = StringVar()
3197 | dn_button_var.set("Download")
3198 | dn_button = customtkinter.CTkButton(sWindow, textvariable = dn_button_var, font = ("arial bold", 15), command = onDnClick, corner_radius = 20)
3199 | dn_button.place(x = 290, y = 420)
3200 | customtkinter.CTkLabel(sWindow, textvariable = loaded_counter, font = ("arial bold", 13)).place(x = 350, y = 390)
3201 | customtkinter.CTkLabel(sWindow, textvariable = page_counter, font = ("arial bold", 15)).place(x = 450, y = 420)
3202 | nr_button = customtkinter.CTkButton(sWindow, textvariable = nr_button_var, font = ("arial bold", 15), command = onNrClick, corner_radius = 20)
3203 | nr_button.place(x = 540, y = 420)
3204 |
3205 | results()
3206 | whenError()
3207 | sWindow.deiconify()
3208 | root.withdraw()
3209 |
3210 | # Run the app
3211 | root.mainloop()
3212 |
--------------------------------------------------------------------------------