From 1ad1e53b5721c79de102c4e334fcd195acac510b Mon Sep 17 00:00:00 2001 From: Yuvi9587 <114073886+Yuvi9587@users.noreply.github.com> Date: Sun, 28 Dec 2025 09:23:20 +0530 Subject: [PATCH] Commit --- data/dejavu-sans/DejaVuSans-Bold.pkl | Bin 0 -> 136893 bytes data/dejavu-sans/DejaVuSans.cw127.pkl | Bin 0 -> 514 bytes data/dejavu-sans/DejaVuSans.pkl | Bin 0 -> 136864 bytes src/core/api_client.py | 90 +++++++--- src/core/hentaifox.txt | 0 src/core/hentaifox_client.py | 60 +++++++ src/core/workers.py | 13 +- src/ui/classes/downloader_factory.py | 12 ++ src/ui/classes/hentaifox_downloader_thread.py | 136 +++++++++++++++ src/ui/dialogs/SinglePDF.py | 9 +- src/ui/main_window.py | 157 +++++++++++++----- 11 files changed, 412 insertions(+), 65 deletions(-) create mode 100644 data/dejavu-sans/DejaVuSans-Bold.pkl create mode 100644 data/dejavu-sans/DejaVuSans.cw127.pkl create mode 100644 data/dejavu-sans/DejaVuSans.pkl create mode 100644 src/core/hentaifox.txt create mode 100644 src/core/hentaifox_client.py create mode 100644 src/ui/classes/hentaifox_downloader_thread.py diff --git a/data/dejavu-sans/DejaVuSans-Bold.pkl b/data/dejavu-sans/DejaVuSans-Bold.pkl new file mode 100644 index 0000000000000000000000000000000000000000..dff76a03f9471c40f9e7bb052b75cb380fe4252e GIT binary patch literal 136893 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 n$vAfvXP-Q`Lo@>wT3QENQ*SjAOSiWO.*') + title_match = re.search(r'(.*?)', html) + title = title_match.group(1).replace(" - HentaiFox", "").strip() if title_match else f"Gallery {gallery_id}" + + # Extract Total Pages (Bash: grep -Eo 'Pages: [0-9]*') + pages_match = re.search(r'Pages: (\d+)', html) + if not pages_match: + raise ValueError("Could not find total pages count.") + + total_pages = int(pages_match.group(1)) + + return { + "id": gallery_id, + "title": title, + "total_pages": total_pages + } + +def get_image_link_for_page(gallery_id, page_num): + """ + Fetches the specific reader page to find the actual image URL. + Equivalent to the loop in the 'hentaifox' function: + url="https://hentaifox.com/g/${id}/${i}/" + """ + url = f"{BASE_URL}/g/{gallery_id}/{page_num}/" + response = requests.get(url, headers=HEADERS) + + # Extract image source (Bash: grep -Eo 'data-src="..."') + # Regex looks for: data-src="https://..." + match = re.search(r'data-src="(https://[^"]+)"', response.text) + + if match: + return match.group(1) + return None \ No newline at end of file diff --git a/src/core/workers.py b/src/core/workers.py index 43fcd93..171f732 100644 --- a/src/core/workers.py +++ b/src/core/workers.py @@ -62,7 +62,8 @@ def robust_clean_name(name): """A more robust function to remove illegal characters for filenames and folders.""" if not name: return "" - illegal_chars_pattern = r'[\x00-\x1f<>:"/\\|?*\']' + # FIX: Removed \' from the list so apostrophes are kept + illegal_chars_pattern = r'[\x00-\x1f<>:"/\\|?*]' cleaned_name = re.sub(illegal_chars_pattern, '', name) cleaned_name = cleaned_name.strip(' .') @@ -1599,12 +1600,11 @@ class PostProcessorWorker: should_create_post_subfolder = self.use_post_subfolders - if (not self.use_post_subfolders and self.use_subfolders and + if (not self.use_post_subfolders and self.sfp_threshold is not None and num_potential_files_in_post >= self.sfp_threshold): self.logger(f" ℹ️ Post has {num_potential_files_in_post} files (≥{self.sfp_threshold}). Activating Subfolder per Post via [sfp] command.") should_create_post_subfolder = True - base_folder_names_for_post_content = [] determined_post_save_path_for_history = self.override_output_dir if self.override_output_dir else self.download_root if not self.extract_links_only and self.use_subfolders: @@ -2462,6 +2462,7 @@ class DownloadThread(QThread): proxies=self.proxies ) + processed_count_for_delay = 0 for posts_batch_data in post_generator: if self.isInterruptionRequested(): was_process_cancelled = True @@ -2472,7 +2473,11 @@ class DownloadThread(QThread): was_process_cancelled = True break - # --- FIX: Ensure 'proxies' is in this dictionary --- + processed_count_for_delay += 1 + if processed_count_for_delay > 0 and processed_count_for_delay % 50 == 0: + self.logger(" ⏳ Safety Pause: Waiting 10 seconds to respect server rate limits...") + time.sleep(10) + worker_args = { 'post_data': individual_post_data, 'emitter': worker_signals_obj, diff --git a/src/ui/classes/downloader_factory.py b/src/ui/classes/downloader_factory.py index c24fa7f..0611f13 100644 --- a/src/ui/classes/downloader_factory.py +++ b/src/ui/classes/downloader_factory.py @@ -25,6 +25,7 @@ from .saint2_downloader_thread import Saint2DownloadThread from .simp_city_downloader_thread import SimpCityDownloadThread from .toonily_downloader_thread import ToonilyDownloadThread from .deviantart_downloader_thread import DeviantArtDownloadThread +from .hentaifox_downloader_thread import HentaiFoxDownloadThread def create_downloader_thread(main_app, api_url, service, id1, id2, effective_output_dir_for_run): """ @@ -185,6 +186,17 @@ def create_downloader_thread(main_app, api_url, service, id1, id2, effective_out cancellation_event=main_app.cancellation_event, parent=main_app ) + + # Handler for HentaiFox (New) + if 'hentaifox.com' in api_url or service == 'hentaifox': + main_app.log_signal.emit("🦊 HentaiFox URL detected.") + return HentaiFoxDownloadThread( + url_or_id=api_url, + output_dir=effective_output_dir_for_run, + parent=main_app + ) + + # ---------------------- # --- Fallback --- # If no specific handler matched based on service name or URL pattern, return None. diff --git a/src/ui/classes/hentaifox_downloader_thread.py b/src/ui/classes/hentaifox_downloader_thread.py new file mode 100644 index 0000000..023f130 --- /dev/null +++ b/src/ui/classes/hentaifox_downloader_thread.py @@ -0,0 +1,136 @@ +import os +import time +import requests +from PyQt5.QtCore import QThread, pyqtSignal +from ...core.hentaifox_client import get_gallery_metadata, get_image_link_for_page, get_gallery_id +from ...utils.file_utils import clean_folder_name + +class HentaiFoxDownloadThread(QThread): + progress_signal = pyqtSignal(str) # Log messages + file_progress_signal = pyqtSignal(str, object) # filename, (current_bytes, total_bytes) + # finished_signal: (downloaded_count, skipped_count, was_cancelled, kept_files_list) + finished_signal = pyqtSignal(int, int, bool, list) + + def __init__(self, url_or_id, output_dir, parent=None): + super().__init__(parent) + self.gallery_id = get_gallery_id(url_or_id) + self.output_dir = output_dir + self.is_running = True + self.downloaded_count = 0 + self.skipped_count = 0 + + def run(self): + try: + self.progress_signal.emit(f"🔍 [HentaiFox] Fetching metadata for ID: {self.gallery_id}...") + + # 1. Get Info + try: + data = get_gallery_metadata(self.gallery_id) + except Exception as e: + self.progress_signal.emit(f"❌ [HentaiFox] Failed to fetch metadata: {e}") + self.finished_signal.emit(0, 0, False, []) + return + + title = clean_folder_name(data['title']) + total_pages = data['total_pages'] + + # 2. Setup Folder + save_folder = os.path.join(self.output_dir, f"[{self.gallery_id}] {title}") + os.makedirs(save_folder, exist_ok=True) + + self.progress_signal.emit(f"📂 Saving to: {save_folder}") + self.progress_signal.emit(f"📄 Found {total_pages} pages. Starting download...") + + # 3. Iterate and Download + for i in range(1, total_pages + 1): + if not self.is_running: + self.progress_signal.emit("🛑 Download cancelled by user.") + break + + # Fetch image link for this specific page + try: + img_url = get_image_link_for_page(self.gallery_id, i) + + if img_url: + ext = img_url.split('.')[-1] + filename = f"{i:03d}.{ext}" + filepath = os.path.join(save_folder, filename) + + # Check if exists + if os.path.exists(filepath): + self.progress_signal.emit(f"⚠️ [{i}/{total_pages}] Skipped (Exists): {filename}") + self.skipped_count += 1 + else: + self.progress_signal.emit(f"⬇️ [{i}/{total_pages}] Downloading: {filename}") + + # CALL NEW DOWNLOAD FUNCTION + success = self.download_image_with_progress(img_url, filepath, filename) + + if success: + self.progress_signal.emit(f"✅ [{i}/{total_pages}] Finished: {filename}") + self.downloaded_count += 1 + else: + self.progress_signal.emit(f"❌ [{i}/{total_pages}] Failed: {filename}") + self.skipped_count += 1 + else: + self.progress_signal.emit(f"❌ [{i}/{total_pages}] Error: No image link found.") + self.skipped_count += 1 + + except Exception as e: + self.progress_signal.emit(f"❌ [{i}/{total_pages}] Exception: {e}") + self.skipped_count += 1 + + time.sleep(0.5) + + # 4. Final Summary + summary = ( + f"\n🏁 [HentaiFox] Task Complete!\n" + f" - Total: {total_pages}\n" + f" - Downloaded: {self.downloaded_count}\n" + f" - Skipped: {self.skipped_count}\n" + ) + self.progress_signal.emit(summary) + + except Exception as e: + self.progress_signal.emit(f"❌ Critical Error: {str(e)}") + + self.finished_signal.emit(self.downloaded_count, self.skipped_count, not self.is_running, []) + + def download_image_with_progress(self, url, path, filename): + """Downloads file while emitting byte-level progress signals.""" + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Referer": "https://hentaifox.com/" + } + + try: + # stream=True is required to get size before downloading body + r = requests.get(url, headers=headers, stream=True, timeout=20) + if r.status_code != 200: + return False + + # Get Total Size (in bytes) + total_size = int(r.headers.get('content-length', 0)) + downloaded_size = 0 + + chunk_size = 1024 # 1KB chunks + + with open(path, 'wb') as f: + for chunk in r.iter_content(chunk_size): + if not self.is_running: + r.close() + return False + + if chunk: + f.write(chunk) + downloaded_size += len(chunk) + + self.file_progress_signal.emit(filename, (downloaded_size, total_size)) + + return True + except Exception as e: + print(f"Download Error: {e}") + return False + + def stop(self): + self.is_running = False \ No newline at end of file diff --git a/src/ui/dialogs/SinglePDF.py b/src/ui/dialogs/SinglePDF.py index f8cddca..d8ff49a 100644 --- a/src/ui/dialogs/SinglePDF.py +++ b/src/ui/dialogs/SinglePDF.py @@ -1,5 +1,7 @@ import os import re +import sys + try: from fpdf import FPDF FPDF_AVAILABLE = True @@ -18,7 +20,9 @@ try: self.set_font(self.font_family_main, '', 8) self.cell(0, 10, 'Page ' + str(self.page_no()), 0, 0, 'C') -except ImportError: +except Exception as e: + print(f"\n❌ DEBUG INFO: Import failed. The specific error is: {e}") + print(f"❌ DEBUG INFO: Python running this script is located at: {sys.executable}\n") FPDF_AVAILABLE = False FPDF = None PDF = None @@ -244,6 +248,9 @@ def create_single_pdf_from_content(posts_data, output_filename, font_path, add_i pdf.multi_cell(w=0, h=7, txt=post.get('content', 'No Content')) try: + output_dir = os.path.dirname(output_filename) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) pdf.output(output_filename) logger(f"✅ Successfully created single PDF: '{os.path.basename(output_filename)}'") return True diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 5800ee9..a9b1ff5 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -106,6 +106,7 @@ from .classes.external_link_downloader_thread import ExternalLinkDownloadThread from .classes.nhentai_downloader_thread import NhentaiDownloadThread from .classes.downloader_factory import create_downloader_thread from .classes.kemono_discord_downloader_thread import KemonoDiscordDownloadThread +from .classes.hentaifox_downloader_thread import HentaiFoxDownloadThread _ff_ver = (datetime.date.today().toordinal() - 735506) // 28 USERAGENT_FIREFOX = (f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; " @@ -309,6 +310,9 @@ class DownloaderApp (QWidget ): self.downloaded_hash_counts_lock = threading.Lock() self.session_temp_files = [] self.single_pdf_mode = False + + self.temp_pdf_content_list = [] + self.last_effective_download_dir = None self.save_creator_json_enabled_this_session = True self.date_prefix_format = self.settings.value(DATE_PREFIX_FORMAT_KEY, "YYYY-MM-DD {post}", type=str) self.is_single_post_session = False @@ -346,7 +350,7 @@ class DownloaderApp (QWidget ): self.download_location_label_widget = None self.remove_from_filename_label_widget = None self.skip_words_label_widget = None - self.setWindowTitle("Kemono Downloader v7.9.0") + self.setWindowTitle("Kemono Downloader v7.9.1") setup_ui(self) self._connect_signals() if hasattr(self, 'character_input'): @@ -3918,7 +3922,11 @@ class DownloaderApp (QWidget ): 'txt_file': 'coomer.txt', 'url_regex': r'https?://(?:www\.)?coomer\.(?:su|party|st)/[^/\s]+/user/[^/\s]+(?:/post/\d+)?/?' }, - + 'hentaifox.com': { + 'name': 'HentaiFox', + 'txt_file': 'hentaifox.txt', + 'url_regex': r'https?://(?:www\.)?hentaifox\.com/(?:g|gallery)/\d+/?' + }, 'allporncomic.com': { 'name': 'AllPornComic', 'txt_file': 'allporncomic.txt', @@ -3999,7 +4007,8 @@ class DownloaderApp (QWidget ): 'toonily.com', 'toonily.me', 'hentai2read.com', 'saint2.su', 'saint2.pk', - 'imgur.com', 'bunkr.' + 'imgur.com', 'bunkr.', + 'hentaifox.com' ] for url in urls_to_download: @@ -4087,6 +4096,7 @@ class DownloaderApp (QWidget ): self._clear_stale_temp_files() self.session_temp_files = [] + self.temp_pdf_content_list = [] processed_post_ids_for_restore = [] manga_counters_for_restore = None @@ -4170,6 +4180,7 @@ class DownloaderApp (QWidget ): return False effective_output_dir_for_run = os.path.normpath(main_ui_download_dir) if main_ui_download_dir else "" + self.last_effective_download_dir = effective_output_dir_for_run if not is_restore: self._create_initial_session_file(api_url, effective_output_dir_for_run, remaining_queue=self.favorite_download_queue) @@ -5600,8 +5611,18 @@ class DownloaderApp (QWidget ): permanent, history_data, temp_filepath) = result_tuple - if temp_filepath: self.session_temp_files.append(temp_filepath) - + if temp_filepath: + self.session_temp_files.append(temp_filepath) + + # If Single PDF mode is enabled, we need to load the data + # from the temp file into memory for the final aggregation. + if self.single_pdf_setting: + try: + with open(temp_filepath, 'r', encoding='utf-8') as f: + post_content_data = json.load(f) + self.temp_pdf_content_list.append(post_content_data) + except Exception as e: + self.log_signal.emit(f"⚠️ Error reading temp file for PDF aggregation: {e}") with self.downloaded_files_lock: self.download_counter += downloaded self.skip_counter += skipped @@ -5627,47 +5648,73 @@ class DownloaderApp (QWidget ): self.finished_signal.emit(self.download_counter, self.skip_counter, self.cancellation_event.is_set(), self.all_kept_original_filenames) def _trigger_single_pdf_creation(self): - """Reads temp files, sorts them by date, then creates the single PDF.""" - self.log_signal.emit("="*40) - self.log_signal.emit("Creating single PDF from collected text files...") - - posts_content_data = [] - for temp_filepath in self.session_temp_files: - try: - with open(temp_filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - posts_content_data.append(data) - except Exception as e: - self.log_signal.emit(f" ⚠️ Could not read temp file '{temp_filepath}': {e}") - - if not posts_content_data: - self.log_signal.emit(" No content was collected. Aborting PDF creation.") + """ + Triggers the creation of a single PDF from collected text content in a BACKGROUND THREAD. + """ + if not self.temp_pdf_content_list: + self.log_signal.emit("⚠️ No content collected for Single PDF.") return - output_dir = self.dir_input.text().strip() or QStandardPaths.writableLocation(QStandardPaths.DownloadLocation) - default_filename = os.path.join(output_dir, "Consolidated_Content.pdf") - filepath, _ = QFileDialog.getSaveFileName(self, "Save Single PDF", default_filename, "PDF Files (*.pdf)") + # 1. Sort the content + self.log_signal.emit(" Sorting collected content for PDF...") + def sort_key(post): + p_date = post.get('published') or "0000-00-00" + a_date = post.get('added') or "0000-00-00" + pid = post.get('id') or "0" + return (p_date, a_date, pid) - if not filepath: - self.log_signal.emit(" Single PDF creation cancelled by user.") - return + sorted_content = sorted(self.temp_pdf_content_list, key=sort_key) - if not filepath.lower().endswith('.pdf'): - filepath += '.pdf' + # 2. Determine Filename + first_post = sorted_content[0] + creator_name = first_post.get('creator_name') or first_post.get('user') or "Unknown_Creator" + clean_creator = clean_folder_name(creator_name) + filename = f"[{clean_creator}] Complete_Collection.pdf" + + # --- FIX 3: Corrected Fallback Logic --- + # Use the stored dir, or fall back to the text input in the UI, or finally the app root + base_dir = self.last_effective_download_dir + if not base_dir: + base_dir = self.dir_input.text().strip() + if not base_dir: + base_dir = self.app_base_dir + + output_path = os.path.join(base_dir, filename) + # --------------------------------------- + + # 3. Get Options font_path = os.path.join(self.app_base_dir, 'data', 'dejavu-sans', 'DejaVuSans.ttf') - - self.log_signal.emit(" Sorting collected posts by date (oldest first)...") - sorted_content = sorted(posts_content_data, key=lambda x: x.get('published', 'Z')) + # Get 'Add Info Page' preference + add_info = True + if hasattr(self, 'more_options_dialog') and self.more_options_dialog: + add_info = self.more_options_dialog.get_add_info_state() + elif hasattr(self, 'add_info_in_pdf_setting'): + add_info = self.add_info_in_pdf_setting - create_single_pdf_from_content( - sorted_content, - filepath, - font_path, - add_info_page=self.add_info_in_pdf_setting, # Pass the flag here - logger=self.log_signal.emit + # 4. START THE THREAD + self.pdf_thread = PdfGenerationThread( + posts_data=sorted_content, + output_filename=output_path, + font_path=font_path, + add_info_page=add_info, + logger_func=self.log_signal.emit ) - self.log_signal.emit("="*40) + + self.pdf_thread.finished_signal.connect(self._on_pdf_generation_finished) + self.pdf_thread.start() + + def _on_pdf_generation_finished(self, success, message): + """Callback for when the PDF thread is done.""" + if success: + self.log_signal.emit(f"✅ {message}") + QMessageBox.information(self, "PDF Created", message) + else: + self.log_signal.emit(f"❌ PDF Creation Error: {message}") + QMessageBox.warning(self, "PDF Error", f"Could not create PDF: {message}") + + # Optional: Clear the temp list now that we are done + self.temp_pdf_content_list = [] def _add_to_history_candidates(self, history_data): """Adds processed post data to the history candidates list and updates the creator profile.""" @@ -7468,4 +7515,36 @@ class DownloaderApp (QWidget ): if not success_starting_download: self.log_signal.emit(f"⚠️ Failed to initiate download for '{item_display_name}'. Skipping and moving to the next item in queue.") - QTimer.singleShot(100, self._process_next_favorite_download) \ No newline at end of file + QTimer.singleShot(100, self._process_next_favorite_download) + +class PdfGenerationThread(QThread): + finished_signal = pyqtSignal(bool, str) # success, message + + def __init__(self, posts_data, output_filename, font_path, add_info_page, logger_func): + super().__init__() + self.posts_data = posts_data + self.output_filename = output_filename + self.font_path = font_path + self.add_info_page = add_info_page + self.logger_func = logger_func + + def run(self): + try: + from .dialogs.SinglePDF import create_single_pdf_from_content + self.logger_func("📄 Background Task: Generating Single PDF... (This may take a while)") + + success = create_single_pdf_from_content( + self.posts_data, + self.output_filename, + self.font_path, + self.add_info_page, + logger=self.logger_func + ) + + if success: + self.finished_signal.emit(True, f"PDF Saved: {os.path.basename(self.output_filename)}") + else: + self.finished_signal.emit(False, "PDF generation failed.") + + except Exception as e: + self.finished_signal.emit(False, str(e)) \ No newline at end of file