Skip to content

Commit abf5ff5

Browse files
committed
feat(gui): remodel all the UI, add some control flags over the downloading process
1 parent c89434b commit abf5ff5

8 files changed

Lines changed: 674 additions & 498 deletions

File tree

spankbang_dl/gui.py

Lines changed: 0 additions & 411 deletions
This file was deleted.

spankbang_dl/gui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .main import VideoDownloaderUI

spankbang_dl/gui/commands.py

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import time
2+
import tkinter as tk
3+
from datetime import date
4+
from pathlib import Path
5+
from threading import Thread
6+
from tkinter import messagebox, ttk
7+
from typing import TYPE_CHECKING, Optional
8+
from urllib.parse import ParseResult, urlparse
9+
10+
import requests
11+
from core_helpers.logs import logger
12+
13+
if TYPE_CHECKING:
14+
from . import VideoDownloaderUI
15+
16+
from ..consts import AUTHOR, MB
17+
from ..consts import __version__ as VERSION
18+
from ..scraper import extract_video_info, fetch_web_content
19+
from ..translations import get_translations
20+
from ..utils import format_file_size
21+
22+
23+
def _create_pop_up_window(gui: "VideoDownloaderUI", title: str, message: str) -> None:
24+
"""
25+
Create a pop-up window with the given title and message.
26+
27+
Args:
28+
gui (VideoDownloaderUI): The GUI instance.
29+
title (str): The title of the pop-up window.
30+
message (str): The message to display in the pop-up window.
31+
"""
32+
# Create a top-level overlay window rather than using system messagebox
33+
help_win = tk.Toplevel(gui.win)
34+
help_win.title(title)
35+
help_win.geometry("450x250")
36+
help_win.minsize(650, 200)
37+
38+
# Configure the inner frame wrapper to scale cleanly on resize mechanics
39+
frame = ttk.Frame(help_win, padding="15")
40+
frame.pack(expand=True, fill="both")
41+
frame.columnconfigure(0, weight=1)
42+
frame.rowconfigure(0, weight=1)
43+
44+
# Message widget scales text wrapping seamlessly when width parameters shift
45+
msg = tk.Message(frame, text=message, justify="left", aspect=200, width=600)
46+
msg.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
47+
48+
close_btn = ttk.Button(frame, text="OK", command=help_win.destroy)
49+
close_btn.grid(row=1, column=0, pady=5)
50+
51+
52+
def start_download_thread(gui: VideoDownloaderUI) -> None:
53+
"""
54+
Callback function to handle the download button click event.
55+
56+
Retrieves the video URL from the input field, fetches the web content,
57+
extracts video information, and downloads the video while displaying
58+
progress in the GUI.
59+
"""
60+
logger.debug("Download button clicked")
61+
62+
if gui.downloading:
63+
gui.log_message(gui.translations["already_downloading_message"])
64+
logger.warning("Download already in progress!")
65+
return
66+
67+
url: str = gui.a_url.get()
68+
if not url:
69+
messagebox.showwarning(
70+
gui.translations["warning_title"],
71+
gui.translations["empty_url_message"],
72+
)
73+
logger.warning("No URL provided for download!")
74+
return
75+
76+
gui.log_message(f"Starting download from: {url}")
77+
78+
r: requests.Response | None = handle_web_content_fetch(gui, url)
79+
if r is None:
80+
return
81+
82+
video: str | None = extract_and_display_video_info(gui, url, r.text)
83+
if not video:
84+
return
85+
86+
resp: requests.Response | None = handle_web_content_fetch(gui, video, stream=True)
87+
if resp is None:
88+
return
89+
90+
try:
91+
thread = Thread(
92+
target=download_video_and_display_progress,
93+
args=(gui, resp),
94+
daemon=True,
95+
)
96+
thread.start()
97+
except Exception as e:
98+
gui.log_message(gui.translations["download_failed_message"].format(str(e)))
99+
logger.error("Error downloading video from %s", url, exc_info=True)
100+
gui.downloading = False
101+
raise
102+
103+
104+
def handle_web_content_fetch(
105+
gui: VideoDownloaderUI, url: str, stream: bool = False
106+
) -> Optional[requests.Response]:
107+
"""
108+
Fetch web content from the given URL and handle exceptions.
109+
110+
Args:
111+
url (str): The URL to fetch web content from.
112+
stream (bool): Whether to stream the response content. Defaults to False.
113+
114+
Returns:
115+
requests.Response | None: The response object if successful, None
116+
otherwise.
117+
"""
118+
logger.debug("Fetching and handling web content for URL: %s", url)
119+
120+
try:
121+
response: requests.Response = fetch_web_content(url, stream=stream)
122+
logger.info(
123+
"Web content fetched successfully for %s: %s", url, response.status_code
124+
)
125+
return response
126+
except requests.exceptions.RequestException as e:
127+
if e.response is not None:
128+
error_message = e.response.status_code
129+
else:
130+
error_message = str(e)
131+
gui.log_message(gui.translations["failure_message"].format(error_message))
132+
logger.error(
133+
"Failed to fetch content from %s: %s", url, error_message, exc_info=True
134+
)
135+
return None
136+
137+
138+
def extract_and_display_video_info(
139+
gui: VideoDownloaderUI, url: str, html: str
140+
) -> Optional[str]:
141+
"""
142+
Extract and display video information.
143+
144+
Args:
145+
url (str): The URL of the video page.
146+
html (str): The HTML content of the page.
147+
148+
Returns:
149+
str: The video source URL if found, otherwise an empty string.
150+
"""
151+
logger.debug("Extracting and displaying video information for URL: %s", url)
152+
153+
try:
154+
title, video = extract_video_info(html)
155+
except ValueError as e:
156+
gui.log_message(gui.translations["video_not_found"].format(str(e)))
157+
logger.error("Video information not found in the web content", exc_info=True)
158+
return ""
159+
160+
# Display the translated messages and labels
161+
gui.log_message(gui.translations["download_video_message"].format(url))
162+
gui.log_message(gui.translations["video_title"].format(title))
163+
gui.log_message(gui.translations["video_url"].format(video))
164+
logger.debug("Extracted video info: Title: %r, URL: %s", title, video)
165+
166+
return video
167+
168+
169+
def download_video_and_display_progress(
170+
gui: VideoDownloaderUI, resp: requests.Response
171+
) -> None:
172+
"""gui
173+
Download the video and display the download progress.
174+
175+
Args:
176+
resp (requests.Response): The response object containing the video data.
177+
"""
178+
logger.debug("Starting video download for URL: %s", resp.url)
179+
180+
content_size = int(resp.headers["Content-Length"])
181+
parsed_url: ParseResult = urlparse(resp.url)
182+
if not parsed_url.path:
183+
gui.win.after(
184+
0,
185+
lambda: gui.log_message(
186+
gui.translations["invalid_url_message"].format(resp.url)
187+
),
188+
)
189+
logger.error("Invalid URL: %s", resp.url)
190+
return
191+
192+
content_size_text: str = format_file_size(content_size)
193+
gui.win.after(
194+
0,
195+
lambda: gui.log_message(
196+
gui.translations["total_size_message"].format(content_size_text)
197+
),
198+
)
199+
logger.debug("Total size of the video: %s", content_size_text)
200+
201+
# Extract the media extension from the URL path
202+
file_extension: str = parsed_url.path.split(".")[-1]
203+
file_name: str = parsed_url.path.split("/")[-1] or f"video.{file_extension}"
204+
# Create the file path with the correct extension
205+
file_path: Path = Path(file_name).with_suffix(f".{file_extension}")
206+
207+
# Set up progress bar
208+
gui.progress_bar["maximum"] = content_size
209+
gui.progress_bar["value"] = 0
210+
gui.progress_percent.set("0%")
211+
# Reset flags
212+
gui.cancel_download = False
213+
gui.downloading = True
214+
# Ensure initial draw
215+
gui.win.update_idletasks()
216+
217+
downloaded = 0
218+
start_time: float = time.time()
219+
220+
with open(file_path, mode="wb") as f:
221+
222+
def update_progress() -> None:
223+
gui.progress_bar["value"] = downloaded
224+
gui.progress_percent.set(f"{percent:.1f}%")
225+
226+
for chunk in resp.iter_content(MB):
227+
if gui.cancel_download:
228+
# Reset progress bar, label and flags
229+
gui.progress_bar["value"] = 0
230+
gui.progress_percent.set("0%")
231+
gui.cancel_download = False # reset cancel flag
232+
gui.downloading = False
233+
logger.info("Download cancelled by user.")
234+
return
235+
236+
if not chunk:
237+
continue
238+
239+
f.write(chunk)
240+
downloaded += len(chunk)
241+
242+
percent: float = (downloaded / content_size) * 100
243+
elapsed: float = time.time() - start_time
244+
rate: float = downloaded / elapsed if elapsed > 0 else 0
245+
remaining: float = (content_size - downloaded) / rate if rate > 0 else 0
246+
eta: str = time.strftime("%M:%S", time.gmtime(remaining))
247+
248+
gui.win.after(0, update_progress)
249+
250+
gui.downloading = False
251+
gui.win.after(
252+
0, lambda: gui.log_message(gui.translations["download_complete_message"])
253+
)
254+
gui.win.after(
255+
0, lambda: gui.log_message(gui.translations["video_saved"].format(file_path))
256+
)
257+
logger.info("Download complete for %r", file_path)
258+
259+
260+
def quit_app(gui: VideoDownloaderUI) -> None:
261+
"""
262+
Callback function to handle the quit button click event.
263+
264+
Closes the main application window and exits the program.
265+
"""
266+
logger.debug("Quit button clicked")
267+
268+
gui.win.quit()
269+
gui.win.destroy()
270+
logger.debug("Destroyed the main window")
271+
272+
273+
def show_about(gui: VideoDownloaderUI) -> None:
274+
"""
275+
Callback function to handle the about button click event.
276+
277+
Displays an informational message box with author, version, and current
278+
date.
279+
"""
280+
logger.debug("About button clicked")
281+
282+
today: str = date.today().strftime("%d/%m/%Y")
283+
about_message: str = gui.translations["about_message"].format(
284+
AUTHOR, VERSION, today
285+
)
286+
messagebox.showinfo(gui.translations["about_button"], about_message)
287+
288+
289+
def show_help(gui: VideoDownloaderUI) -> None:
290+
"""
291+
Callback function to handle the help button click event.
292+
293+
Displays an informational message box with usage instructions or help
294+
content.
295+
"""
296+
logger.debug("Help button clicked")
297+
help_message: str = gui.translations["help_message"]
298+
messagebox.showinfo(gui.translations["help_button"], help_message)
299+
300+
301+
def cancel_download(gui: VideoDownloaderUI) -> None:
302+
"""Sets the cancel flag to True."""
303+
logger.debug("Cancel download button clicked")
304+
if not gui.downloading:
305+
gui.log_message(gui.translations["no_download_message"])
306+
logger.warning("No download in progress to cancel.")
307+
return
308+
309+
gui.cancel_download = True
310+
gui.log_message(gui.translations["cancelled_message"])
311+
312+
313+
def change_language(gui: VideoDownloaderUI, lang_code: str) -> None:
314+
if gui.current_language == lang_code:
315+
logger.debug("Language change requested but already set to %s", lang_code)
316+
gui.log_message(gui.translations["language_already_set"].format(lang_code))
317+
return
318+
319+
gui.translations = get_translations(lang_code)
320+
gui.current_language = lang_code
321+
322+
# Update the OS Window title
323+
gui.win.title(gui.translations["window_title"])
324+
325+
# Update buttons
326+
gui.help_btn.config(text=gui.translations["help_button"])
327+
gui.about_btn.config(text=gui.translations["about_button"])
328+
329+
# Update other componentes
330+
gui.url_frame.config(text=gui.translations["enter_url_label"])
331+
gui.log_frame.config(text=gui.translations["log_frame_label"])
332+
gui.lang_label.config(text=gui.translations["language"])
333+
334+
gui.log_message(gui.translations["language_changed_message"].format(lang_code))

0 commit comments

Comments
 (0)