├── 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 | Get it on GitHub
12 |
13 | 14 | ## Features 15 | 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 |
  1. Install python and git, then make sure both are added to your PATH.
  2. 92 | 93 |
  3. Download FFmpeg and either: 94 |
  4. 98 | 99 |
  5. Git-clone this repo & change directory
  6. 100 | 101 | ``` 102 | git clone https://github.com/MAyman007/YouTube-Downloader.git 103 | 104 | cd YouTube-Downloader 105 | ``` 106 |
  7. Create a virtual environment & activate it (optional)
  8. 107 | 108 | ``` 109 | python -m venv venv 110 | venv\Scripts\Activate.ps1 (or venv\Scripts\activate.bat) 111 | ``` 112 |
  9. Install modules using pip
  10. 113 | 114 | ``` 115 | pip install -r requirements.txt 116 | ``` 117 |
  11. Run the .py file!
  12. 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 |
  1. Install the following packages: 137 | 138 | 165 |
  2. 166 | 167 |
  3. 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 |
  4. 175 |
  5. Create a virtual environment & activate it 176 | 177 | ``` 178 | python -m venv venv 179 | source venv/bin/activate 180 | ``` 181 |
  6. 182 |
  7. 183 | Install modules using pip 184 | 185 | ``` 186 | pip install -r requirements.txt 187 | ``` 188 |
  8. 189 |
  9. 190 | Run the .py file! 191 | 192 | ``` 193 | python3 main.py 194 | ``` 195 |
  10. 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 | --------------------------------------------------------------------------------