22import tkinter
33from asyncio import AbstractEventLoop , new_event_loop
44from concurrent .futures import ThreadPoolExecutor
5+ from enum import Enum , auto
56from pathlib import Path
67from queue import Queue
78from tkinter import filedialog , messagebox
9+ from typing import Final
810
911import customtkinter as ctk
1012
1113from async_rutube_downloader .downloader import Downloader
12- from async_rutube_downloader .settings import _
14+ from async_rutube_downloader .settings import DOWNLOAD_CANCELED , _
1315from async_rutube_downloader .utils .create_session import create_aiohttp_session
1416from async_rutube_downloader .utils .exceptions import (
1517 APIResponseError ,
2123 UploadDirectoryNotSelectedError ,
2224)
2325
24- INVALID_URL_MSG = "The provided URL is invalid. Please check and try again."
25- API_RESPONSE_ERROR_MSG = _ (
26+ INVALID_URL_MSG : Final [str ] = (
27+ "The provided URL is invalid. Please check and try again."
28+ )
29+ API_RESPONSE_ERROR_MSG : Final [str ] = _ (
2630 "Failed to fetch video data. "
2731 "The URL might be incorrect, or there may be a connection issue."
2832)
29- SEGMENT_DOWNLOAD_ERROR_MSG = _ (
33+ SEGMENT_DOWNLOAD_ERROR_MSG : Final [ str ] = _ (
3034 "A network issue occurred while downloading a video "
3135 "segment. Please check your internet connection and retry."
3236)
37+ ERROR_COLOR : Final [str ] = "red"
38+ DOWNLOAD : Final [str ] = _ ("Download" )
39+ CANCEL_DOWNLOAD : Final [str ] = _ ("Cancel download" )
40+
41+
42+ class State (Enum ):
43+ cancel = auto ()
44+ disable = auto ()
45+ normal = auto ()
3346
3447
3548class DownloaderUI (ctk .CTk ):
@@ -83,12 +96,12 @@ def __init__(
8396 # Get video info
8497 self ._fetch_result_label = ctk .CTkLabel (self , text = "" )
8598 self ._fetch_result_label .grid (column = 0 , row = 2 , padx = 10 , pady = 10 )
86-
99+ # Get video info button
87100 self ._video_info_button = ctk .CTkButton (
88101 self ,
89102 text = _ ("Get Video Info" ),
90103 command = self ._run_fetch_concurrently ,
91- state = "disabled" ,
104+ state = tkinter . DISABLED ,
92105 )
93106 self ._video_info_button .grid (column = 1 , row = 1 , padx = 10 , pady = 10 )
94107
@@ -107,7 +120,7 @@ def __init__(
107120 self ,
108121 text = _ ("Download" ),
109122 command = self .start_download ,
110- state = "disabled" ,
123+ state = tkinter . DISABLED ,
111124 )
112125 self ._download_button .grid (column = 1 , row = 3 , padx = 10 , pady = 10 )
113126
@@ -134,13 +147,16 @@ def _update_bar(self, progress_bar_value: int) -> None:
134147 """Update the progress bar.
135148 Call only from main thread."""
136149 if progress_bar_value == 100 and self ._download :
137- self ._progress_bar . set ( 1 )
138- messagebox .showinfo (
150+ self .__thread_pool . submit (
151+ messagebox .showinfo ,
139152 _ ("Download Complete" ),
140153 _ ("Download Complete" ),
141- ) # TODO: A "Download Complete" alert is shown
154+ )
155+ # TODO: A "Download Complete" alert is shown
142156 # a little bit before the last downloaded chunk is actually saved.
143157 self ._download = None
158+ self ._progress_bar .set (1 )
159+ self .change_download_button_state (State .normal )
144160 else :
145161 self ._progress_bar .set (
146162 progress_bar_value / self ._progress_bar_divider
@@ -173,31 +189,32 @@ def fetch_video_info(self, *args) -> None:
173189 this object goes here.
174190 """
175191 self ._fetch_result_label .configure (text = "" )
192+ self .change_download_button_state (State .normal )
176193 if not self ._url_entry .get ():
177- messagebox .showerror (
178- "Error" , _ ("Please enter a video URL before proceeding." )
194+ self .__thread_pool .submit (
195+ messagebox .showerror ,
196+ _ ("Error" ),
197+ _ ("Please enter a video URL before proceeding." ),
179198 )
180199 elif self ._upload_directory and self ._url_entry .get ():
181200 try :
182- self .__fetch_video_info ()
183- self .__fill_qualities ()
184- self .__fill_title ()
201+ self .__thread_pool .submit (self ._fetch_video_info )
185202 except (InvalidURLError , KeyError ):
186203 self ._fetch_result_label .configure (
187204 text = INVALID_URL_MSG + self .__error_counter ,
188- text_color = "red" ,
205+ text_color = ERROR_COLOR ,
189206 )
190207 self .__increase_error_counter ()
191208 except (APIResponseError , MasterPlaylistInitializationError ):
192209 self ._fetch_result_label .configure (
193210 text = API_RESPONSE_ERROR_MSG + self .__error_counter ,
194- text_color = "red" ,
211+ text_color = ERROR_COLOR ,
195212 )
196213 self .__increase_error_counter ()
197214 except SegmentDownloadError :
198215 self ._fetch_result_label .configure (
199216 text = SEGMENT_DOWNLOAD_ERROR_MSG + self .__error_counter ,
200- text_color = "red" ,
217+ text_color = ERROR_COLOR ,
201218 )
202219 self .__increase_error_counter ()
203220
@@ -207,6 +224,11 @@ def __increase_error_counter(self) -> None:
207224 else :
208225 self .__error_counter = "1"
209226
227+ def _fetch_video_info (self ) -> None :
228+ self .__fetch_video_info ()
229+ self .__fill_qualities ()
230+ self .__fill_title ()
231+
210232 def __fetch_video_info (self ) -> None :
211233 """
212234 1. Creates a Downloader object in the main thread.
@@ -251,8 +273,45 @@ def start_download(self) -> None:
251273 asyncio .run_coroutine_threadsafe (
252274 self ._download .download_video (), self ._loop
253275 )
276+ self .change_download_button_state (State .cancel )
254277 self .after (self ._refresh_ms , self ._poll_queue )
255278
279+ def change_download_button_state (self , state : State ) -> None :
280+ states = {
281+ State .cancel : {
282+ "text" : CANCEL_DOWNLOAD ,
283+ "command" : self .cancel_download ,
284+ "state" : tkinter .NORMAL ,
285+ },
286+ State .disable : {
287+ "text" : DOWNLOAD ,
288+ "command" : None ,
289+ "state" : tkinter .DISABLED ,
290+ },
291+ State .normal : {
292+ "text" : DOWNLOAD ,
293+ "command" : self .start_download ,
294+ "state" : tkinter .NORMAL ,
295+ },
296+ }
297+ self ._download_button .configure (** states [state ])
298+
299+ def cancel_download (self ) -> None :
300+ assert self ._download
301+ self ._download .interrupt_download ()
302+ self .__thread_pool .submit (
303+ messagebox .showinfo , _ ("Info" ), DOWNLOAD_CANCELED
304+ )
305+ self .change_download_button_state (State .disable )
306+ self .clean_ui ()
307+
308+ def clean_ui (self ) -> None :
309+ """Clean up: title, qualities,progress,bar"""
310+ self ._video_title_dynamic .configure (text = "" )
311+ self ._progress_bar .set (0 )
312+ self ._dropdown .configure (values = [], state = tkinter .DISABLED )
313+ self ._dropdown .set ("" )
314+
256315 def __set_quality (self ) -> None :
257316 """
258317 Set the quality of the video to download.
@@ -280,14 +339,13 @@ def select_folder(self) -> None:
280339 self ._upload_directory = Path (directory )
281340 if not self ._upload_directory .is_dir ():
282341 raise ValueError ("Selected folder does not exist" )
283- self ._download_button .configure (state = "normal" )
284342 self ._video_info_button .configure (state = "normal" )
285343
286344
287345if __name__ == "__main__" :
288346 app = DownloaderUI ()
289347 app ._video_title_dynamic .configure (
290348 text = "don't run this file directly, use run_ui.py" ,
291- text_color = "red" ,
349+ text_color = ERROR_COLOR ,
292350 )
293351 app .mainloop ()
0 commit comments