Skip to content

Commit 719ca40

Browse files
committed
feat: add download cancel to UI, also fetch video info in separate thread
1 parent a071af9 commit 719ca40

File tree

1 file changed

+78
-20
lines changed
  • async_rutube_downloader

1 file changed

+78
-20
lines changed

async_rutube_downloader/ui.py

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
import tkinter
33
from asyncio import AbstractEventLoop, new_event_loop
44
from concurrent.futures import ThreadPoolExecutor
5+
from enum import Enum, auto
56
from pathlib import Path
67
from queue import Queue
78
from tkinter import filedialog, messagebox
9+
from typing import Final
810

911
import customtkinter as ctk
1012

1113
from async_rutube_downloader.downloader import Downloader
12-
from async_rutube_downloader.settings import _
14+
from async_rutube_downloader.settings import DOWNLOAD_CANCELED, _
1315
from async_rutube_downloader.utils.create_session import create_aiohttp_session
1416
from async_rutube_downloader.utils.exceptions import (
1517
APIResponseError,
@@ -21,15 +23,26 @@
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

3548
class 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

287345
if __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

Comments
 (0)