From 77f887adc9f4f11ab737ab4bebc5287ff16eadac Mon Sep 17 00:00:00 2001 From: Thisal Dilmith <93121062+Thisal-D@users.noreply.github.com> Date: Thu, 9 May 2024 02:50:13 +0530 Subject: [PATCH] v0.0.1 v0.0.1 --- LICENSE | 21 ++ README.md | 84 +++++ VERSION | 1 + app.py | 415 +++++++++++++++++++++++++ dependencies_installer.py | 3 + functions/convertTime.py | 9 + functions/createDownloadDirectory.py | 12 + functions/formatToComboBoxValues.py | 8 + functions/getColor.py | 4 + functions/getConvertedPath.py | 2 + functions/getConvertedSize.py | 13 + functions/getGeneralSettings.py | 6 + functions/getSupportedDownloadTypes.py | 31 ++ functions/getThemeSettings.py | 9 + functions/getThumbnails.py | 65 ++++ functions/getValidFileName.py | 11 + functions/getWindowScale.py | 4 + functions/passIt.py | 2 + functions/removeInvalidCharts.py | 6 + functions/saveSettings.py | 6 + functions/sortDict.py | 31 ++ main.py | 38 +++ requirements.txt | 4 + settings/general.json | 4 + settings/theme.json | 41 +++ src/icon.ico | Bin 0 -> 225342 bytes temp/this directory is necessary | 1 + widgets/Video.py | 193 ++++++++++++ widgets/addedPlayList.py | 218 +++++++++++++ widgets/addedVideo.py | 222 +++++++++++++ widgets/colorBtn.py | 53 ++++ widgets/downloadedPlayList.py | 94 ++++++ widgets/downloadedVideo.py | 89 ++++++ widgets/downloadingPlayList.py | 222 +++++++++++++ widgets/downloadingVideo.py | 339 ++++++++++++++++++++ widgets/playList.py | 214 +++++++++++++ widgets/settingPanel.py | 146 +++++++++ 37 files changed, 2621 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 VERSION create mode 100644 app.py create mode 100644 dependencies_installer.py create mode 100644 functions/convertTime.py create mode 100644 functions/createDownloadDirectory.py create mode 100644 functions/formatToComboBoxValues.py create mode 100644 functions/getColor.py create mode 100644 functions/getConvertedPath.py create mode 100644 functions/getConvertedSize.py create mode 100644 functions/getGeneralSettings.py create mode 100644 functions/getSupportedDownloadTypes.py create mode 100644 functions/getThemeSettings.py create mode 100644 functions/getThumbnails.py create mode 100644 functions/getValidFileName.py create mode 100644 functions/getWindowScale.py create mode 100644 functions/passIt.py create mode 100644 functions/removeInvalidCharts.py create mode 100644 functions/saveSettings.py create mode 100644 functions/sortDict.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 settings/general.json create mode 100644 settings/theme.json create mode 100644 src/icon.ico create mode 100644 temp/this directory is necessary create mode 100644 widgets/Video.py create mode 100644 widgets/addedPlayList.py create mode 100644 widgets/addedVideo.py create mode 100644 widgets/colorBtn.py create mode 100644 widgets/downloadedPlayList.py create mode 100644 widgets/downloadedVideo.py create mode 100644 widgets/downloadingPlayList.py create mode 100644 widgets/downloadingVideo.py create mode 100644 widgets/playList.py create mode 100644 widgets/settingPanel.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..32e1cd5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Thisal Dilmith + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e18869 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +🌟**If you find this project helpful, your support by starring it would mean a lot! Your feedback motivates me to keep refining and enhancing it further.** πŸš€ + +Your support means a lot and inspires me to do better with each update. Thank you for taking the time to check out this project!πŸ₯° + +
+ +# PyTube Downloader + +**Download .exe for Windows** + + +PyTube Downloader is a user-friendly application that allows users to download YouTube videos with ease. It features a simple and intuitive user interface, making the downloading process straightforward for all users. + +
+ +## Features + +- **Easy Downloading :** Download YouTube videos effortlessly by pasting the video URL into the application. +- **Playlist Downloading :** Download entire playlists using just the playlist URL +- **Format Selection :** Choose from various video and audio formats for downloading. +- **Progress Tracking :** Track the download progress within the application. +- **Customization Options :** Customize download locations and theme and colors + + +
+ +### Dark theme view + + + + + +
+ +## Technologies Used + +- **Programming Language:** [Python] +- **Frameworks/Libraries:** [tkinter, customtkinter, pytube, pillow ] + +
+ +## How to Use + +1. Clone the repository to your local machine. +2. Install the necessary dependencies (if any). +3. Run the application. +4. Paste the YouTube video URL into the designated field. +5. Choose the desired format +6. Click the download button to initiate the download process. +7. Monitor the download progress within the application. +8. Enjoy your downloaded YouTube video! + + +
+ +## Contribution + +Contributions to this project are welcome! Feel free to fork the repository, make improvements, and submit pull requests. + +
+ +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +
+ +## Disclaimer + +This application is intended for personal use only. Please respect YouTube's terms of service and the rights of content creators when downloading videos. + + +## Contributors + +- [](https://github.com/childeyouyu) +[youyu](https://github.com/childeyouyu) + + +- [](https://github.com/Navindu21) +[Navindu Pahasara](https://github.com/Navindu21) + +- [](https://github.com/sooryasuraweera) + [Soorya Suraweera](https://github.com/sooryasuraweera) + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..58991df --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +VERSION = '0.0.1' diff --git a/app.py b/app.py new file mode 100644 index 0000000..d6b3bca --- /dev/null +++ b/app.py @@ -0,0 +1,415 @@ +import customtkinter as ctk +from widgets import addedVideo, downloadingVideo, downloadedVideo, \ + settingPanel, addedPlayList, downloadingPlayList, downloadedPlayList +from functions.saveSettings import saveSettings +import sys +import os + +class app(ctk.CTk): + def __init__(self, + app_fg_color=("#ffffff", "#101010"), + app_btn_fg_color=("#eeeeee", "#202020"), + app_btn_fg_hover_color=("#cccccc", "#252525"), + app_frame_fg_color=("#ffffff", "#101010"), + app_widget_fg_color=("#ffffff", "#101010"), + app_widget_text_color=("#404040", "#aaaaaa"), + app_theme_color="#1f9bfd", + app_theme_mode="System", + app_special_color="#fc4a46", + app_theme_colors=[], + download_directory="", + ): + + super().__init__() + self.root_w = self.winfo_width() + self.root_h = self.winfo_height() + + self.app_fg_color = app_fg_color + self.app_btn_fg_color = app_btn_fg_color + self.app_btn_fg_hover_color = app_btn_fg_hover_color + self.app_frame_fg_color = app_frame_fg_color + self.app_widget_fg_color = app_widget_fg_color + self.app_widget_text_color = app_widget_text_color + self.app_theme_color = app_theme_color + self.app_special_color = app_special_color + self.app_theme_colors = app_theme_colors + self.app_theme_mode = app_theme_mode + self.reset_widgets = True + self.loop_run = False + self.download_directory = download_directory + self.selected_download_mode = "video" # "video" + + self.downloading = False + self.downloaded = False + self.added = False + + def create_widgets(self): + self.link_entry = ctk.CTkEntry(master=self, height=40, placeholder_text="Enter Youtube URL") + self.add_btn = ctk.CTkButton(master=self, text="Add +", height=40, width=100, command=self.add_video) + + self.video_radio_btn = ctk.CTkRadioButton(master=self, text="Video", fg_color=self.app_theme_color, + radiobutton_width=16, radiobutton_height=16, + width=60, height=18, + command=lambda: self.select_download_mode("video")) + self.video_radio_btn.select() + self.playlist_radio_btn = ctk.CTkRadioButton(master=self, text="Playlist", fg_color=self.app_theme_color, + radiobutton_width=16, radiobutton_height=16, + width=60, height=18, + command=lambda: self.select_download_mode("playlist")) + + self.scroll_frame_added = ctk.CTkScrollableFrame(master=self) + self.scroll_frame_downloading = ctk.CTkScrollableFrame(master=self) + self.scroll_frame_downloaded = ctk.CTkScrollableFrame(master=self) + self.settings_btn = ctk.CTkButton(master=self, text="Setting") + + self.added_btn = ctk.CTkButton(master=self, text="Added", height=40, + command=lambda: self.place_frame(self.scroll_frame_added, "added")) + self.downloading_btn = ctk.CTkButton(master=self, text="Downloading", height=40, + command=lambda: self.place_frame(self.scroll_frame_downloading, + "downloading")) + self.downloaded_btn = ctk.CTkButton(master=self, text="Downloaded", height=40, + command=lambda: self.place_frame(self.scroll_frame_downloaded, + "downloaded")) + + self.added_frame_label = ctk.CTkLabel(master=self, text="Added videos & playlists will be display here.", + text_color=self.app_theme_color, ) + self.downloading_frame_label = ctk.CTkLabel(master=self, text="Downloading videos & playlists will be display here.", + text_color=self.app_theme_color, ) + self.downloaded_frame_label = ctk.CTkLabel(master=self, text="Downloaded videos & playlists will be display here.", + text_color=self.app_theme_color, ) + + self.settings_panel = settingPanel.settingPanel(master=self, fg_color=self.app_fg_color, + bg_color=self.app_fg_color, + text_color=self.app_widget_text_color, + directory_change_call_back=self.directory_change_callback, + theme_color_change_call_back=self.theme_color_change_callback, + ) + self.setting_btn = ctk.CTkButton(master=self, text="⚑", border_spacing=0, + hover=False, fg_color=self.app_fg_color, width=30, height=40, + command=self.open_settings) + + def place_forget_frames(self): + self.scroll_frame_added.place_forget() + self.scroll_frame_downloading.place_forget() + self.scroll_frame_downloaded.place_forget() + + def place_forget_labels(self, label: str = None): + if label == None: + self.added_frame_label.place_forget() + self.downloading_frame_label.place_forget() + self.downloaded_frame_label.place_forget() + elif label == "added": + self.added_frame_label.place_forget() + elif label == "downloading": + self.downloading_frame_label.place_forget() + elif label == "downloaded": + self.downloaded_frame_label.place_forget() + + def place_label(self, frame_name: str): + self.place_forget_labels() + if frame_name == "added" and self.added is not True: + self.added_frame_label.place(y=45, rely=0.5, relx=0.5, anchor="center") + elif frame_name == "downloading" and self.downloading is not True: + self.downloading_frame_label.place(y=45, rely=0.5, relx=0.5, anchor="center") + elif frame_name == "downloaded" and self.downloaded is not True: + self.downloaded_frame_label.place(y=45, rely=0.5, relx=0.5, anchor="center") + + def place_frame(self, frame: ctk.CTkScrollableFrame, frame_name: str): + self.place_forget_frames() + frame.place(y=90, x=10) + self.place_label(frame_name) + + def set_widgets_size(self): + root_width = self.winfo_width() + root_height = self.winfo_height() + self.link_entry.configure(width=root_width - 250) + + btn_width = (root_width - 26) / 3 + self.added_btn.configure(width=btn_width) + self.downloading_btn.configure(width=btn_width) + self.downloading_btn.place(x=btn_width + 10 + 3) + self.downloaded_btn.configure(width=btn_width) + self.downloaded_btn.place(x=btn_width * 2 + 10 + 6) + + frame_height = root_height - 105 + frame_width = root_width - 40 + self.scroll_frame_added.configure(height=frame_height, width=frame_width) + self.scroll_frame_downloading.configure(height=frame_height, width=frame_width) + self.scroll_frame_downloaded.configure(height=frame_height, width=frame_width) + + def loop(self): + self.loop_run = True + change = False + + if self.root_w != self.winfo_width() or self.root_h != self.winfo_height(): + change = True + self.reset_widgets = True + self.root_w = self.winfo_width() + self.root_h = self.winfo_height() + + if self.reset_widgets and change == False: + self.loop_run = False + self.reset_widgets = False + self.set_widgets_size() + elif self.reset_widgets == False and change == False: + self.loop_run = False + pass + else: + self.after(200, self.loop) + + def check_root_size(self, e): + if not self.loop_run: + self.loop() + + def place_widgets(self): + self.link_entry.place(x=43) + self.video_radio_btn.place(relx=1, x=-190, y=2) + self.playlist_radio_btn.place(relx=1, x=-190, y=22) + self.add_btn.place(relx=1, x=-110) + self.added_btn.place(y=45, x=10) + self.downloading_btn.place(y=45) + self.downloaded_btn.place(y=45) + self.setting_btn.place(x=-5) + self.bind("", self.check_root_size) + + def set_widgets_colors(self): + self.configure(fg_color=self.app_fg_color) + self.link_entry.configure(fg_color=self.app_fg_color, border_color=self.app_theme_color) + self.add_btn.configure(fg_color=self.app_btn_fg_color, border_color=self.app_theme_color, + hover_color=self.app_btn_fg_hover_color, text_color=self.app_theme_color, + border_width=2) + + self.added_btn.configure(fg_color=self.app_btn_fg_color, hover_color=self.app_btn_fg_hover_color, + text_color=self.app_theme_color) + self.downloading_btn.configure(fg_color=self.app_btn_fg_color, hover_color=self.app_btn_fg_hover_color, + text_color=self.app_theme_color) + self.downloaded_btn.configure(fg_color=self.app_btn_fg_color, hover_color=self.app_btn_fg_hover_color, + text_color=self.app_theme_color) + + self.scroll_frame_added.configure(fg_color=self.app_frame_fg_color) + self.scroll_frame_downloading.configure(fg_color=self.app_frame_fg_color) + self.scroll_frame_downloaded.configure(fg_color=self.app_frame_fg_color) + self.setting_btn.configure(text_color=self.app_theme_color) + + def set_widgets_fonts(self): + self.link_entry.configure(font=ctk.CTkFont(family="arial", size=16, weight="normal", slant="italic", underline=True)) + self.video_radio_btn.configure(font=("Monospace", 12, "bold")) + self.playlist_radio_btn.configure(font=("Monospace", 12, "bold")) + self.add_btn.configure(font=("arial", 15, "bold")) + self.added_btn.configure(font=("arial", 15, "bold")) + font_style = ctk.CTkFont(family="arial", size=16, weight="bold", slant="italic") + self.added_frame_label.configure(font=font_style) + self.downloading_frame_label.configure(font=font_style) + self.downloaded_frame_label.configure(font=font_style) + self.downloading_btn.configure(font=("arial", 15, "bold")) + self.downloaded_btn.configure(font=("arial", 15, "bold")) + self.setting_btn.configure(font=("arial", 25, "normal")) + + def select_download_mode(self, download_mode): + self.selected_download_mode = download_mode + if download_mode == "playlist": + self.video_radio_btn.deselect() + else: + self.playlist_radio_btn.deselect() + + def add_video(self): + self.added = True + self.place_forget_labels("added") + yt_url = self.link_entry.get() + if self.selected_download_mode == "video": + addedVideo.addedVideo(master=self.scroll_frame_added, + height=70, + width=self.scroll_frame_added.winfo_width(), + fg_color=self.app_widget_fg_color, + bg_color=self.app_fg_color, + theme_color=self.app_theme_color, + text_color=self.app_widget_text_color, + hover_color=self.app_btn_fg_hover_color, + special_color=self.app_special_color, + + border_width=1, + url=yt_url, download_btn_command=self.download_video). \ + pack(fill="x", pady=2) + else: + addedPlayList.addedPlayList(master=self.scroll_frame_added, + height=85, + width=self.scroll_frame_added.winfo_width(), + fg_color=self.app_widget_fg_color, + bg_color=self.app_fg_color, + theme_color=self.app_theme_color, + text_color=self.app_widget_text_color, + hover_color=self.app_btn_fg_hover_color, + special_color=self.app_special_color, + download_btn_command=self.download_playlist, + video_download_btn_command=self.download_video, + border_width=1, + playlist_url=yt_url). \ + pack(fill="x", pady=2) + + def download_video(self, video: addedVideo.addedVideo): + self.downloading = True + self.place_forget_labels("downloading") + downloadingVideo.downloadingVideo(master=self.scroll_frame_downloading, + height=70, + border_width=1, + width=self.scroll_frame_downloading.winfo_width(), + + fg_color=self.app_widget_fg_color, + bg_color=self.app_fg_color, + theme_color=self.app_theme_color, + text_color=self.app_widget_text_color, + hover_color=self.app_btn_fg_hover_color, + special_color=self.app_special_color, + + channel_url=video.channel_url, + url=video.url, + download_quality=video.download_quality, + download_type=video.download_type, + title=video.title, + channel=video.channel, + thumbnails=video.thumbnails, + video_stream_data=video.video_stream_data, + length=video.length, + + download_directory=self.download_directory, + downloaded_callback_function=self.downloaded_video). \ + pack(fill="x", pady=2) + + def download_playlist(self, playlist: addedPlayList.addedPlayList): + self.downloading = True + self.place_forget_labels("downloading") + downloadingPlayList.downloadingPlayList(master=self.scroll_frame_downloading, + height=85, + width=self.scroll_frame_downloading.winfo_width(), + border_width=1, + + fg_color=self.app_widget_fg_color, + bg_color=self.app_fg_color, + theme_color=self.app_theme_color, + text_color=self.app_widget_text_color, + hover_color=self.app_btn_fg_hover_color, + special_color=self.app_special_color, + + channel_url=playlist.channel_url, + channel=playlist.channel, + playlist_title=playlist.playlist_title, + video_count=playlist.video_count, + playlist_url=playlist.playlist_url, + + download_directory=self.download_directory, + videos=playlist.videos, + downloaded_callback_function=self.downloaded_playlist + ). \ + pack(fill="x", pady=2) + + def downloaded_video(self, video: downloadingVideo.downloadingVideo): + self.downloaded = True + self.place_forget_labels("downloaded") + downloadedVideo.downloadedVideo(master=self.scroll_frame_downloaded, + height=70, + border_width=1, + width=self.scroll_frame_downloaded.winfo_width(), + download_quality=video.download_quality, + download_type=video.download_type, + + fg_color=self.app_widget_fg_color, + bg_color=self.app_fg_color, + text_color=self.app_widget_text_color, + hover_color=self.app_btn_fg_hover_color, + theme_color=self.app_theme_color, + special_color=self.app_special_color, + + thumbnails=video.thumbnails, + title=video.title, + channel=video.channel, + channel_url=video.channel_url, + url=video.url, + + download_path=video.download_file_name, + file_size=video.total_bytes, + length=video.length + ). \ + pack(fill="x", pady=2) + + def downloaded_playlist(self, playlist: downloadingPlayList.downloadingPlayList): + self.downloaded = True + self.place_forget_labels("downloaded") + downloadedPlayList.downloadedPlayList(master=self.scroll_frame_downloaded, + height=85, + width=self.scroll_frame_downloaded.winfo_width(), + border_width=1, + + fg_color=self.app_widget_fg_color, + bg_color=self.app_fg_color, + theme_color=self.app_theme_color, + text_color=self.app_widget_text_color, + hover_color=self.app_btn_fg_hover_color, + special_color=self.app_special_color, + + channel_url=playlist.channel_url, + channel=playlist.channel, + playlist_title=playlist.playlist_title, + video_count=playlist.video_count, + playlist_url=playlist.playlist_url, + + videos=playlist.downloading_videos). \ + pack(fill="x", pady=2) + + def theme_color_change_callback(self, new_theme_color): + self.app_theme_color = new_theme_color + self.set_widgets_colors() + for video_object in self.scroll_frame_added.winfo_children(): + if type(video_object) == addedVideo.addedVideo or type(video_object) == addedPlayList.addedPlayList: + video_object.set_new_theme(self.app_theme_color) + + for video_object in self.scroll_frame_downloading.winfo_children(): + if type(video_object) == downloadingVideo.downloadingVideo or type( + video_object) == downloadingPlayList.downloadingPlayList: + video_object.set_new_theme(self.app_theme_color) + + for video_object in self.scroll_frame_downloaded.winfo_children(): + if type(video_object) == downloadedVideo.downloadedVideo or type( + video_object) == downloadedPlayList.downloadedPlayList: + video_object.set_new_theme(self.app_theme_color) + + self.added_frame_label.configure(text_color=self.app_theme_color) + self.downloading_frame_label.configure(text_color=self.app_theme_color) + self.downloaded_frame_label.configure(text_color=self.app_theme_color) + self.video_radio_btn.configure(fg_color=self.app_theme_color) + self.playlist_radio_btn.configure(fg_color=self.app_theme_color) + + def directory_change_callback(self, download_directory): + self.download_directory = download_directory + + def open_settings(self): + self.settings_panel.place(relwidth=1, relheight=1) + self.setting_btn.configure(command=self.close_settings) + + def close_settings(self): + self.settings_panel.place_forget() + self.setting_btn.configure(command=self.open_settings) + + def set_geometry(self, geometry: str): + self.geometry(geometry) + + def clear_temp(self): + for file in os.listdir("temp") : + try: + if file != 'this directory is necessary': + os.remove("temp\\"+file) + except: + pass + + def on_closing(self): + data = { + 'download_directory': self.download_directory, + 'geometry': self.geometry(), + } + self.clear_temp() + saveSettings("settings/general.json", data) + self.destroy() + os._exit(1) + + def run(self): + self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.mainloop() \ No newline at end of file diff --git a/dependencies_installer.py b/dependencies_installer.py new file mode 100644 index 0000000..055cfe7 --- /dev/null +++ b/dependencies_installer.py @@ -0,0 +1,3 @@ +import os + +os.system("pip install -r requirements.txt") \ No newline at end of file diff --git a/functions/convertTime.py b/functions/convertTime.py new file mode 100644 index 0000000..0606307 --- /dev/null +++ b/functions/convertTime.py @@ -0,0 +1,9 @@ +def convertTime(s: int) -> str: + h = int(s/3600) + m = int((s - (h*3600)) / 60) + s = s - (h*3600) - (m*60) + if h>0: + converted_time = f"{h}:{m:0>2}:{s:0>2}" + else: + converted_time = f"{m}:{s:0>2}" + return converted_time \ No newline at end of file diff --git a/functions/createDownloadDirectory.py b/functions/createDownloadDirectory.py new file mode 100644 index 0000000..8941adb --- /dev/null +++ b/functions/createDownloadDirectory.py @@ -0,0 +1,12 @@ +import os + +def createDownloadDirectory(path): + try: + if not os.path.exists(path): + sub_paths = path.split("\\") + for i in range(0, len(sub_paths)): + sub_path = "\\".join(sub_paths[0:i+1]) + if not os.path.exists(sub_path): + os.mkdir(sub_path) + except Exception: + raise BufferError("download path errror :/") \ No newline at end of file diff --git a/functions/formatToComboBoxValues.py b/functions/formatToComboBoxValues.py new file mode 100644 index 0000000..7fbfa8e --- /dev/null +++ b/functions/formatToComboBoxValues.py @@ -0,0 +1,8 @@ +from .getConvertedSize import getConvertedSize + +def formatToComboBoxValues(supported_download_types): + reso_box_values = [] + for data_dict in supported_download_types: + for data_key in data_dict: + reso_box_values.append(data_key + " | " + getConvertedSize(data_dict[data_key] ,1)) + return (reso_box_values) \ No newline at end of file diff --git a/functions/getColor.py b/functions/getColor.py new file mode 100644 index 0000000..821f6eb --- /dev/null +++ b/functions/getColor.py @@ -0,0 +1,4 @@ +def getColor(color, theme): + if theme == "Dark": + return color[1] + return color[0] \ No newline at end of file diff --git a/functions/getConvertedPath.py b/functions/getConvertedPath.py new file mode 100644 index 0000000..43407ee --- /dev/null +++ b/functions/getConvertedPath.py @@ -0,0 +1,2 @@ +def getConvertedPath(path): + return path.replace("/","\\") \ No newline at end of file diff --git a/functions/getConvertedSize.py b/functions/getConvertedSize.py new file mode 100644 index 0000000..8845c4a --- /dev/null +++ b/functions/getConvertedSize.py @@ -0,0 +1,13 @@ +def getConvertedSize(s,with_decimal): + datas = ["B","KB","MB","GB","TB","PB","EB"] + index= 0 + + while len(str(int(s))) > 3 and (index+1) < len(datas) : + s = s /1024 + index += 1 + if with_decimal > 0: + val = str(round(s,with_decimal)) + " " + datas[index] + else: + val = str(int(s)) + " " + datas[index] + + return val \ No newline at end of file diff --git a/functions/getGeneralSettings.py b/functions/getGeneralSettings.py new file mode 100644 index 0000000..5783289 --- /dev/null +++ b/functions/getGeneralSettings.py @@ -0,0 +1,6 @@ +import json + +def getGeneralSettings(): + file_name = ".\\settings\\general.json" + settings = json.load(open(file_name, "r")) + return settings \ No newline at end of file diff --git a/functions/getSupportedDownloadTypes.py b/functions/getSupportedDownloadTypes.py new file mode 100644 index 0000000..c10626c --- /dev/null +++ b/functions/getSupportedDownloadTypes.py @@ -0,0 +1,31 @@ +import pytube + +def toDict(data): + data_list = [] + for d in data: + data_list.append({value.split("=")[0]:value.split("=")[1] for value in str(d)[9:-1].replace('"',"").split(" ")}) + return data_list + + +def getSupportedDownloadTypes(video_streams: pytube.StreamQuery): + supportedDownloadTypes = [] + data = toDict(video_streams.all()) + for type_info in data: + if type_info["type"] == "video": + try: + file_size = video_streams.get_by_resolution(type_info["res"]).filesize + download_info = {type_info["res"] : file_size} + if download_info not in supportedDownloadTypes: + supportedDownloadTypes.append(download_info) + except: + #print("Error :",type_info["res"]) + pass + try: + audio_only = video_streams.get_audio_only() + file_size = audio_only.filesize + bitrate = str(int((audio_only.bitrate)/1024)) + "kbps" + supportedDownloadTypes.append({bitrate: file_size}) + except: + pass + #print("Error :","Audio") + return supportedDownloadTypes \ No newline at end of file diff --git a/functions/getThemeSettings.py b/functions/getThemeSettings.py new file mode 100644 index 0000000..1b5c82e --- /dev/null +++ b/functions/getThemeSettings.py @@ -0,0 +1,9 @@ +import json + +def getThemeSettings(): + file_name = ".\\settings\\theme.json" + settings = json.load(open(file_name, "r")) + for key in settings.keys(): + if type(settings[key]) == list: + settings[key] = tuple(settings[key]) + return settings \ No newline at end of file diff --git a/functions/getThumbnails.py b/functions/getThumbnails.py new file mode 100644 index 0000000..a233931 --- /dev/null +++ b/functions/getThumbnails.py @@ -0,0 +1,65 @@ +from .getValidFileName import getValidFileName +from .removeInvalidCharts import removeInvalidChars +from urllib import request +import tkinter +from PIL import Image, ImageDraw + +def add_corners(im, rad): + circle = Image.new('L', (rad * 2, rad * 2), 0) + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, rad * 2 - 1, rad * 2 - 1), fill=255) + alpha = Image.new('L', im.size, 255) + w, h = im.size + alpha.paste(circle.crop((0, 0, rad, rad)), (0, 0)) + alpha.paste(circle.crop((0, rad, rad, rad * 2)), (0, h - rad)) + alpha.paste(circle.crop((rad, 0, rad * 2, rad)), (w - rad, 0)) + alpha.paste(circle.crop((rad, rad, rad * 2, rad * 2)), (w - rad, h - rad)) + im.putalpha(alpha) + return im + + +def getHoverThumbnail(thumbnail_download_path): + thumbnail_hover_temp = Image.open(thumbnail_download_path) + thumbnail_hover_path = ".".join(thumbnail_download_path.split(".")[0:-1]) + "-hover." + thumbnail_download_path.split(".")[-1] + thumbnail_hover_temp = thumbnail_hover_temp.convert("RGB") + thumbnail_hover_data = thumbnail_hover_temp.getdata() + thumbnail_hover_data_list = [] + for item in thumbnail_hover_data: + item = list(item) + for index ,i in enumerate(item): + if i+30 < 256 : + item[index] = i+30 + else: + item[index] = 255 + thumbnail_hover_data_list.append(tuple(item)) + + thumbnail_hover_temp.putdata(thumbnail_hover_data_list) + #thumbnail_hover_temp.save(thumbnail_hover_path) + thumbnail_hover_corner_rounded = add_corners(thumbnail_hover_temp,6) + thumbnail_hover_corner_rounded.save(thumbnail_hover_path) + thumbnail_hover = tkinter.PhotoImage(file = thumbnail_hover_path) + return thumbnail_hover + + +def getThumbnail(video): + thumbnail_url = video.thumbnail_url + thumbnail_download_path = getValidFileName("./temp/" + removeInvalidChars(thumbnail_url) + ".png") + request.urlretrieve(thumbnail_url, thumbnail_download_path) + + thumbnail_temp = Image.open(thumbnail_download_path) + #print(thumbnail_temp.width) + #print(thumbnail_temp.height) + if round(thumbnail_temp.width/4*3) <= 480: + thumbnail_temp = thumbnail_temp.resize((113, 64),Image.Resampling.LANCZOS).crop((0,8,113,56)).resize((113,64)) + else: + thumbnail_temp = thumbnail_temp.resize((113,64),Image.Resampling.LANCZOS)#.crop((0,7,110,55)) + #else: + # thumbnail_temp = thumbnail_temp.resize((103, 58),Image.Resampling.LANCZOS).crop((0,7,110,55)).resize((97,55)) + thumbnail_temp_corner_rounded = add_corners(thumbnail_temp,6) + thumbnail_temp_corner_rounded.save(thumbnail_download_path) + thumbnail = tkinter.PhotoImage(file=thumbnail_download_path) + return thumbnail, getHoverThumbnail(thumbnail_download_path) + + +def getThumbnails(video): + return getThumbnail(video) diff --git a/functions/getValidFileName.py b/functions/getValidFileName.py new file mode 100644 index 0000000..0ce7556 --- /dev/null +++ b/functions/getValidFileName.py @@ -0,0 +1,11 @@ +import os + +def getValidFileName(path): + splited = path.split(".") + file_name ,extenstion = ".".join(splited[0:-1]),splited[-1] + generate_file_name = file_name + i = 2 + while os.path.exists(generate_file_name+"."+extenstion): + generate_file_name = file_name + " ({})".format(i) + i += 1 + return generate_file_name + "." + extenstion \ No newline at end of file diff --git a/functions/getWindowScale.py b/functions/getWindowScale.py new file mode 100644 index 0000000..778dfcc --- /dev/null +++ b/functions/getWindowScale.py @@ -0,0 +1,4 @@ +import customtkinter as ctk + +def getWindowScale(widget): + return 1/ctk.ScalingTracker.get_widget_scaling(widget) \ No newline at end of file diff --git a/functions/passIt.py b/functions/passIt.py new file mode 100644 index 0000000..67546b6 --- /dev/null +++ b/functions/passIt.py @@ -0,0 +1,2 @@ +def passIt(): + pass \ No newline at end of file diff --git a/functions/removeInvalidCharts.py b/functions/removeInvalidCharts.py new file mode 100644 index 0000000..93b72b9 --- /dev/null +++ b/functions/removeInvalidCharts.py @@ -0,0 +1,6 @@ +def removeInvalidChars(url:str): + filename = url + replaces = ["\\","/",":",'"',"?","<",">","|","*"] + for re in replaces: + filename = filename.replace(re,"~") + return filename \ No newline at end of file diff --git a/functions/saveSettings.py b/functions/saveSettings.py new file mode 100644 index 0000000..c84d68b --- /dev/null +++ b/functions/saveSettings.py @@ -0,0 +1,6 @@ +import json + +def saveSettings(file, settings): + file = open(file, "w") + json.dump(obj=settings, fp=file, indent=8, sort_keys=True) + file.close() \ No newline at end of file diff --git a/functions/sortDict.py b/functions/sortDict.py new file mode 100644 index 0000000..97d7ce1 --- /dev/null +++ b/functions/sortDict.py @@ -0,0 +1,31 @@ +def sortDict(info): + video_keys = [] + audio_keys = [] + for data in (info): + key = (list(data.keys())[0]) + if "kbps" in key: + audio_keys.append(key) + else: + video_keys.append(key) + + for i in range(len(video_keys)): + for i2 in range(len(video_keys)-1): + if (int(video_keys[i2][:-1]) < int(video_keys[i2+1][:-1])): + video_keys[i2], video_keys[i2+1] = video_keys[i2+1], video_keys[i2] + + keys = video_keys+audio_keys + sorted_dict: list = [] + index2 = 0 + break_ = False + while True: + for dict_ in info : + if list(dict_.keys())[0] == keys[index2]: + sorted_dict.append(dict_) + index2 += 1 + if index2>=len(keys): + break_ = True + break + if break_: + break + + return sorted_dict \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..71ab647 --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +from app import app +from functions.getThemeSettings import getThemeSettings +from functions.getGeneralSettings import getGeneralSettings +import customtkinter as ctk +import os + +app_theme_settings = getThemeSettings() +app_general_settings = getGeneralSettings() + +if app_general_settings["download_directory"] == False: + app_general_settings["download_directory"] = f"C:\\Users\\{os.getlogin()}\\Downloads\\PyTube Downloader\\" + +app = app( + app_fg_color = app_theme_settings["app_fg_color"], + app_btn_fg_color = app_theme_settings["app_btn_fg_color"], + app_btn_fg_hover_color = app_theme_settings["app_btn_fg_hover_color"], + app_frame_fg_color = app_theme_settings["app_frame_fg_color"], + app_widget_fg_color = app_theme_settings["app_widget_fg_color"], + app_widget_text_color = app_theme_settings["app_widget_text_color"], + app_theme_color = app_theme_settings["app_theme_color"], + app_theme_colors = app_theme_settings["default_theme_colors"], + app_theme_mode = app_theme_settings["app_theme_mode"], + download_directory = app_general_settings["download_directory"], +) + +ctk.set_appearance_mode(app_theme_settings["app_theme_mode"]) +ctk.deactivate_automatic_dpi_awareness() +app.set_geometry(app_general_settings["geometry"]) +app.iconbitmap("src\\icon.ico") +app.geometry("900x500") +app.attributes("-alpha",0.97) +app.title("PyTube Downloader") +app.create_widgets() +app.place_widgets() +app.set_widgets_colors() +app.set_widgets_fonts() +app.place_frame(app.scroll_frame_added, "added") +app.run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..780392b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Project dependencies +customtkinter==5.2.2 # for ui elements +pillow # for image handling +pytube==15.0.0 # for working with yt \ No newline at end of file diff --git a/settings/general.json b/settings/general.json new file mode 100644 index 0000000..8a62a86 --- /dev/null +++ b/settings/general.json @@ -0,0 +1,4 @@ +{ + "download_directory": false, + "geometry": "900x500+0+0" +} \ No newline at end of file diff --git a/settings/theme.json b/settings/theme.json new file mode 100644 index 0000000..da6447e --- /dev/null +++ b/settings/theme.json @@ -0,0 +1,41 @@ +{ + "app_btn_fg_color": [ + "#eeeeee", + "#202020" + ], + "app_btn_fg_hover_color": [ + "#cccccc", + "#252525" + ], + "app_fg_color": [ + "#ffffff", + "#101010" + ], + "app_frame_fg_color": [ + "#ffffff", + "#101010" + ], + "app_special_color": "#1f9bfd", + "app_theme_color": "#1f9bfd", + "app_theme_mode": "System", + "app_widget_fg_color": [ + "#ffffff", + "#101010" + ], + "app_widget_text_color": [ + "#404040", + "#aaaaaa" + ], + "default_theme_colors": [ + "#1f9bfd", + "#5ffd66", + "#fa63ee", + "#f3465e", + "#f5ab55", + "#f1e85d", + "#adadad", + "#9d9d9d", + "#808080", + "#454545" + ] +} \ No newline at end of file diff --git a/src/icon.ico b/src/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..009185a27e5673d1d8078d368211152515da7e4f GIT binary patch literal 225342 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 ItckY&2Rh#)L;wH) literal 0 HcmV?d00001 diff --git a/temp/this directory is necessary b/temp/this directory is necessary new file mode 100644 index 0000000..51501d7 --- /dev/null +++ b/temp/this directory is necessary @@ -0,0 +1 @@ +temp directory :D \ No newline at end of file diff --git a/widgets/Video.py b/widgets/Video.py new file mode 100644 index 0000000..807e27b --- /dev/null +++ b/widgets/Video.py @@ -0,0 +1,193 @@ +import customtkinter as ctk +import tkinter as tk +import webbrowser +import time +import threading +from functions.getColor import getColor +from functions.convertTime import convertTime + +class Video(ctk.CTkFrame): + def __init__(self, + master, + border_width=None, + width=None, + height=None, + url=None, + title = "---------", + channel = "---------", + thumbnails = (None, None), + channel_url = None, + loading_done = False, + length = 0, + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None): + + super().__init__(master=master, border_width=border_width, corner_radius=8, + fg_color=fg_color, bg_color=bg_color, height=height, width=width) + + self.loading_done = loading_done + self.url = url + self.title = title + self.channel = channel + self.thumbnails = thumbnails + self.supported_download_types = ["..........", "..........", ".........."] + + self.channel_url = channel_url + self.length = length + self.theme = "-" + self.fg_color = fg_color + self.text_color = text_color + self.height = height + self.theme_color = theme_color + self.hover_color = hover_color + self.special_color = special_color + + self.killed = False + + + self.create_widgets() + self.place_widgets() + threading.Thread(target=self.RunThemeTracker).start() + self.set_video_data() + + + def create_widgets(self): + self.thumbnail_btn = tk.Button(master=self, + bd=0, + font=("arial", 14, "bold"), + bg=self.getColorBasedOnTheme(self.fg_color), + relief="sunken", + state="disabled", + cursor="hand2", + command=lambda:webbrowser.open(self.url) + ) + + self.len_label = ctk.CTkLabel(master=self, width=1, height=1, + text_color=self.text_color, + fg_color=self.fg_color, + bg_color=self.fg_color, + font=("arial", 11, "bold")) + + self.title_label = tk.Label(master=self ,anchor="w", + text = "Title : ", + font=('arial',10,'normal'), + bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color) + ) + + self.channel_label = tk.Button(master=self ,font=('arial',9,'bold'), + anchor="w", + text = "Channel : ", + bd=0, + bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color), + command=lambda:webbrowser.open(self.channel_url), + relief="sunken", + state="disabled", + cursor="hand2", + + ) + + self.url_label = tk.Label(master=self ,anchor="w", + bg=self.getColorBasedOnTheme(self.fg_color), + text=self.url, font=('arial',10,"italic underline"), + ) + + self.remove_btn = ctk.CTkButton(master=self, + command=self.kill, + text="X", + font=("arial", 12, "bold"), + fg_color=self.fg_color, + bg_color=self.fg_color, + border_width=2, + border_color=self.special_color, + text_color=self.special_color, + width=12, height=10, + border_spacing=0, + hover=False, + ) + + self.bind("", self.configure_widget_sizes) + + + def set_theme(self): + self.configure(border_color=self.theme_color) + + self.thumbnail_btn.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color), + disabledforeground=self.getColorBasedOnTheme(self.text_color), + activebackground=self.getColorBasedOnTheme(self.fg_color)) + + self.title_label.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color)) + + self.url_label.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.theme_color) + + self.channel_label.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color), + activebackground=self.getColorBasedOnTheme(self.fg_color), + activeforeground=self.theme_color,) + + + def getColorBasedOnTheme(self, color): + return getColor(color, self.theme) + + + def RunThemeTracker(self): + while True: + if ctk.get_appearance_mode() != self.theme: + self.theme = ctk.get_appearance_mode() + self.set_theme() + time.sleep(2) + + + def configure_widget_sizes(self, e): + self.title_label.place(width=self.winfo_width()-450) + self.url_label.place(width=self.winfo_width()-450) + self.channel_label.place(width=self.winfo_width()-450) + + + def place_widgets(self): + self.remove_btn.place(relx=1, x=-24, y=4) + self.thumbnail_btn.place(x=5, y=2, relheight=1, height=-4, width=int((self.height-4)/9*16)) + self.len_label.place(rely=1, y=-10, x=117, anchor="e") + self.title_label.place(x=130, y=4, height=20) + self.channel_label.place(x=130, y=24, height=20) + self.url_label.place(x=130, y=44, height=20) + + + def set_video_data(self): + self.title_label.configure(text="Title : "+self.title) + self.channel_label.configure(text="Channel : "+self.channel) + self.url_label.configure(text=self.url) + self.len_label.configure(text=convertTime(self.length)) + if self.loading_done: + self.thumbnail_btn.configure(image=self.thumbnails[0], text="") + def thumbnail_hover_img_set(e): + self.thumbnail_btn.configure(image=self.thumbnails[1], text="") + def thumbnail_img_set(e): + self.thumbnail_btn.configure(image=self.thumbnails[0], text="") + self.thumbnail_btn.bind("",thumbnail_hover_img_set) + self.thumbnail_btn.bind("",thumbnail_img_set) + self.len_label.bind("",thumbnail_hover_img_set) + self.len_label.bind("",thumbnail_img_set) + + + def kill(self): + for child_widget in self.winfo_children(): + for sub_child_widget in child_widget.winfo_children(): + sub_child_widget.destroy() + child_widget.destroy() + self.pack_forget() + self.destroy() + + + def set_new_theme(self, new_theme_color): + self.theme_color = new_theme_color + self.set_theme() \ No newline at end of file diff --git a/widgets/addedPlayList.py b/widgets/addedPlayList.py new file mode 100644 index 0000000..f23fff2 --- /dev/null +++ b/widgets/addedPlayList.py @@ -0,0 +1,218 @@ +from .playList import playList +import pytube +from .addedVideo import addedVideo +import threading +import time +import customtkinter as ctk + +class addedPlayList(playList): + def __init__(self, + master=None, + width=None, + height=None, + border_width=None, + + playlist_url=None, + channel_url = None, + playlist_title = "---------", + channel = "---------", + + download_btn_command = None, + video_download_btn_command = None, + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None, + ): + + super().__init__( + master=master, + height=height, + width=width, + border_width=border_width, + + channel_url = channel_url, + playlist_url=playlist_url, + playlist_title=playlist_title, + channel=channel, + + bg_color=bg_color, + fg_color=fg_color, + text_color=text_color, + theme_color=theme_color, + hover_color=hover_color, + special_color=special_color, + ) + + self.removed_count = 0 + self.download_btn_command = download_btn_command + self.video_download_btn_command = video_download_btn_command + self.videos_thumbnails = [] + self.videos = [] + threading.Thread(target=self.get_playlist_data).start() + + + def create_widgets(self): + super().create_widgets() + self.info_frame = ctk.CTkFrame(master=self.playlist_info_widget, + height=self.height-4, + width=270, + bg_color=self.fg_color, + fg_color=self.fg_color) + self.resolutions_box = ctk.CTkComboBox(master=self.info_frame, values=["..........", "..........", ".........."]) + + #⇩ – + self.download_btn = ctk.CTkButton(master=self.info_frame, text="Download", + width=80, height=25, + border_width=2, + fg_color=self.fg_color, + bg_color=self.fg_color, + hover_color=self.hover_color, + text_color=self.text_color, + state="disabled", + command=lambda:self.download_btn_command(self)) + + self.status_label = ctk.CTkLabel(master=self.info_frame, + text="Loading", + height=15, + font=("arial", 13, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + ) + + self.reload_btn = ctk.CTkButton(self.playlist_info_widget ,text="⟳", + width=15 ,height=15, + font=("arial", 22, "normal"), + command = self.reload_playlist, + fg_color=self.fg_color, + bg_color=self.fg_color, + hover=False, + ) + + + def set_theme(self): + super().set_theme() + self.download_btn.configure(border_color=self.theme_color) + if self.status_label.cget("text") != "Failed": + self.status_label.configure(text_color=self.theme_color) + self.reload_btn.configure(text_color=self.theme_color) + for video in self.videos: + video.set_new_theme(self.theme_color) + + + def place_widgets(self): + super().place_widgets() + self.info_frame.place(y=2, relx=1, x=-330) + self.resolutions_box.place(y=25,x=0) + self.download_btn.place(x=160 ,y=12) + self.status_label.place(x=200, anchor="n", y=52) + + + def reload_playlist(self): + self.reload_btn.place_forget() + self.status_label.configure(text_color=self.theme_color, text="Loading") + if len(self.videos) != 0: + for video in self.videos: + if video.loading_failed: + video.reload_video() + else: + threading.Thread(target=self.get_playlist_data).start() + + + def get_playlist_data(self): + self.view_btn.configure(state="disabled") + try: + self.playlist = pytube.Playlist(self.playlist_url) + self.video_count = self.playlist.length + self.channel = self.playlist.owner + self.playlist_title = self.playlist.title + self.channel_url = self.playlist.owner_url + self.view_btn.configure(state="normal") + self.set_playlist_data() + self.get_videos_data() + except: + self.set_loading_failed() + + + def get_videos_data(self): + for video_url in (self.playlist.video_urls): + self.videos.append( + addedVideo(master=self.playlist_item_frame, + height=70, + width=self.playlist_item_frame.winfo_width()-20, + fg_color=self.fg_color, + bg_color=self.bg_color, + theme_color = self.theme_color, + text_color=self.text_color, + hover_color=self.hover_color, + special_color=self.special_color, + border_width=1, + url=video_url, download_btn_command=self.video_download_btn_command) + ) + self.videos[-1].remove_btn.configure(command=lambda video = self.videos[-1]: self.remove_video(video)) + self.videos[-1].pack(fill="x", padx=(20,0), pady=1) + threading.Thread(target=self.progress_track).start() + + + def progress_track(self): + videos = self.videos.copy() + loaded = 0 + while True: + load_fails = False + for video in videos: + if video not in self.videos: + videos.remove(video) + continue + if video.loading_failed: + self.set_loading_failed() + load_fails = True + elif video.loading_done: + loaded += 1 + videos.remove(video) + if loaded == len(self.videos): + break + if not load_fails: + self.reload_btn.place_forget() + self.status_label.configure(text="Loading", text_color=self.theme_color) + time.sleep(1) + self.set_loading_done() + + + def set_loading_failed(self): + self.status_label.configure(text="Failed", text_color=self.special_color) + self.reload_btn.place(relx=1, y=32, x=-80) + + + def set_loading_done(self): + self.status_label.configure(text="Loaded") + self.download_btn.configure(state="normal") + self.resolutions_box.configure(values=["Highest Quality", "Lowest Quality", "Audio Only"]) + self.resolutions_box.set("Highest Quality") + self.resolutions_box.configure(command=self.select_download_option) + + + def select_download_option(self, e): + if e=="Highest Quality": + index = 0 + elif e=="Lowest Quality": + index = -2 + elif e=="Audio Only": + index = -1 + for video in self.videos: + video.resolutions_box.set(video.resolutions_box.cget("values")[index]) + video.select_download_option(video.resolutions_box.cget("values")[index]) + + + def remove_video(self, video): + self.videos.remove(video) + self.complete_count_label.configure(text=len(self.videos)) + self.video_count -= 1 + video.pack_forget() + video.kill() + + def kill(self): + for video in self.videos: + video.kill() + super().kill() \ No newline at end of file diff --git a/widgets/addedVideo.py b/widgets/addedVideo.py new file mode 100644 index 0000000..29974db --- /dev/null +++ b/widgets/addedVideo.py @@ -0,0 +1,222 @@ +from .Video import Video +import customtkinter as ctk +from functions.getThumbnails import getThumbnails +from functions.passIt import passIt +from functions.getSupportedDownloadTypes import getSupportedDownloadTypes +from functions.sortDict import sortDict +from functions.formatToComboBoxValues import formatToComboBoxValues +import time +import pytube +import threading + + +class addedVideo(Video): + dot_count = 1 + + waiting_started = False + loading_configure_started = False + + simultaneous_loading = 0 + max_simultaneous_loading = 1 + + waiting_added_videos = [] + + def configure_loading(): + def loading(): + while True: + addedVideo.dot_count += 1 + if addedVideo.dot_count > 4: + addedVideo.dot_count = 1 + time.sleep(0.7) + threading.Thread(target=loading).start() + + def waiting_for_loading(): + def waiting(): + while True: + if addedVideo.max_simultaneous_loading > addedVideo.simultaneous_loading and len(addedVideo.waiting_added_videos) > 0: + try: + addedVideo.waiting_added_videos[0].start_get_video_data() + except: + pass + addedVideo.waiting_added_videos.pop(0) + time.sleep(1) + threading.Thread(target=waiting).start() + + + def __init__(self, master, + border_width=None, + width=None, + height=None, + url=None, + download_btn_command=passIt, + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None, + ): + + if not addedVideo.waiting_started: + addedVideo.waiting_started = True + addedVideo.waiting_for_loading() + + if not addedVideo.loading_configure_started: + addedVideo.loading_configure_started = True + addedVideo.configure_loading() + + self.loading_failed = False + self.loading_loop_running = True + self.download_btn_command = download_btn_command + + super().__init__(master=master, border_width=border_width, theme_color=theme_color, hover_color=hover_color, + special_color=special_color, width=width, + fg_color=fg_color, bg_color=bg_color, height=height ,url=url, text_color=text_color) + + threading.Thread(target=self.loading).start() + + self.start_get_video_data() + + + def start_get_video_data(self): + if addedVideo.max_simultaneous_loading > addedVideo.simultaneous_loading : + addedVideo.simultaneous_loading += 1 + self.status_label.configure(text="Loading") + threading.Thread(target=self.get_video_data).start() + else: + #print("Here") + addedVideo.waiting_added_videos.append(self) + self.status_label.configure(text="Waiting") + + + def loading(self): + while not self.loading_done and not self.loading_failed : + self.loading_loop_running = True + self.thumbnail_btn.configure(text="."*addedVideo.dot_count) + self.update() + time.sleep(0.7) + if not self.loading_failed: + self.set_video_data() + self.set_fetch_data() + self.loading_loop_running = False + + + def get_video_data(self): + try: + #print("Loading : ",addedVideo.simultaneous_loading) + self.video = pytube.YouTube(self.url) + self.title = self.video.title + self.channel = self.video.author + self.length = self.video.length + self.video_stream_data = self.video.streams + self.channel_url = self.video.channel_url + self.thumbnails = getThumbnails(self.video) + self.supported_download_types = sortDict(getSupportedDownloadTypes(self.video_stream_data)) + self.loading_done = True + self.status_label.configure(text="Loaded") + addedVideo.simultaneous_loading -= 1 + except Exception as error: + if self.killed is not True: + addedVideo.simultaneous_loading -= 1 + self.set_loading_failed() + + + def select_download_option(self, e: str): + self.download_quality = e.replace(" ","").split("|")[0] + if "kbps" in self.download_quality: + self.download_type = "Audio" + else: + self.download_type = "Video" + + + def set_fetch_data(self): + self.resolutions_box.configure(values=formatToComboBoxValues(self.supported_download_types)) + self.resolutions_box.set(self.resolutions_box.cget("values")[0]) + self.select_download_option(self.resolutions_box.get()) + self.resolutions_box.configure(command=self.select_download_option) + self.thumbnail_btn.configure(state="normal") + self.channel_label.configure(state="normal") + self.download_btn.configure(state="normal") + + + def create_widgets(self): + super().create_widgets() + self.info_frame = ctk.CTkFrame(master=self, + height=self.height-4, + width=250, + bg_color=self.fg_color, + fg_color=self.fg_color) + self.resolutions_box = ctk.CTkComboBox(master=self.info_frame, values=[".........."]) + + #⇩ – + self.download_btn = ctk.CTkButton(master=self.info_frame, text="Download", width=80, height=25, + border_width=2, + fg_color=self.fg_color, bg_color=self.fg_color, + hover_color=self.hover_color, + text_color=self.text_color, + state="disabled", + command=lambda:self.download_btn_command(self)) + + self.status_label = ctk.CTkLabel(master=self.info_frame, + text="", + height=15, + text_color=self.theme_color, + font=("arial", 12, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + ) + + self.reload_btn = ctk.CTkButton(self ,text="⟳", + width=15 ,height=15, + font=("arial", 20, "normal"), + command = self.reload_video, + fg_color=self.fg_color, + bg_color=self.fg_color, + hover=False, + ) + # ⏯ β†Ί ↻ ⏡ ⏸ β–· + + def set_theme(self): + super().set_theme() + self.download_btn.configure(border_color=self.theme_color) + if self.status_label.cget("text") != "Failed": + self.status_label.configure(text_color=self.theme_color) + self.reload_btn.configure(text_color=self.theme_color) + + + def place_widgets(self): + super().place_widgets() + self.info_frame.place(y=2, relx=1, x=-350) + self.resolutions_box.place(y=15,x=20) + self.download_btn.place(x=170 ,y=8) + self.status_label.place(x=210, anchor="n", y=44) + + + def set_loading_failed(self): + self.loading_failed = True + while self.loading_loop_running: + time.sleep(1) + self.master.master.master.update() + self.thumbnail_btn.configure(text="...", + disabledforeground=self.special_color) + self.status_label.configure(text_color=self.special_color, + text="Failed") + self.reload_btn.place(relx=1, y=22, x=-80) + + + def reload_video(self): + self.loading_failed = False + self.reload_btn.place_forget() + self.thumbnail_btn.configure(disabledforeground=self.getColorBasedOnTheme(self.text_color)) + self.status_label.configure(text_color=self.theme_color, text="Loading") + threading.Thread(target=self.loading).start() + self.start_get_video_data() + + def kill(self): + self.killed = True + if self in addedVideo.waiting_added_videos: + addedVideo.waiting_added_videos.remove(self) + elif self.loading_done is not True: + addedVideo.simultaneous_loading -= 1 + super().kill() \ No newline at end of file diff --git a/widgets/colorBtn.py b/widgets/colorBtn.py new file mode 100644 index 0000000..847f9c1 --- /dev/null +++ b/widgets/colorBtn.py @@ -0,0 +1,53 @@ +import customtkinter as ctk + + +class ColorBtn(ctk.CTkButton): + def __init__(self, + master=None, + height=1, + width=1, + fg_color=None, + bg_color=None, + corner_radius=0, + hover=False, + text="", + font=None, + text_color=None, + border_width=0, + ): + super().__init__(master=master, height=height, width=width, fg_color=fg_color,border_width=border_width,text_color=text_color, + bg_color=bg_color, corner_radius=corner_radius, hover=hover, text=text, font=font) + self.height = height + self.width = width + self.bind("", self.react) + self.bind("", self.react_back) + + + def react(self, e): + self.configure(width=self.cget("width")+4, + height=self.cget("height")+4) + self.grid(pady=8, padx=2) + + + def react_back(self, e): + self.configure(width=self.cget("width")-4, + height=self.cget("height")-4) + self.grid(pady=10, padx=4) + + + def set_clicked(self): + self.configure(state="disabled") + self.unbind("") + self.unbind("") + self.set_unclicked_before_btn() + + + def set_unclicked_before_btn(self): + for widget in self.master.winfo_children(): + if type(widget) == ColorBtn and widget!=self: + if widget.cget("width") > self.width: + widget.bind("", widget.react) + widget.bind("", widget.react_back) + widget.configure(width=self.width, height=self.height) + widget.grid(pady=10, padx=4) + widget.configure(state="normal") \ No newline at end of file diff --git a/widgets/downloadedPlayList.py b/widgets/downloadedPlayList.py new file mode 100644 index 0000000..72cb4fe --- /dev/null +++ b/widgets/downloadedPlayList.py @@ -0,0 +1,94 @@ +import tkinter as tk +from .playList import playList +from .downloadingPlayList import downloadingPlayList +from .downloadedVideo import downloadedVideo +from .downloadingVideo import downloadingVideo + +class downloadedPlayList(playList): + def __init__(self, + master=None, + width=None, + height=None, + border_width=None, + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None, + + channel_url = None, + playlist_url=None, + playlist_title = "---------", + channel = "---------", + video_count = "?", + + videos=None, + ): + super().__init__(master=master, + width=width, + height=height, + border_width=border_width, + + bg_color=bg_color, + fg_color=fg_color, + text_color=text_color, + theme_color=theme_color, + hover_color=hover_color, + special_color=special_color, + + channel_url=channel_url, + playlist_url=playlist_url, + playlist_title=playlist_title, + channel=channel, + video_count=video_count,) + + self.videos: list[downloadingVideo] = videos + self.downloaded_videos = [] + self.create_downloaded_widgets() + + def create_downloaded_widgets(self): + for video in self.videos: + self.downloaded_videos.append( + downloadedVideo(master=self.playlist_item_frame, + height=70, + border_width=1, + width=self.playlist_item_frame.winfo_width()-20, + download_quality=video.download_quality, + download_type=video.download_type, + + fg_color=self.fg_color, + bg_color=self.bg_color, + text_color=self.text_color, + hover_color=self.hover_color, + theme_color=self.theme_color, + special_color=self.special_color, + + thumbnails=video.thumbnails, + title=video.title, + channel=video.channel, + channel_url=video.channel_url, + url=video.url, + download_path=video.download_file_name, + file_size=video.total_bytes, + length=video.length, + ) + ) + + self.downloaded_videos[-1].remove_btn.configure(command = lambda video=self.downloaded_videos[-1]: self.remove_video(video)) + self.downloaded_videos[-1].pack(fill="x", padx=(20,0), pady=1) + + self.view_btn.configure(state="normal") + + + def set_theme(self): + super().set_theme() + for video in self.downloaded_videos: + video.set_new_theme(self.theme_color) + + + def remove_video(self, video: downloadingVideo): + self.video_count -= 1 + self.complete_count_label.configure(text=self.video_count) + video.kill() \ No newline at end of file diff --git a/widgets/downloadedVideo.py b/widgets/downloadedVideo.py new file mode 100644 index 0000000..9901ec2 --- /dev/null +++ b/widgets/downloadedVideo.py @@ -0,0 +1,89 @@ +from .Video import Video +import customtkinter as ctk +import os +from functions.getConvertedSize import getConvertedSize + +class downloadedVideo(Video): + def __init__(self, master, + border_width=None, + height=None, + width=None, + download_quality=None, + download_type=None, + + title=None, + channel=None, + thumbnails=(None,None), + loading_done = True, + url=None, + channel_url=None, + file_size=0, + download_path="", + length=None, + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None + ): + + self.download_path = download_path + self.file_size=file_size + self.download_quality = download_quality + self.download_type = download_type + super().__init__(master=master, border_width=border_width, theme_color=theme_color,hover_color=hover_color, + channel_url=channel_url,special_color=special_color,width=width, length=length, + fg_color=fg_color, bg_color=bg_color, height=height ,url=url, text_color=text_color, + thumbnails=thumbnails, title=title, channel=channel, loading_done=loading_done) + self.set_state() + + + def create_widgets(self): + super().create_widgets() + self.download_type_label = ctk.CTkLabel(master=self, + text=self.download_type + " : "+ self.download_quality, + fg_color=self.fg_color, + height=15, + font=("arial", 12, "bold"), + text_color=self.text_color, bg_color=self.fg_color) + self.file_size_label = ctk.CTkLabel(master=self, + text=getConvertedSize(self.file_size,2) , fg_color=self.fg_color, + font=("arial", 12, "normal"), + height=15, + text_color=self.text_color, bg_color=self.fg_color) + #πŸ“πŸ—€πŸ“‚πŸ–ΏπŸ— + self.download_path_btn = ctk.CTkButton(master=self, + text="πŸ“‚", + font=("arial", 30, "bold"), + cursor="hand2", + command=lambda:os.startfile("\\".join(self.download_path.split("\\")[0:-1])), + hover=False, + bg_color=self.fg_color, + fg_color=self.fg_color, height=15,width=30) + + + def set_theme(self): + super().set_theme() + self.download_path_btn.configure(text_color=self.theme_color) + + + def place_widgets(self): + super().place_widgets() + self.download_type_label.place(y=14) + self.file_size_label.place(y=40) + self.download_path_btn.place(y=12) + + + def configure_widget_sizes(self, e): + super().configure_widget_sizes(e) + self.download_type_label.place(x=self.winfo_width()-300) + self.file_size_label.place(x=self.winfo_width()-300) + self.download_path_btn.place(x=self.winfo_width()-150) + + + def set_state(self): + self.thumbnail_btn.configure(state="normal") + self.channel_label.configure(state="normal") + self.thumbnail_btn.configure(command=lambda:os.startfile(self.download_path)) \ No newline at end of file diff --git a/widgets/downloadingPlayList.py b/widgets/downloadingPlayList.py new file mode 100644 index 0000000..632a8c5 --- /dev/null +++ b/widgets/downloadingPlayList.py @@ -0,0 +1,222 @@ +import customtkinter as ctk +import tkinter as tk +import pytube +import time +import threading +from functions.getColor import getColor +from .playList import playList +from .downloadingVideo import downloadingVideo +import webbrowser + +class downloadingPlayList(playList): + def __init__(self, + master=None, + width=None, + height=None, + border_width=None, + + channel_url = None, + playlist_url=None, + playlist_title = "---------", + channel = "---------", + video_count = "?", + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None, + + videos: list[downloadingVideo] = None, + download_directory = "", + downloaded_callback_function = None + ): + + super().__init__(master=master, + height=height, + width=width, + border_width=border_width, + + channel_url = channel_url, + playlist_url=playlist_url, + playlist_title=playlist_title, + channel=channel, + video_count=video_count, + + bg_color=bg_color, + fg_color=fg_color, + text_color=text_color, + theme_color=theme_color, + hover_color=hover_color, + special_color=special_color, + ) + + self.downloaded_callback_function = downloaded_callback_function + self.videos = videos + self.download_directory = download_directory + self.downloading_videos = [] + threading.Thread(target=self.download_videos).start() + + def create_widgets(self): + super().create_widgets() + self.info_frame = ctk.CTkFrame(self, height=self.height-4, bg_color=self.fg_color, fg_color=self.fg_color) + + self.download_progress_bar = ctk.CTkProgressBar(master=self.info_frame, + bg_color=self.fg_color, + height=8) + + self.download_percentage_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + text_color=self.text_color) + + self.status_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + ) + + self.redownload_btn = ctk.CTkButton(self ,text="⟳", + width=15 ,height=15, + font=("arial", 20,"normal"), + command = self.redownload_video, + fg_color=self.fg_color, + bg_color=self.fg_color, + hover=False, + ) + + def set_theme(self): + super().set_theme() + self.redownload_btn.configure(text_color=self.theme_color) + for video in self.downloading_videos: + video.set_new_theme(self.theme_color) + + + def configure_widget_sizes(self, e): + self.info_frame.configure(width=self.winfo_width()/2 - 150) + + def place_widgets(self): + super().place_widgets() + + self.title_label.place(x=50, y=10, height=20, width=-50, relwidth=0.5) + self.channel_label.place(x=50, y=34, height=20, width=-50, relwidth=0.5) + self.url_label.place(x=50, y=54, height=20, width=-50, relwidth=0.5) + + self.info_frame.place(relx=0.5, y=2, x=50) + self.download_percentage_label.place(relx=0.5, anchor="n", y=12) + self.download_percentage_label.configure(height=20) + self.download_progress_bar.place(relwidth=1, y=40) + self.status_label.place(relx=0.775, anchor="n", y=55) + self.status_label.configure(height=20) + + + def redownload_video(self): + self.redownload_btn.place_forget() + for video in self.downloading_videos: + if video.download_failed: + video.redownload_video() + + + def download_videos(self): + for video in self.videos: + self.downloading_videos.append( + downloadingVideo( + master=self.playlist_item_frame, + height=70, + width=self.playlist_item_frame.winfo_width()-20, + border_width=1, + + fg_color=self.fg_color, + bg_color=self.bg_color, + theme_color=self.theme_color, + text_color=self.text_color, + hover_color=self.hover_color, + special_color=self.special_color, + + channel_url=video.channel_url, + url=video.url, + download_quality=video.download_quality, + download_type=video.download_type, + title=video.title, + channel=video.channel, + thumbnails=video.thumbnails, + video_stream_data=video.video_stream_data, + length=video.length, + + download_directory = self.download_directory, + downloaded_callback_function = None + ) + ) + self.downloading_videos[-1].pack(fill="x", padx=(20,0), pady=1) + self.downloading_videos[-1].remove_btn.configure(command=lambda video = self.downloading_videos[-1]: self.remove_video(video)) + self.view_btn.configure(state="normal") + threading.Thread(target=self.progress_track).start() + + + def progress_track(self): + downloading_videos = self.downloading_videos.copy() + while True: + download_complete = True + download_fails = False + downloaded_count = 0 + for video in downloading_videos: + if video not in self.downloading_videos: + downloading_videos.remove(video) + continue + if video.download_failed: + download_complete = False + download_fails = True + elif video.download_completed: + downloaded_count += 1 + #downloading_videos.remove(video) + else: + download_complete = False + + self.set_progress(downloaded_count/self.video_count) + + if not download_fails: + self.set_download_runnning() + if download_fails: + self.set_download_failed() + elif download_complete: + break + time.sleep(1) + self.set_downloading_done() + + + def set_progress(self, progress): + self.download_progress_bar.set(progress) + self.download_percentage_label.configure(text = str(round(progress*100, 2)) + " %") + + + def set_download_failed(self): + self.redownload_btn.place(relx=1, y=32, x=-80) + self.status_label.configure(text="Failed", text_color=self.special_color) + + + def set_downloading_done(self): + self.status_label.configure(text="Downloaded", text_color=self.theme_color) + self.downloaded_callback_function(self) + self.kill() + + + def set_download_runnning(self): + self.redownload_btn.place_forget() + self.status_label.configure(text="Downloading", text_color=self.theme_color) + + def remove_video(self, video): + self.downloading_videos.remove(video) + self.complete_count_label.configure(text=len(self.downloading_videos)) + self.video_count -= 1 + video.pack_forget() + video.kill() + + + def kill(self): + for video in self.downloading_videos: + video.kill() + super().kill() \ No newline at end of file diff --git a/widgets/downloadingVideo.py b/widgets/downloadingVideo.py new file mode 100644 index 0000000..8b9704b --- /dev/null +++ b/widgets/downloadingVideo.py @@ -0,0 +1,339 @@ +from .Video import Video +import customtkinter as ctk +import pytube +import threading +import time +from pytube import request as pytube_request +from functions.passIt import passIt +from functions.getConvertedSize import getConvertedSize +from functions.getValidFileName import getValidFileName +from functions.removeInvalidCharts import removeInvalidChars +from functions.createDownloadDirectory import createDownloadDirectory + + +class downloadingVideo(Video): + waiting_started = False + simultaneous_downloading = 0 + max_simultaneous_downloading = 1 + waiting_downloading_videos = [] + def waiting_for_downloading(): + def waiting(): + while True: + if downloadingVideo.max_simultaneous_downloading > downloadingVideo.simultaneous_downloading and len(downloadingVideo.waiting_downloading_videos) > 0: + try: + downloadingVideo.waiting_downloading_videos[0].start_download_video() + except: + pass + downloadingVideo.waiting_downloading_videos.pop(0) + time.sleep(1) + threading.Thread(target=waiting).start() + + def __init__(self, master, + border_width=None, + width=None, + height=None, + download_quality=None, + download_type=None, + title=None, + channel=None, + thumbnails=(None,None), + loading_done = True, + video_stream_data: pytube.StreamQuery = None, + url=None, + channel_url=None, + length = None, + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None, + + downloaded_callback_function=None, + download_directory = ""): + + if not downloadingVideo.waiting_started: + downloadingVideo.waiting_started = True + downloadingVideo.waiting_for_downloading() + + self.download_completed = False + self.download_failed = False + self.download_pause_req = False + self.downloaded_callback_function = downloaded_callback_function + self.download_quality = download_quality + self.download_type = download_type + self.video_stream_data = video_stream_data + self.download_directory = download_directory + super().__init__(master=master, border_width=border_width, theme_color=theme_color,hover_color=hover_color, + channel_url=channel_url, width=width, length=length, + fg_color=fg_color, bg_color=bg_color, height=height ,url=url, text_color=text_color, + thumbnails=thumbnails, title=title, channel=channel, loading_done=loading_done, special_color=special_color) + self.set_state() + threading.Thread(target=self.start_download_video).start() + + + def create_widgets(self): + super().create_widgets() + + self.info_frame = ctk.CTkFrame(self, height=self.height-4, bg_color=self.fg_color, fg_color=self.fg_color) + + self.download_progress_bar = ctk.CTkProgressBar(master=self.info_frame, + bg_color=self.fg_color, + height=8) + + self.download_progress_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + text_color=self.text_color) + + self.download_percentage_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + text_color=self.text_color) + + self.download_type_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "normal"), + bg_color=self.fg_color, + fg_color=self.fg_color, + text_color=self.text_color) + + self.net_speed_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "normal"), + bg_color=self.fg_color, + fg_color=self.fg_color, + text_color=self.text_color) + + self.status_label = ctk.CTkLabel(master=self.info_frame, + text="", + font=("arial", 12, "bold"), + bg_color=self.fg_color, + fg_color=self.fg_color, + ) + + self.redownload_btn = ctk.CTkButton(self ,text="⟳", + width=15 ,height=15, + font=("arial", 20,"normal"), + command = self.redownload_video, + fg_color=self.fg_color, + bg_color=self.fg_color, + hover=False, + ) + # ⏯ β†Ί ↻ ⏡ ⏸ β–· + self.pause_resume_button = ctk.CTkButton(self ,text="⏸", + width=15 ,height=15, + font=("arial", 20,"normal"), + command = self.pause_downloading, + fg_color=self.fg_color, + bg_color=self.fg_color, + hover=False, + ) + + + def place_widgets(self): + self.remove_btn.place(relx=1, x=-24, y=4) + self.thumbnail_btn.place(x=5, y=2, relheight=1, height=-4, width=int((self.height-4)/9*16)) + self.title_label.place(x=130, y=4, height=20, relwidth=0.5, width=-150) + self.channel_label.place(x=130, y=24, height=20, relwidth=0.5, width=-150) + self.url_label.place(x=130, y=44, height=20, relwidth=0.5, width=-150) + self.len_label.place(rely=1, y=-10, x=117, anchor="e") + #self.pause_resume_button.place(y=22, relx=1, x=-80) + self.info_frame.place(relx=0.5, y=2) + + self.download_progress_label.place(relx=0.25, anchor="n", y=4) + self.download_progress_label.configure(height=20) + self.download_percentage_label.place(relx=0.75, anchor="n", y=4) + self.download_percentage_label.configure(height=20) + self.download_progress_bar.place(relwidth=1, y=30) + self.download_type_label.place(relx=0.115, anchor="n", y=40) + self.download_type_label.configure(height=20) + self.net_speed_label.place(relx=0.445, anchor="n", y=40) + self.net_speed_label.configure(height=20) + self.status_label.place(relx=0.775, anchor="n", y=40) + self.status_label.configure(height=20) + + def set_theme(self): + super().set_theme() + self.download_progress_bar.configure(progress_color=self.theme_color) + if self.status_label.cget("text") != "Failed": + self.status_label.configure(text_color=self.theme_color) + self.redownload_btn.configure(text_color=self.theme_color) + self.pause_resume_button.configure(text_color=self.theme_color) + + def start_download_video(self): + self.download_completed = False + self.download_failed = False + if downloadingVideo.max_simultaneous_downloading > downloadingVideo.simultaneous_downloading : + downloadingVideo.simultaneous_downloading += 1 + try: + threading.Thread(target=self.download_video).start() + self.set_pause_btn() + self.pause_resume_button.place(y=22, relx=1, x=-80) + self.net_speed_label.configure(text="0.0 B/s") + self.download_progress_bar.set(0) + self.download_percentage_label.configure(text="0.0 %") + self.set_status("Downloading") + except: + downloadingVideo.simultaneous_downloading -= 1 + else: + self.set_waiting() + downloadingVideo.waiting_downloading_videos.append(self) + + def redownload_video(self): + self.redownload_btn.place_forget() + self.start_download_video() + + + def set_download_failed(self): + self.download_failed = True + if self.killed is not True: + downloadingVideo.simultaneous_downloading -= 1 + self.pause_resume_button.place_forget() + self.redownload_btn.place(y=22, relx=1, x=-80) + + def set_waiting(self): + self.pause_resume_button.place_forget() + self.download_progress_bar.set(0.5) + self.download_percentage_label.configure(text="") + self.net_speed_label.configure(text="") + self.download_progress_label.configure(text="") + self.download_type_label.configure(text="") + self.set_status("Waiting") + + def configure_widget_sizes(self, e): + self.info_frame.configure(width=self.winfo_width()/2-100) + + def set_state(self): + self.thumbnail_btn.configure(state="normal") + self.channel_label.configure(state="normal") + + def set_status(self, status): + if status=="Failed": + self.status_label.configure(text_color=self.special_color) + else: + self.status_label.configure(text_color=self.theme_color) + self.status_label.configure(text=status) + + + def download_video(self): + try: + createDownloadDirectory(self.download_directory) + except BufferError: + pass + + self.downloaded_bytes = 0 + self.download_file_name = self.download_directory + "\\" + removeInvalidChars(self.channel + " - " + self.title) + try: + self.download_type_label.configure(text=self.download_type + " : "+self.download_quality) + if self.download_type == "Video": + stream = self.video_stream_data.get_by_resolution(self.download_quality) + self.download_file_name += ".mp4" + else: + stream = self.video_stream_data.get_audio_only() + self.download_file_name += ".mp3" + self.total_bytes = stream.filesize + self.converted_total_bytes = getConvertedSize(self.total_bytes,2) + self.download_file_name = getValidFileName(self.download_file_name) + self.set_download_progress() + except Exception as error: + self.set_download_failed() + self.set_status("Failed") + + print(self.download_file_name) + try: + with open(self.download_file_name,"wb") as self.video_file : + stream = pytube_request.stream(stream.url) + count = 0 + while 1: + try: + time_s = time.time() + if self.download_pause_req: + if count == 0: + self.pause_resume_button.configure(command=self.resume_downloading) + self.set_status("Paused") + self.set_resume_btn() + count = 1 + time.sleep(0.3) + continue + self.pause_resume_button.configure(command=self.pause_downloading) + count = 0 + chunk = next(stream, None) + time_e = time.time() + if chunk: + self.video_file.write(chunk) + self.net_speed_label.configure(text=getConvertedSize(((self.downloaded_bytes + len(chunk)) - self.downloaded_bytes)/(time_e-time_s),1)+"/s") + self.downloaded_bytes += len(chunk) + self.set_download_progress() + else: + if self.downloaded_bytes == self.total_bytes: + self.set_download_complete() + break + else: + self.set_download_failed() + self.set_status("Failed") + break + except Exception as error: + self.set_download_failed() + self.set_status("Failed") + break + except Exception as error: + self.set_download_failed() + self.set_status("Failed") + + + def set_resume_btn(self): + # ⏯ β†Ί ↻ ⏡ ⏸ β–· + self.pause_resume_button.configure(text="β–·") + + + def set_pause_btn(self): + # ⏯ β†Ί ↻ ⏡ ⏸ β–· + self.pause_resume_button.configure(text="⏸") + + + def pause_downloading(self): + self.pause_resume_button.configure(command=passIt) + self.set_status("Pausing") + self.download_pause_req = True + + + def resume_downloading(self): + self.pause_resume_button.configure(command=passIt) + self.set_status("Downloading") + self.download_pause_req = False + self.set_pause_btn() + + + def set_download_progress(self): + percentage = self.downloaded_bytes/self.total_bytes + self.download_progress_bar.set(percentage) + self.download_percentage_label.configure(text=str(round(percentage*100,2))+" %") + self.download_progress_label.configure(text=f"{getConvertedSize(self.downloaded_bytes,2)} / {self.converted_total_bytes}") + + + def set_download_complete(self): + self.set_status("Downloaded") + self.download_completed = True + downloadingVideo.simultaneous_downloading -= 1 + try: + if self.downloaded_callback_function is not None: + self.downloaded_callback_function(self) + self.kill() + except Exception as error: + print(error) + + + def kill(self): + self.killed = True + if self in downloadingVideo.waiting_downloading_videos: + downloadingVideo.waiting_downloading_videos.remove(self) + elif self.download_completed is not True: + downloadingVideo.simultaneous_downloading -= 1 + super().kill() + diff --git a/widgets/playList.py b/widgets/playList.py new file mode 100644 index 0000000..f37b22e --- /dev/null +++ b/widgets/playList.py @@ -0,0 +1,214 @@ +import customtkinter as ctk +import tkinter as tk +import pytube +import time +import threading +from functions.getColor import getColor +import webbrowser + +class playList(ctk.CTkFrame): + def __init__(self, + master=None, + width=None, + height=None, + border_width=None, + + channel_url = None, + playlist_url=None, + playlist_title = "---------", + channel = "---------", + video_count = "?", + + bg_color=None, + fg_color=None, + text_color=None, + theme_color=None, + hover_color=None, + special_color=None, + ): + + super().__init__(master=master, fg_color=fg_color) + + self.height = height + self.width = width + self.border_width = border_width + + self.channel_url = channel_url + self.channel = channel + + self.playlist_url = playlist_url + + self.playlist_title =playlist_title + + self.fg_color = fg_color + self.bg_color = bg_color + self.theme = "-" + self.theme_color = theme_color + self.text_color = text_color + self.special_color = special_color + self.hover_color = hover_color + self.video_count = video_count + + self.create_widgets() + self.place_widgets() + self.set_playlist_data() + threading.Thread(target=self.RunThemeTracker).start() + + + def create_widgets(self): + self.playlist_info_widget = ctk.CTkFrame(master=self, border_width=self.border_width, + fg_color=self.fg_color, + bg_color=self.bg_color, + height=85) + + self.view_btn = ctk.CTkButton(master=self.playlist_info_widget, + font=('arial', 18, 'bold'), + text = ">", + width=1, + height=1, + bg_color=self.fg_color, + fg_color=self.fg_color, + hover=False, + command=self.view_videos, + state="disabled", + cursor="hand2", + ) + """ + self.thumbnail_btn = tk.Button(master=self.playlist_info_widget, + bd=0, + font=("arial", 14, "bold"), + bg=self.getColorBasedOnTheme(self.bg_color), + relief="sunken", + state="disabled", + cursor="hand2", + command=lambda:webbrowser.open(self.playlist_url) + ) + """ + self.title_label = tk.Label(master=self.playlist_info_widget, + anchor="w", + text = "Title : ", + font=('arial',10,'bold'), + bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color) + ) + + self.channel_label = tk.Button(master=self.playlist_info_widget, + font=('arial',9,'bold'), + anchor="w", + text = "Channel : ", + bd=0, + bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color), + command=lambda:webbrowser.open(self.channel_url), + relief="sunken", + state="disabled", + cursor="hand2", + ) + + self.url_label = tk.Label(master=self.playlist_info_widget ,anchor="w", + bg=self.getColorBasedOnTheme(self.fg_color), + text=self.playlist_url, + font=('arial',11,"italic underline"), + ) + + self.remove_btn = ctk.CTkButton(master=self.playlist_info_widget, + command=self.kill, + text="X", + font=("arial", 10, "bold"), + fg_color=self.fg_color, + bg_color=self.fg_color, + border_width=2, + border_color=self.special_color, + text_color=self.special_color, + width=12, height=10, + border_spacing=0, + hover=False, + ) + + self.complete_count_label = ctk.CTkLabel(master=self.playlist_info_widget, + width=15 ,height=15, + font=("arial", 13, "normal"), + fg_color=self.fg_color, + bg_color=self.fg_color, + justify="right") + + self.playlist_item_frame = ctk.CTkFrame(self, fg_color=self.fg_color) + + self.bind("", self.configure_widget_sizes) + + def getColorBasedOnTheme(self, color): + return getColor(color, self.theme) + + + def RunThemeTracker(self): + while True: + if ctk.get_appearance_mode() != self.theme: + self.theme = ctk.get_appearance_mode() + self.set_theme() + time.sleep(2) + + + def set_theme(self): + self.configure(border_color=self.theme_color) + + self.playlist_info_widget.configure(border_color=self.theme_color) + #self.thumbnail_btn.configure(bg=self.getColorBasedOnTheme(self.fg_color), + # fg=self.getColorBasedOnTheme(self.text_color), + # disabledforeground=self.getColorBasedOnTheme(self.text_color), + # activebackground=self.getColorBasedOnTheme(self.fg_color)) + self.view_btn.configure(text_color=self.theme_color) + + self.title_label.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color)) + + self.url_label.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.theme_color) + + self.channel_label.configure(bg=self.getColorBasedOnTheme(self.fg_color), + fg=self.getColorBasedOnTheme(self.text_color), + activebackground=self.getColorBasedOnTheme(self.fg_color), + activeforeground=self.theme_color,) + + self.complete_count_label.configure(text_color=self.theme_color) + + + def place_widgets(self): + self.playlist_info_widget.pack(fill="x") + self.view_btn.place(y=55, x=10) + #self.thumbnail_btn.place(x=25, y=2, relheight=1, height=-4, width=int((self.height-4)/9*16)) + self.title_label.place(x=50, y=10, height=20, width=-420, relwidth=1) + self.channel_label.place(x=50, y=34, height=20, width=-420, relwidth=1) + self.url_label.place(x=50, y=54, height=20, width=-420, relwidth=1) + self.complete_count_label.place(relx=1, x=-60, rely=1, y=-25) + self.remove_btn.place(relx=1, x=-23, y=4) + + + def configure_widget_sizes(self, e): + pass + + + def hide_videos(self): + self.view_btn.configure(command=self.view_videos, text=">", font=('arial', 18, 'bold')) + self.playlist_item_frame.pack_forget() + + + def view_videos(self): + self.view_btn.configure(command=self.hide_videos, text="V", font=('arial', 13, 'bold')) + self.playlist_item_frame.pack(padx=10, fill="x", pady=2) + + + def set_playlist_data(self): + self.complete_count_label.configure(text=f"{self.video_count}") + self.title_label.configure(text="Title : "+self.playlist_title) + self.channel_label.configure(text="Channel : "+self.channel) + self.url_label.configure(text=self.playlist_url) + + + def kill(self): + self.pack_forget() + self.destroy() + + + def set_new_theme(self, new_theme_color): + self.theme_color = new_theme_color + self.set_theme() \ No newline at end of file diff --git a/widgets/settingPanel.py b/widgets/settingPanel.py new file mode 100644 index 0000000..3547677 --- /dev/null +++ b/widgets/settingPanel.py @@ -0,0 +1,146 @@ +import customtkinter as ctk +from .colorBtn import ColorBtn +from functions.saveSettings import saveSettings +from functions.getThemeSettings import getThemeSettings +from functions.getGeneralSettings import getGeneralSettings +from functions.getConvertedPath import getConvertedPath +import os + +class settingPanel(ctk.CTkFrame): + + def __init__(self,master=None, + fg_color=None, + bg_color=None, + text_color=None, + theme_color_change_call_back=None, + directory_change_call_back=None): + + self.theme_color_change_call_back = theme_color_change_call_back + self.theme_settings_file = "./settings/theme.json" + self.theme_settings = getThemeSettings() + + self.directory_change_call_back = directory_change_call_back + self.general_settings_file = "./settings/general.json" + self.general_settings = getGeneralSettings() + + + + super().__init__(master=master, fg_color=fg_color, bg_color=bg_color) + + + self.color_scheme_frame = ctk.CTkFrame(master=self, fg_color=fg_color, bg_color=bg_color, + border_width=2) + self.color_scheme_frame.place(x=10, y=50,) + + self.color_scheme_label = ctk.CTkLabel(master=self.color_scheme_frame, fg_color=fg_color, bg_color=bg_color, + font=("arial", 14, "bold"), text="Color Scheme : ", height=20) + self.color_scheme_label.grid(row=0, columnspan=len(self.theme_settings["default_theme_colors"])+2, pady=10) + + ctk.CTkLabel(master=self.color_scheme_frame, fg_color=fg_color, bg_color=bg_color, text="").grid(row=1, column=0, padx=4) + col_ = 1 + for app_theme_color in self.theme_settings["default_theme_colors"]: + btn = ColorBtn(master=self.color_scheme_frame, width=30, height=30, hover=False, + fg_color=app_theme_color, bg_color=fg_color, corner_radius=10, text='') + btn.configure(command=lambda btn_=btn,color=app_theme_color,:self.save_theme_color(btn_, color)) + btn.grid(row=1, column=col_, padx=4, pady=10) + if app_theme_color == self.theme_settings["app_theme_color"]: + btn.react("E") + btn.set_clicked() + btn.set_unclicked_before_btn() + col_ += 1 + ctk.CTkLabel(master=self.color_scheme_frame, fg_color=fg_color, bg_color=bg_color, text="").grid(row=1, column=col_, padx=4) + + + + self.theme_mode_frame = ctk.CTkFrame(master=self, fg_color=fg_color, bg_color=bg_color, + border_width=2) + self.theme_mode_frame.place(x=420, y=50) + self.color_mode_label = ctk.CTkLabel(master=self.theme_mode_frame, fg_color=fg_color, bg_color=bg_color, + font=("arial", 14, "bold"), text="Theme : ", height=20) + self.color_mode_label.grid(row=0, columnspan=5, pady=10) + ctk.CTkLabel(master=self.theme_mode_frame, fg_color=fg_color, bg_color=bg_color, text="").grid(row=1, column=0, padx=4) + col_ = 1 + for theme_mode in ["System", "Dark", "Light"]: + btn = ColorBtn(master=self.theme_mode_frame, width=60, height=30, hover=False, border_width=2, text_color=text_color, + fg_color=fg_color, bg_color=bg_color, corner_radius=6, text=theme_mode, font=("arial", 12, "bold")) + btn.configure(command=lambda btn_=btn,color=theme_mode,:self.save_theme_mode(btn_, color)) + btn.grid(row=1, column=col_, padx=4, pady=10) + if theme_mode == self.theme_settings["app_theme_mode"]: + btn.react("E") + btn.set_clicked() + btn.set_unclicked_before_btn() + col_ += 1 + ctk.CTkLabel(master=self.theme_mode_frame, fg_color=fg_color, bg_color=bg_color, text="").grid(row=1, column=4, padx=4) + + + self.download_path_frame = ctk.CTkFrame(master=self, fg_color=fg_color, bg_color=bg_color, + border_width=2) + self.download_path_frame.place(x=10, y=200) + + self.download_path_label = ctk.CTkLabel(master=self.download_path_frame, fg_color=fg_color, bg_color=bg_color, width=200, + font=("arial", 14, "bold"), text="Download Directory : ", height=40) + self.download_path_label.grid(row=0, column=0, pady=10, padx=10) + + self.download_path_btn = ctk.CTkButton(master=self.download_path_frame, + text="πŸ“‚", + font=("arial", 25, "bold"), + cursor="hand2", + hover=False, + command=self.get_download_directory, + bg_color=fg_color, + fg_color=fg_color, height=40,width=40) + self.download_path_btn.grid(row=0, column=1, pady=10, padx=10) + self.download_path_entry = ctk.CTkEntry(master=self.download_path_frame, fg_color=fg_color, bg_color=bg_color, width=610, + font=("arial", 13, "underline"), height=40) + self.download_path_entry.insert(0, self.general_settings["download_directory"] if self.general_settings["download_directory"]!=False else f"C:\\Users\\{os.getlogin()}\\Downloads\\PyTube Downloader\\") + self.download_path_entry.grid(row=1, columnspan=2, pady=10, padx=10) + self.configure_theme_color() + + + def save_theme_color(self,btn: ColorBtn, theme_color): + if self.theme_settings["app_theme_color"] != theme_color: + self.theme_settings["app_theme_color"] = theme_color + btn.set_clicked() + btn.set_unclicked_before_btn() + self.theme_settings["app_theme_color"] = theme_color + saveSettings(self.theme_settings_file,self.theme_settings) + self.configure_theme_color() + self.theme_color_change_call_back(theme_color) + + + def save_theme_mode(self,btn: ColorBtn, theme): + btn.set_clicked() + btn.set_unclicked_before_btn() + self.theme_settings["app_theme_mode"] = theme + saveSettings(self.theme_settings_file,self.theme_settings) + self.configure_theme_color() + ctk.set_appearance_mode(theme) + + + def configure_theme_color(self): + self.color_scheme_frame.configure(border_color=self.theme_settings["app_theme_color"]) + self.color_scheme_label.configure(text_color=self.theme_settings["app_theme_color"]) + + self.theme_mode_frame.configure(border_color=self.theme_settings["app_theme_color"]) + self.color_mode_label.configure(text_color=self.theme_settings["app_theme_color"]) + + for btn in self.theme_mode_frame.winfo_children(): + if type(btn) == ColorBtn: + btn.configure(border_color=self.theme_settings["app_theme_color"], text_color=self.theme_settings["app_theme_color"]) + + self.download_path_frame.configure(border_color=self.theme_settings["app_theme_color"]) + self.download_path_label.configure(text_color=self.theme_settings["app_theme_color"]) + self.download_path_btn.configure(text_color=self.theme_settings["app_theme_color"]) + self.download_path_entry.configure(border_color=self.theme_settings["app_theme_color"], text_color=self.theme_settings["app_theme_color"]) + + + def get_download_directory(self): + directory = ctk.filedialog.askdirectory() + if directory != "" and self.general_settings["download_directory"] != directory: + directory = getConvertedPath(directory) + self.general_settings["download_directory"] = directory + saveSettings(self.general_settings_file,self.general_settings) + self.download_path_entry.delete(0, "end") + self.download_path_entry.insert(0, directory) + self.directory_change_call_back(directory) + \ No newline at end of file