diff --git a/backend.py b/backend.py index 2d18f646..555bcd7e 100644 --- a/backend.py +++ b/backend.py @@ -144,16 +144,9 @@ def _download_book_with_cancellation(book_id: str, cancel_flag: Event) -> Option if cancel_flag.is_set(): logger.info(f"Download cancelled before book manager call: {book_id}") return None - - # Update progress periodically during download - progress_thread = threading.Thread( - target=_update_download_progress, - args=(book_id, cancel_flag), - daemon=True - ) - progress_thread.start() - - success = book_manager.download_book(book_info, book_path) + + progress_callback = lambda progress: update_download_progress(book_id, progress) + success = book_manager.download_book(book_info, book_path, progress_callback, cancel_flag) # Stop progress updates cancel_flag.wait(0.1) # Brief pause for progress thread cleanup @@ -209,14 +202,9 @@ def _download_book_with_cancellation(book_id: str, cancel_flag: Event) -> Option logger.error_trace(f"Error downloading book: {e}") return None -def _update_download_progress(book_id: str, cancel_flag: Event) -> None: - """Update download progress periodically.""" - progress = 0.0 - while not cancel_flag.is_set() and progress < 100.0: - # Simulate progress (in real implementation, this would get actual progress) - progress = min(100.0, progress + 10.0) - book_queue.update_progress(book_id, progress) - time.sleep(DOWNLOAD_PROGRESS_UPDATE_INTERVAL) +def update_download_progress(book_id: str, progress: float) -> None: + """Update download progress.""" + book_queue.update_progress(book_id, progress) def cancel_download(book_id: str) -> bool: """Cancel a download. diff --git a/book_manager.py b/book_manager.py index b046a817..aef28851 100644 --- a/book_manager.py +++ b/book_manager.py @@ -3,7 +3,8 @@ import time, json, re from pathlib import Path from urllib.parse import quote -from typing import List, Optional, Dict, Union +from typing import List, Optional, Dict, Union, Callable +from threading import Event from bs4 import BeautifulSoup, Tag, NavigableString, ResultSet import downloader @@ -11,10 +12,10 @@ from logger import setup_logger from config import SUPPORTED_FORMATS, BOOK_LANGUAGE, AA_BASE_URL from env import AA_DONATOR_KEY, USE_CF_BYPASS, PRIORITIZE_WELIB from models import BookInfo, SearchFilters - logger = setup_logger(__name__) + def search_books(query: str, filters: SearchFilters) -> List[BookInfo]: """Search for books matching the query. @@ -198,7 +199,7 @@ def _parse_book_info_page(soup: BeautifulSoup, book_id: str) -> BookInfo: ): libgen_url = url["href"] # TODO : Temporary fix ? Maybe get URLs from https://open-slum.org/ ? - libgen_url = libgen_url = re.sub(r'libgen\.(\w+)', 'libgen.gs', url["href"]) + libgen_url = libgen_url = re.sub(r'libgen\.(\w+)', 'libgen.la', url["href"]) external_urls_libgen.add(libgen_url) elif url.text.strip().lower().startswith("z-lib"): if ".onion/" not in url["href"]: @@ -300,7 +301,7 @@ def _extract_book_metadata( } -def download_book(book_info: BookInfo, book_path: Path) -> bool: +def download_book(book_info: BookInfo, book_path: Path, progress_callback: Optional[Callable[[float], None]] = None, cancel_flag: Optional[Event] = None) -> bool: """Download a book from available sources. Args: @@ -324,10 +325,11 @@ def download_book(book_info: BookInfo, book_path: Path) -> bool: for link in download_links: try: - download_url = _get_download_url(link, book_info.title) + download_url = _get_download_url(link, book_info.title, cancel_flag) if download_url != "": logger.info(f"Downloading `{book_info.title}` from `{download_url}`") - data = downloader.download_url(download_url, book_info.size or "") + + data = downloader.download_url(download_url, book_info.size or "", progress_callback, cancel_flag) if not data: raise Exception("No data received") @@ -344,7 +346,7 @@ def download_book(book_info: BookInfo, book_path: Path) -> bool: return False -def _get_download_url(link: str, title: str) -> str: +def _get_download_url(link: str, title: str, cancel_flag: Optional[Event] = None) -> str: """Extract actual download URL from various source pages.""" url = "" @@ -371,8 +373,10 @@ def _get_download_url(link: str, title: str) -> str: if countdown: sleep_time = int(countdown[0].text) logger.info(f"Waiting {sleep_time}s for {title}") - time.sleep(sleep_time) - url = _get_download_url(link, title) + if cancel_flag is not None and cancel_flag.wait(timeout=sleep_time): + logger.info(f"Cancelled wait for {title}") + return "" + url = _get_download_url(link, title, cancel_flag) else: url = download_links[0]["href"] else: diff --git a/cloudflare_bypasser.py b/cloudflare_bypasser.py index 90273f8b..26293061 100644 --- a/cloudflare_bypasser.py +++ b/cloudflare_bypasser.py @@ -80,7 +80,6 @@ def _is_bypassed(sb) -> bool: "please wait", "ddos protection", "security check", - "browser check", "moment please", "hold on", "loading", diff --git a/downloader.py b/downloader.py index d64e0028..cbbf5f97 100644 --- a/downloader.py +++ b/downloader.py @@ -8,7 +8,8 @@ from io import BytesIO from typing import Optional from urllib.parse import urlparse from tqdm import tqdm - +from typing import Callable +from threading import Event from logger import setup_logger from config import PROXIES from env import MAX_RETRY, DEFAULT_SLEEP, USE_CF_BYPASS @@ -71,7 +72,7 @@ def html_get_page(url: str, retry: int = MAX_RETRY, use_bypasser: bool = False) time.sleep(sleep_time) return html_get_page(url, retry - 1, use_bypasser) -def download_url(link: str, size: str = "") -> Optional[BytesIO]: +def download_url(link: str, size: str = "", progress_callback: Optional[Callable[[float], None]] = None, cancel_flag: Optional[Event] = None) -> Optional[BytesIO]: """Download content from URL into a BytesIO buffer. Args: @@ -99,6 +100,11 @@ def download_url(link: str, size: str = "") -> Optional[BytesIO]: for chunk in response.iter_content(chunk_size=1000): buffer.write(chunk) pbar.update(len(chunk)) + if progress_callback is not None: + progress_callback(pbar.n * 100.0 / total_size) + if cancel_flag is not None and cancel_flag.is_set(): + logger.info(f"Download cancelled: {link}") + return None pbar.close() if buffer.tell() * 0.1 < total_size * 0.9: