mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Commit
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# --- Standard Library Imports ---
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt, QStandardPaths
|
||||
@@ -17,9 +18,9 @@ from ...config.constants import (
|
||||
THEME_KEY, LANGUAGE_KEY, DOWNLOAD_LOCATION_KEY,
|
||||
RESOLUTION_KEY, UI_SCALE_KEY, SAVE_CREATOR_JSON_KEY,
|
||||
COOKIE_TEXT_KEY, USE_COOKIE_KEY,
|
||||
FETCH_FIRST_KEY ### ADDED ###
|
||||
FETCH_FIRST_KEY
|
||||
)
|
||||
|
||||
from ...services.updater import UpdateChecker, UpdateDownloader
|
||||
|
||||
class FutureSettingsDialog(QDialog):
|
||||
"""
|
||||
@@ -30,6 +31,7 @@ class FutureSettingsDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app_ref
|
||||
self.setModal(True)
|
||||
self.update_downloader_thread = None # To keep a reference
|
||||
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
@@ -37,7 +39,7 @@ class FutureSettingsDialog(QDialog):
|
||||
|
||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 800
|
||||
scale_factor = screen_height / 800.0
|
||||
base_min_w, base_min_h = 420, 390
|
||||
base_min_w, base_min_h = 420, 480 # Increased height for update section
|
||||
scaled_min_w = int(base_min_w * scale_factor)
|
||||
scaled_min_h = int(base_min_h * scale_factor)
|
||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
||||
@@ -53,21 +55,19 @@ class FutureSettingsDialog(QDialog):
|
||||
self.interface_group_box = QGroupBox()
|
||||
interface_layout = QGridLayout(self.interface_group_box)
|
||||
|
||||
# Theme
|
||||
# Theme, UI Scale, Language (unchanged)...
|
||||
self.theme_label = QLabel()
|
||||
self.theme_toggle_button = QPushButton()
|
||||
self.theme_toggle_button.clicked.connect(self._toggle_theme)
|
||||
interface_layout.addWidget(self.theme_label, 0, 0)
|
||||
interface_layout.addWidget(self.theme_toggle_button, 0, 1)
|
||||
|
||||
# UI Scale
|
||||
self.ui_scale_label = QLabel()
|
||||
self.ui_scale_combo_box = QComboBox()
|
||||
self.ui_scale_combo_box.currentIndexChanged.connect(self._display_setting_changed)
|
||||
interface_layout.addWidget(self.ui_scale_label, 1, 0)
|
||||
interface_layout.addWidget(self.ui_scale_combo_box, 1, 1)
|
||||
|
||||
# Language
|
||||
|
||||
self.language_label = QLabel()
|
||||
self.language_combo_box = QComboBox()
|
||||
self.language_combo_box.currentIndexChanged.connect(self._language_selection_changed)
|
||||
@@ -78,6 +78,7 @@ class FutureSettingsDialog(QDialog):
|
||||
|
||||
self.download_window_group_box = QGroupBox()
|
||||
download_window_layout = QGridLayout(self.download_window_group_box)
|
||||
|
||||
self.window_size_label = QLabel()
|
||||
self.resolution_combo_box = QComboBox()
|
||||
self.resolution_combo_box.currentIndexChanged.connect(self._display_setting_changed)
|
||||
@@ -91,7 +92,7 @@ class FutureSettingsDialog(QDialog):
|
||||
download_window_layout.addWidget(self.save_path_button, 1, 1)
|
||||
|
||||
self.save_creator_json_checkbox = QCheckBox()
|
||||
self.save_creator_json_checkbox.stateChanged.connect(self._creator_json_setting_changed)
|
||||
self.save_creator_json_checkbox.stateChanged.connect(self._creator_json_setting_changed)
|
||||
download_window_layout.addWidget(self.save_creator_json_checkbox, 2, 0, 1, 2)
|
||||
|
||||
self.fetch_first_checkbox = QCheckBox()
|
||||
@@ -100,14 +101,96 @@ class FutureSettingsDialog(QDialog):
|
||||
|
||||
main_layout.addWidget(self.download_window_group_box)
|
||||
|
||||
# --- NEW: Update Section ---
|
||||
self.update_group_box = QGroupBox()
|
||||
update_layout = QGridLayout(self.update_group_box)
|
||||
self.version_label = QLabel()
|
||||
self.update_status_label = QLabel()
|
||||
self.check_update_button = QPushButton()
|
||||
self.check_update_button.clicked.connect(self._check_for_updates)
|
||||
update_layout.addWidget(self.version_label, 0, 0)
|
||||
update_layout.addWidget(self.update_status_label, 0, 1)
|
||||
update_layout.addWidget(self.check_update_button, 1, 0, 1, 2)
|
||||
main_layout.addWidget(self.update_group_box)
|
||||
# --- END: New Section ---
|
||||
|
||||
main_layout.addStretch(1)
|
||||
|
||||
self.ok_button = QPushButton()
|
||||
self.ok_button.clicked.connect(self.accept)
|
||||
main_layout.addWidget(self.ok_button, 0, Qt.AlignRight | Qt.AlignBottom)
|
||||
|
||||
def _retranslate_ui(self):
|
||||
self.setWindowTitle(self._tr("settings_dialog_title", "Settings"))
|
||||
self.interface_group_box.setTitle(self._tr("interface_group_title", "Interface Settings"))
|
||||
self.download_window_group_box.setTitle(self._tr("download_window_group_title", "Download & Window Settings"))
|
||||
self.theme_label.setText(self._tr("theme_label", "Theme:"))
|
||||
self.ui_scale_label.setText(self._tr("ui_scale_label", "UI Scale:"))
|
||||
self.language_label.setText(self._tr("language_label", "Language:"))
|
||||
self.window_size_label.setText(self._tr("window_size_label", "Window Size:"))
|
||||
self.default_path_label.setText(self._tr("default_path_label", "Default Path:"))
|
||||
self.save_creator_json_checkbox.setText(self._tr("save_creator_json_label", "Save Creator.json file"))
|
||||
self.fetch_first_checkbox.setText(self._tr("fetch_first_label", "Fetch First (Download after all pages are found)"))
|
||||
self.fetch_first_checkbox.setToolTip(self._tr("fetch_first_tooltip", "If checked, the downloader will find all posts from a creator first before starting any downloads.\nThis can be slower to start but provides a more accurate progress bar."))
|
||||
self._update_theme_toggle_button_text()
|
||||
self.save_path_button.setText(self._tr("settings_save_cookie_path_button", "Save Cookie + Download Path"))
|
||||
self.save_path_button.setToolTip(self._tr("settings_save_cookie_path_tooltip", "Save the current 'Download Location' and Cookie settings for future sessions."))
|
||||
self.ok_button.setText(self._tr("ok_button", "OK"))
|
||||
|
||||
# --- NEW: Translations for Update Section ---
|
||||
self.update_group_box.setTitle(self._tr("update_group_title", "Application Updates"))
|
||||
current_version = self.parent_app.windowTitle().split(' v')[-1]
|
||||
self.version_label.setText(self._tr("current_version_label", f"Current Version: v{current_version}"))
|
||||
self.update_status_label.setText(self._tr("update_status_ready", "Ready to check."))
|
||||
self.check_update_button.setText(self._tr("check_for_updates_button", "Check for Updates"))
|
||||
# --- END: New Translations ---
|
||||
|
||||
self._populate_display_combo_boxes()
|
||||
self._populate_language_combo_box()
|
||||
self._load_checkbox_states()
|
||||
|
||||
def _check_for_updates(self):
|
||||
"""Starts the update check thread."""
|
||||
self.check_update_button.setEnabled(False)
|
||||
self.update_status_label.setText(self._tr("update_status_checking", "Checking..."))
|
||||
current_version = self.parent_app.windowTitle().split(' v')[-1]
|
||||
|
||||
self.update_checker_thread = UpdateChecker(current_version)
|
||||
self.update_checker_thread.update_available.connect(self._on_update_available)
|
||||
self.update_checker_thread.up_to_date.connect(self._on_up_to_date)
|
||||
self.update_checker_thread.update_error.connect(self._on_update_error)
|
||||
self.update_checker_thread.start()
|
||||
|
||||
def _on_update_available(self, new_version, download_url):
|
||||
self.update_status_label.setText(self._tr("update_status_found", f"Update found: v{new_version}"))
|
||||
self.check_update_button.setEnabled(True)
|
||||
|
||||
reply = QMessageBox.question(self, self._tr("update_available_title", "Update Available"),
|
||||
self._tr("update_available_message", f"A new version (v{new_version}) is available.\nWould you like to download and install it now?"),
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.ok_button.setEnabled(False)
|
||||
self.check_update_button.setEnabled(False)
|
||||
self.update_status_label.setText(self._tr("update_status_downloading", "Downloading update..."))
|
||||
self.update_downloader_thread = UpdateDownloader(download_url, self.parent_app)
|
||||
self.update_downloader_thread.download_finished.connect(self._on_download_finished)
|
||||
self.update_downloader_thread.download_error.connect(self._on_update_error)
|
||||
self.update_downloader_thread.start()
|
||||
|
||||
def _on_download_finished(self):
|
||||
QApplication.instance().quit()
|
||||
|
||||
def _on_up_to_date(self, message):
|
||||
self.update_status_label.setText(self._tr("update_status_latest", message))
|
||||
self.check_update_button.setEnabled(True)
|
||||
|
||||
def _on_update_error(self, message):
|
||||
self.update_status_label.setText(self._tr("update_status_error", f"Error: {message}"))
|
||||
self.check_update_button.setEnabled(True)
|
||||
self.ok_button.setEnabled(True)
|
||||
|
||||
# --- (The rest of the file remains unchanged from your provided code) ---
|
||||
def _load_checkbox_states(self):
|
||||
"""Loads the initial state for all checkboxes from settings."""
|
||||
self.save_creator_json_checkbox.blockSignals(True)
|
||||
should_save = self.parent_app.settings.value(SAVE_CREATOR_JSON_KEY, True, type=bool)
|
||||
self.save_creator_json_checkbox.setChecked(should_save)
|
||||
@@ -119,13 +202,11 @@ class FutureSettingsDialog(QDialog):
|
||||
self.fetch_first_checkbox.blockSignals(False)
|
||||
|
||||
def _creator_json_setting_changed(self, state):
|
||||
"""Saves the state of the 'Save Creator.json' checkbox."""
|
||||
is_checked = state == Qt.Checked
|
||||
self.parent_app.settings.setValue(SAVE_CREATOR_JSON_KEY, is_checked)
|
||||
self.parent_app.settings.sync()
|
||||
|
||||
def _fetch_first_setting_changed(self, state):
|
||||
"""Saves the state of the 'Fetch First' checkbox."""
|
||||
is_checked = state == Qt.Checked
|
||||
self.parent_app.settings.setValue(FETCH_FIRST_KEY, is_checked)
|
||||
self.parent_app.settings.sync()
|
||||
@@ -135,34 +216,6 @@ class FutureSettingsDialog(QDialog):
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
self.setWindowTitle(self._tr("settings_dialog_title", "Settings"))
|
||||
|
||||
self.interface_group_box.setTitle(self._tr("interface_group_title", "Interface Settings"))
|
||||
self.download_window_group_box.setTitle(self._tr("download_window_group_title", "Download & Window Settings"))
|
||||
|
||||
self.theme_label.setText(self._tr("theme_label", "Theme:"))
|
||||
self.ui_scale_label.setText(self._tr("ui_scale_label", "UI Scale:"))
|
||||
self.language_label.setText(self._tr("language_label", "Language:"))
|
||||
|
||||
self.window_size_label.setText(self._tr("window_size_label", "Window Size:"))
|
||||
self.default_path_label.setText(self._tr("default_path_label", "Default Path:"))
|
||||
self.save_creator_json_checkbox.setText(self._tr("save_creator_json_label", "Save Creator.json file"))
|
||||
|
||||
self.fetch_first_checkbox.setText(self._tr("fetch_first_label", "Fetch First (Download after all pages are found)"))
|
||||
self.fetch_first_checkbox.setToolTip(self._tr("fetch_first_tooltip", "If checked, the downloader will find all posts from a creator first before starting any downloads.\nThis can be slower to start but provides a more accurate progress bar."))
|
||||
|
||||
self._update_theme_toggle_button_text()
|
||||
self.save_path_button.setText(self._tr("settings_save_cookie_path_button", "Save Cookie + Download Path"))
|
||||
self.save_path_button.setToolTip(self._tr("settings_save_cookie_path_tooltip", "Save the current 'Download Location' and Cookie settings for future sessions."))
|
||||
self.ok_button.setText(self._tr("ok_button", "OK"))
|
||||
|
||||
self._populate_display_combo_boxes()
|
||||
self._populate_language_combo_box()
|
||||
self._load_checkbox_states()
|
||||
|
||||
# --- (The rest of the file remains unchanged) ---
|
||||
|
||||
def _apply_theme(self):
|
||||
if self.parent_app and self.parent_app.current_theme == "dark":
|
||||
scale = getattr(self.parent_app, 'scale_factor', 1)
|
||||
@@ -188,14 +241,7 @@ class FutureSettingsDialog(QDialog):
|
||||
def _populate_display_combo_boxes(self):
|
||||
self.resolution_combo_box.blockSignals(True)
|
||||
self.resolution_combo_box.clear()
|
||||
resolutions = [
|
||||
("Auto", self._tr("auto_resolution", "Auto (System Default)")),
|
||||
("1280x720", "1280 x 720"),
|
||||
("1600x900", "1600 x 900"),
|
||||
("1920x1080", "1920 x 1080 (Full HD)"),
|
||||
("2560x1440", "2560 x 1440 (2K)"),
|
||||
("3840x2160", "3840 x 2160 (4K)")
|
||||
]
|
||||
resolutions = [("Auto", "Auto"), ("1280x720", "1280x720"), ("1600x900", "1600x900"), ("1920x1080", "1920x1080")]
|
||||
current_res = self.parent_app.settings.value(RESOLUTION_KEY, "Auto")
|
||||
for res_key, res_name in resolutions:
|
||||
self.resolution_combo_box.addItem(res_name, res_key)
|
||||
@@ -214,35 +260,22 @@ class FutureSettingsDialog(QDialog):
|
||||
(1.50, "150%"),
|
||||
(1.75, "175%"),
|
||||
(2.0, "200%")
|
||||
]
|
||||
|
||||
current_scale = float(self.parent_app.settings.value(UI_SCALE_KEY, 1.0))
|
||||
]
|
||||
current_scale = self.parent_app.settings.value(UI_SCALE_KEY, 1.0)
|
||||
for scale_val, scale_name in scales:
|
||||
self.ui_scale_combo_box.addItem(scale_name, scale_val)
|
||||
if abs(current_scale - scale_val) < 0.01:
|
||||
if abs(float(current_scale) - scale_val) < 0.01:
|
||||
self.ui_scale_combo_box.setCurrentIndex(self.ui_scale_combo_box.count() - 1)
|
||||
self.ui_scale_combo_box.blockSignals(False)
|
||||
|
||||
def _display_setting_changed(self):
|
||||
selected_res = self.resolution_combo_box.currentData()
|
||||
selected_scale = self.ui_scale_combo_box.currentData()
|
||||
|
||||
self.parent_app.settings.setValue(RESOLUTION_KEY, selected_res)
|
||||
self.parent_app.settings.setValue(UI_SCALE_KEY, selected_scale)
|
||||
self.parent_app.settings.sync()
|
||||
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
msg_box.setWindowTitle(self._tr("display_change_title", "Display Settings Changed"))
|
||||
msg_box.setText(self._tr("language_change_message", "A restart is required for these changes to take effect."))
|
||||
msg_box.setInformativeText(self._tr("language_change_informative", "Would you like to restart now?"))
|
||||
restart_button = msg_box.addButton(self._tr("restart_now_button", "Restart Now"), QMessageBox.ApplyRole)
|
||||
ok_button = msg_box.addButton(self._tr("ok_button", "OK"), QMessageBox.AcceptRole)
|
||||
msg_box.setDefaultButton(ok_button)
|
||||
msg_box.exec_()
|
||||
|
||||
if msg_box.clickedButton() == restart_button:
|
||||
self.parent_app._request_restart_application()
|
||||
QMessageBox.information(self, self._tr("display_change_title", "Display Settings Changed"),
|
||||
self._tr("language_change_message", "A restart is required..."))
|
||||
|
||||
def _populate_language_combo_box(self):
|
||||
self.language_combo_box.blockSignals(True)
|
||||
@@ -252,7 +285,7 @@ class FutureSettingsDialog(QDialog):
|
||||
("de", "Deutsch (German)"), ("es", "Español (Spanish)"), ("pt", "Português (Portuguese)"),
|
||||
("ru", "Русский (Russian)"), ("zh_CN", "简体中文 (Simplified Chinese)"),
|
||||
("zh_TW", "繁體中文 (Traditional Chinese)"), ("ko", "한국어 (Korean)")
|
||||
]
|
||||
]
|
||||
current_lang = self.parent_app.current_selected_language
|
||||
for lang_code, lang_name in languages:
|
||||
self.language_combo_box.addItem(lang_name, lang_code)
|
||||
@@ -266,59 +299,32 @@ class FutureSettingsDialog(QDialog):
|
||||
self.parent_app.settings.setValue(LANGUAGE_KEY, selected_lang_code)
|
||||
self.parent_app.settings.sync()
|
||||
self.parent_app.current_selected_language = selected_lang_code
|
||||
|
||||
self._retranslate_ui()
|
||||
if hasattr(self.parent_app, '_retranslate_main_ui'):
|
||||
self.parent_app._retranslate_main_ui()
|
||||
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
msg_box.setWindowTitle(self._tr("language_change_title", "Language Changed"))
|
||||
msg_box.setText(self._tr("language_change_message", "A restart is required..."))
|
||||
msg_box.setInformativeText(self._tr("language_change_informative", "Would you like to restart now?"))
|
||||
restart_button = msg_box.addButton(self._tr("restart_now_button", "Restart Now"), QMessageBox.ApplyRole)
|
||||
ok_button = msg_box.addButton(self._tr("ok_button", "OK"), QMessageBox.AcceptRole)
|
||||
msg_box.setDefaultButton(ok_button)
|
||||
msg_box.exec_()
|
||||
|
||||
if msg_box.clickedButton() == restart_button:
|
||||
self.parent_app._request_restart_application()
|
||||
self.parent_app._retranslate_main_ui()
|
||||
QMessageBox.information(self, self._tr("language_change_title", "Language Changed"),
|
||||
self._tr("language_change_message", "A restart is required..."))
|
||||
|
||||
def _save_cookie_and_path(self):
|
||||
"""Saves the current download path and/or cookie settings from the main window."""
|
||||
path_saved = False
|
||||
cookie_saved = False
|
||||
|
||||
if hasattr(self.parent_app, 'dir_input') and self.parent_app.dir_input:
|
||||
current_path = self.parent_app.dir_input.text().strip()
|
||||
if current_path and os.path.isdir(current_path):
|
||||
self.parent_app.settings.setValue(DOWNLOAD_LOCATION_KEY, current_path)
|
||||
path_saved = True
|
||||
|
||||
if hasattr(self.parent_app, 'use_cookie_checkbox'):
|
||||
use_cookie = self.parent_app.use_cookie_checkbox.isChecked()
|
||||
cookie_content = self.parent_app.cookie_text_input.text().strip()
|
||||
|
||||
if use_cookie and cookie_content:
|
||||
self.parent_app.settings.setValue(USE_COOKIE_KEY, True)
|
||||
self.parent_app.settings.setValue(COOKIE_TEXT_KEY, cookie_content)
|
||||
cookie_saved = True
|
||||
else:
|
||||
else:
|
||||
self.parent_app.settings.setValue(USE_COOKIE_KEY, False)
|
||||
self.parent_app.settings.setValue(COOKIE_TEXT_KEY, "")
|
||||
|
||||
self.parent_app.settings.sync()
|
||||
|
||||
# --- User Feedback ---
|
||||
if path_saved and cookie_saved:
|
||||
message = self._tr("settings_save_both_success", "Download location and cookie settings saved.")
|
||||
elif path_saved:
|
||||
message = self._tr("settings_save_path_only_success", "Download location saved. No cookie settings were active to save.")
|
||||
elif cookie_saved:
|
||||
message = self._tr("settings_save_cookie_only_success", "Cookie settings saved. Download location was not set.")
|
||||
if path_saved or cookie_saved:
|
||||
QMessageBox.information(self, "Settings Saved", "Settings have been saved.")
|
||||
else:
|
||||
QMessageBox.warning(self, self._tr("settings_save_nothing_title", "Nothing to Save"),
|
||||
self._tr("settings_save_nothing_message", "The download location is not a valid directory and no cookie was active."))
|
||||
return
|
||||
|
||||
QMessageBox.information(self, self._tr("settings_save_success_title", "Settings Saved"), message)
|
||||
QMessageBox.warning(self, "Nothing to Save", "No valid settings to save.")
|
||||
@@ -38,6 +38,9 @@ from ..core.api_client import download_from_api
|
||||
from ..core.discord_client import fetch_server_channels, fetch_channel_messages
|
||||
from ..core.manager import DownloadManager
|
||||
from ..core.nhentai_client import fetch_nhentai_gallery
|
||||
from ..core.bunkr_client import fetch_bunkr_data
|
||||
from ..core.saint2_client import fetch_saint2_data
|
||||
from ..core.erome_client import fetch_erome_data
|
||||
from .assets import get_app_icon_object
|
||||
from ..config.constants import *
|
||||
from ..utils.file_utils import KNOWN_NAMES, clean_folder_name
|
||||
@@ -283,19 +286,15 @@ 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 v6.5.0")
|
||||
self.setWindowTitle("Kemono Downloader v7.0.0")
|
||||
setup_ui(self)
|
||||
self._connect_signals()
|
||||
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
|
||||
self.log_signal.emit("ℹ️ 'Skip Current File' button has been removed.")
|
||||
if hasattr(self, 'character_input'):
|
||||
self.character_input.setToolTip(self._tr("character_input_tooltip", "Enter character names (comma-separated)..."))
|
||||
self.log_signal.emit(f"ℹ️ Manga filename style loaded: '{self.manga_filename_style}'")
|
||||
self.log_signal.emit(f"ℹ️ Skip words scope loaded: '{self.skip_words_scope}'")
|
||||
self.log_signal.emit(f"ℹ️ Character filter scope set to default: '{self.char_filter_scope}'")
|
||||
self.log_signal.emit(f"ℹ️ Multi-part download defaults to: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'}")
|
||||
self.log_signal.emit(f"ℹ️ Cookie text defaults to: Empty on launch")
|
||||
self.log_signal.emit(f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch")
|
||||
self.log_signal.emit(f"ℹ️ Scan post content for images defaults to: {'Enabled' if self.scan_content_images_setting else 'Disabled'}")
|
||||
self.log_signal.emit(f"ℹ️ Application language loaded: '{self.current_selected_language.upper()}' (UI may not reflect this yet).")
|
||||
self._retranslate_main_ui()
|
||||
@@ -304,6 +303,18 @@ class DownloaderApp (QWidget ):
|
||||
self._load_saved_cookie_settings()
|
||||
self._update_button_states_and_connections()
|
||||
self._check_for_interrupted_session()
|
||||
self._cleanup_after_update()
|
||||
|
||||
def _cleanup_after_update(self):
|
||||
"""Deletes the old executable after a successful update."""
|
||||
try:
|
||||
app_path = sys.executable
|
||||
old_app_path = os.path.join(os.path.dirname(app_path), "Kemono.Downloader.exe.old")
|
||||
if os.path.exists(old_app_path):
|
||||
os.remove(old_app_path)
|
||||
self.log_signal.emit("ℹ️ Cleaned up old application file after update.")
|
||||
except Exception as e:
|
||||
self.log_signal.emit(f"⚠️ Could not remove old application file: {e}")
|
||||
|
||||
def _apply_theme_and_restart_prompt(self):
|
||||
"""Applies the theme and prompts the user to restart."""
|
||||
@@ -925,7 +936,7 @@ class DownloaderApp (QWidget ):
|
||||
if hasattr (self ,'use_cookie_checkbox'):
|
||||
self .use_cookie_checkbox .toggled .connect (self ._update_cookie_input_visibility )
|
||||
if hasattr (self ,'link_input'):
|
||||
self .link_input .textChanged .connect (self ._sync_queue_with_link_input )
|
||||
self.link_input.textChanged.connect(self._update_ui_for_url_change)
|
||||
self.link_input.textChanged.connect(self._update_contextual_ui_elements)
|
||||
self.link_input.textChanged.connect(self._update_button_states_and_connections)
|
||||
if hasattr(self, 'discord_scope_toggle_button'):
|
||||
@@ -3125,15 +3136,75 @@ class DownloaderApp (QWidget ):
|
||||
if total_posts >0 or processed_posts >0 :
|
||||
self .file_progress_label .setText ("")
|
||||
|
||||
def _set_ui_for_specialized_downloader(self, is_specialized):
|
||||
"""Disables or enables UI elements for non-standard downloaders."""
|
||||
widgets_to_disable = [
|
||||
self.page_range_label, self.start_page_input, self.to_label, self.end_page_input,
|
||||
self.character_filter_widget, self.skip_words_input, self.skip_scope_toggle_button,
|
||||
self.remove_from_filename_input, self.radio_images, self.radio_videos,
|
||||
self.radio_only_archives, self.radio_only_links, self.radio_only_audio, self.radio_more,
|
||||
self.skip_zip_checkbox, self.download_thumbnails_checkbox, self.compress_images_checkbox,
|
||||
self.use_subfolders_checkbox, self.use_subfolder_per_post_checkbox,
|
||||
self.scan_content_images_checkbox, self.external_links_checkbox, self.manga_mode_checkbox,
|
||||
self.use_cookie_checkbox, self.keep_duplicates_checkbox, self.date_prefix_checkbox,
|
||||
self.manga_rename_toggle_button, self.manga_date_prefix_input,
|
||||
self.multipart_toggle_button, self.custom_folder_input, self.custom_folder_label,
|
||||
self.discord_scope_toggle_button, self.save_discord_as_pdf_btn
|
||||
]
|
||||
|
||||
enable_state = not is_specialized
|
||||
|
||||
for widget in widgets_to_disable:
|
||||
if widget:
|
||||
widget.setEnabled(enable_state)
|
||||
|
||||
# When disabling, force 'All' to be checked and disable it too
|
||||
if is_specialized and self.radio_all:
|
||||
self.radio_all.setChecked(True)
|
||||
self.radio_all.setEnabled(False)
|
||||
elif self.radio_all:
|
||||
self.radio_all.setEnabled(True)
|
||||
|
||||
# Re-run standard UI logic when re-enabling to restore correct states
|
||||
if enable_state:
|
||||
self._update_all_ui_states()
|
||||
|
||||
def _update_all_ui_states(self):
|
||||
"""A single function to call all UI update methods to restore state."""
|
||||
is_manga_checked = self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False
|
||||
is_subfolder_checked = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False
|
||||
is_cookie_checked = self.use_cookie_checkbox.isChecked() if self.use_cookie_checkbox else False
|
||||
|
||||
self.update_ui_for_manga_mode(is_manga_checked)
|
||||
self.update_custom_folder_visibility()
|
||||
self.update_page_range_enabled_state()
|
||||
if self.radio_group.checkedButton():
|
||||
self._handle_filter_mode_change(self.radio_group.checkedButton(), True)
|
||||
self.update_ui_for_subfolders(is_subfolder_checked)
|
||||
self._update_cookie_input_visibility(is_cookie_checked)
|
||||
|
||||
def _update_contextual_ui_elements(self, text=""):
|
||||
"""Shows or hides UI elements based on the URL, like the Discord scope button."""
|
||||
if not hasattr(self, 'discord_scope_toggle_button'): return
|
||||
|
||||
url_text = self.link_input.text().strip()
|
||||
service, _, _ = extract_post_info(url_text)
|
||||
|
||||
# Handle specialized downloaders (Bunkr, nhentai)
|
||||
is_saint2 = 'saint2.su' in url_text or 'saint2.pk' in url_text
|
||||
is_erome = 'erome.com' in url_text
|
||||
is_specialized = service in ['bunkr', 'nhentai'] or is_saint2 or is_erome
|
||||
self._set_ui_for_specialized_downloader(is_specialized)
|
||||
|
||||
# Handle Discord UI
|
||||
is_discord = (service == 'discord')
|
||||
self.discord_scope_toggle_button.setVisible(is_discord)
|
||||
if is_discord: self._update_discord_scope_button_text()
|
||||
else: self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
|
||||
self.save_discord_as_pdf_btn.setVisible(is_discord)
|
||||
|
||||
if is_discord:
|
||||
self._update_discord_scope_button_text()
|
||||
elif not is_specialized: # Don't change button text for specialized downloaders
|
||||
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
|
||||
|
||||
def _update_discord_scope_button_text(self):
|
||||
"""Updates the text of the discord scope button and the main download button."""
|
||||
@@ -3157,8 +3228,8 @@ class DownloaderApp (QWidget ):
|
||||
self._start_download_of_fetched_posts()
|
||||
return True
|
||||
|
||||
self.finish_lock = threading.Lock()
|
||||
self.is_finishing = False
|
||||
self.finish_lock = threading.Lock()
|
||||
self.is_finishing = False
|
||||
if self.active_update_profile:
|
||||
if not self.new_posts_for_update:
|
||||
return self._check_for_updates()
|
||||
@@ -3180,13 +3251,13 @@ class DownloaderApp (QWidget ):
|
||||
|
||||
processed_post_ids_for_restore = []
|
||||
manga_counters_for_restore = None
|
||||
start_offset_for_restore = 0
|
||||
start_offset_for_restore = 0
|
||||
|
||||
if is_restore and self.interrupted_session_data:
|
||||
self.log_signal.emit(" Restoring session state...")
|
||||
download_state = self.interrupted_session_data.get("download_state", {})
|
||||
processed_post_ids_for_restore = download_state.get("processed_post_ids", [])
|
||||
start_offset_for_restore = download_state.get("last_processed_offset", 0)
|
||||
start_offset_for_restore = download_state.get("last_processed_offset", 0)
|
||||
restored_hashes = download_state.get("successfully_downloaded_hashes", [])
|
||||
if restored_hashes:
|
||||
with self.downloaded_file_hashes_lock:
|
||||
@@ -3195,7 +3266,7 @@ class DownloaderApp (QWidget ):
|
||||
manga_counters_for_restore = download_state.get("manga_counters")
|
||||
if processed_post_ids_for_restore:
|
||||
self.log_signal.emit(f" Will skip {len(processed_post_ids_for_restore)} already processed posts.")
|
||||
if start_offset_for_restore > 0:
|
||||
if start_offset_for_restore > 0:
|
||||
self.log_signal.emit(f" Resuming fetch from page offset: {start_offset_for_restore}")
|
||||
if manga_counters_for_restore:
|
||||
self.log_signal.emit(f" Restoring manga counters: {manga_counters_for_restore}")
|
||||
@@ -3218,7 +3289,66 @@ class DownloaderApp (QWidget ):
|
||||
|
||||
api_url = direct_api_url if direct_api_url else self.link_input.text().strip()
|
||||
|
||||
# --- NEW: NHENTAI BATCH DOWNLOAD LOGIC ---
|
||||
# --- START: MOVED AND CORRECTED LOGIC ---
|
||||
# This block is moved to run before any special URL checks.
|
||||
main_ui_download_dir = self.dir_input.text().strip()
|
||||
extract_links_only = (self.radio_only_links and self.radio_only_links.isChecked())
|
||||
effective_output_dir_for_run = ""
|
||||
|
||||
if override_output_dir:
|
||||
if not main_ui_download_dir:
|
||||
QMessageBox.critical(self, "Configuration Error",
|
||||
"The main 'Download Location' must be set in the UI "
|
||||
"before downloading favorites with 'Artist Folders' scope.")
|
||||
if self.is_processing_favorites_queue:
|
||||
self.log_signal.emit(f"❌ Favorite download for '{api_url}' skipped: Main download directory not set.")
|
||||
return False
|
||||
|
||||
if not os.path.isdir(main_ui_download_dir):
|
||||
QMessageBox.critical(self, "Directory Error",
|
||||
f"The main 'Download Location' ('{main_ui_download_dir}') "
|
||||
"does not exist or is not a directory. Please set a valid one for 'Artist Folders' scope.")
|
||||
if self.is_processing_favorites_queue:
|
||||
self.log_signal.emit(f"❌ Favorite download for '{api_url}' skipped: Main download directory invalid.")
|
||||
return False
|
||||
effective_output_dir_for_run = os.path.normpath(override_output_dir)
|
||||
else:
|
||||
is_special_downloader = 'saint2.su' in api_url or 'saint2.pk' in api_url or 'nhentai.net' in api_url or 'bunkr' in api_url or 'erome.com' in api_url
|
||||
|
||||
if not extract_links_only and not main_ui_download_dir:
|
||||
QMessageBox.critical(self, "Input Error", "Download Directory is required.")
|
||||
return False
|
||||
|
||||
if not extract_links_only and main_ui_download_dir and not os.path.isdir(main_ui_download_dir):
|
||||
reply = QMessageBox.question(self, "Create Directory?",
|
||||
f"The directory '{main_ui_download_dir}' does not exist.\nCreate it now?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
os.makedirs(main_ui_download_dir, exist_ok=True)
|
||||
self.log_signal.emit(f"ℹ️ Created directory: {main_ui_download_dir}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Directory Error", f"Could not create directory: {e}")
|
||||
return False
|
||||
else:
|
||||
self.log_signal.emit("❌ Download cancelled: Output directory does not exist and was not created.")
|
||||
return False
|
||||
effective_output_dir_for_run = os.path.normpath(main_ui_download_dir) if main_ui_download_dir else ""
|
||||
|
||||
if 'erome.com' in api_url:
|
||||
self.log_signal.emit("ℹ️ Erome.com URL detected. Starting dedicated Erome download.")
|
||||
self.set_ui_enabled(False)
|
||||
|
||||
self.download_thread = EromeDownloadThread(api_url, effective_output_dir_for_run, self)
|
||||
self.download_thread.progress_signal.connect(self.handle_main_log)
|
||||
self.download_thread.file_progress_signal.connect(self.update_file_progress_display)
|
||||
self.download_thread.finished_signal.connect(
|
||||
lambda dl, skip, cancelled: self.download_finished(dl, skip, cancelled, [])
|
||||
)
|
||||
self.download_thread.start()
|
||||
self._update_button_states_and_connections()
|
||||
return True
|
||||
|
||||
if 'nhentai.net' in api_url and not re.search(r'/g/(\d+)', api_url):
|
||||
self.log_signal.emit("=" * 40)
|
||||
self.log_signal.emit("🚀 nhentai batch download mode detected.")
|
||||
@@ -3235,7 +3365,6 @@ class DownloaderApp (QWidget ):
|
||||
try:
|
||||
with open(nhentai_txt_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
# Find all URLs in the line (handles comma separation or just spaces)
|
||||
found_urls = re.findall(r'https?://nhentai\.net/g/\d+/?', line)
|
||||
if found_urls:
|
||||
urls_to_download.extend(found_urls)
|
||||
@@ -3261,49 +3390,65 @@ class DownloaderApp (QWidget ):
|
||||
if not self.is_processing_favorites_queue:
|
||||
self._process_next_favorite_download()
|
||||
return True
|
||||
# --- END NEW LOGIC ---
|
||||
|
||||
main_ui_download_dir = self.dir_input.text().strip()
|
||||
extract_links_only = (self.radio_only_links and self.radio_only_links.isChecked())
|
||||
effective_output_dir_for_run = ""
|
||||
is_saint2_url = 'saint2.su' in api_url or 'saint2.pk' in api_url
|
||||
if is_saint2_url:
|
||||
# First, check if it's the batch command. If so, do nothing here and let the next block handle it.
|
||||
if api_url.strip().lower() != 'saint2.su':
|
||||
self.log_signal.emit("ℹ️ Saint2.su URL detected. Starting dedicated Saint2 download.")
|
||||
self.set_ui_enabled(False)
|
||||
self.download_thread = Saint2DownloadThread(api_url, effective_output_dir_for_run, self)
|
||||
self.download_thread.progress_signal.connect(self.handle_main_log)
|
||||
self.download_thread.file_progress_signal.connect(self.update_file_progress_display)
|
||||
self.download_thread.finished_signal.connect(
|
||||
lambda dl, skip, cancelled: self.download_finished(dl, skip, cancelled, [])
|
||||
)
|
||||
self.download_thread.start()
|
||||
self._update_button_states_and_connections()
|
||||
return True
|
||||
|
||||
if override_output_dir:
|
||||
if not main_ui_download_dir:
|
||||
QMessageBox.critical(self, "Configuration Error",
|
||||
"The main 'Download Location' must be set in the UI "
|
||||
"before downloading favorites with 'Artist Folders' scope.")
|
||||
if self.is_processing_favorites_queue:
|
||||
self.log_signal.emit(f"❌ Favorite download for '{api_url}' skipped: Main download directory not set.")
|
||||
if api_url.strip().lower() == 'saint2.su':
|
||||
self.log_signal.emit("=" * 40)
|
||||
self.log_signal.emit("🚀 Saint2.su batch download mode detected.")
|
||||
|
||||
saint2_txt_path = os.path.join(self.app_base_dir, "appdata", "saint2.su.txt")
|
||||
self.log_signal.emit(f" Looking for batch file at: {saint2_txt_path}")
|
||||
|
||||
if not os.path.exists(saint2_txt_path):
|
||||
QMessageBox.warning(self, "File Not Found", f"To use batch mode, create a file named 'saint2.su.txt' in your 'appdata' folder.\n\nPlace one saint2.su URL on each line.")
|
||||
self.log_signal.emit(f" ❌ 'saint2.su.txt' not found. Aborting batch download.")
|
||||
return False
|
||||
|
||||
if not os.path.isdir(main_ui_download_dir):
|
||||
QMessageBox.critical(self, "Directory Error",
|
||||
f"The main 'Download Location' ('{main_ui_download_dir}') "
|
||||
"does not exist or is not a directory. Please set a valid one for 'Artist Folders' scope.")
|
||||
if self.is_processing_favorites_queue:
|
||||
self.log_signal.emit(f"❌ Favorite download for '{api_url}' skipped: Main download directory invalid.")
|
||||
return False
|
||||
effective_output_dir_for_run = os.path.normpath(override_output_dir)
|
||||
else:
|
||||
if not extract_links_only and not main_ui_download_dir:
|
||||
QMessageBox.critical(self, "Input Error", "Download Directory is required when not in 'Only Links' mode.")
|
||||
urls_to_download = []
|
||||
try:
|
||||
with open(saint2_txt_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
# Find valid saint2 URLs in the line
|
||||
found_urls = re.findall(r'https?://saint\d*\.(?:su|pk|cr|to)/(?:a|d|embed)/[^/?#\s]+', line)
|
||||
if found_urls:
|
||||
urls_to_download.extend(found_urls)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "File Error", f"Could not read 'saint2.su.txt':\n{e}")
|
||||
self.log_signal.emit(f" ❌ Error reading 'saint2.su.txt': {e}")
|
||||
return False
|
||||
|
||||
if not extract_links_only and main_ui_download_dir and not os.path.isdir(main_ui_download_dir):
|
||||
reply = QMessageBox.question(self, "Create Directory?",
|
||||
f"The directory '{main_ui_download_dir}' does not exist.\nCreate it now?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
os.makedirs(main_ui_download_dir, exist_ok=True)
|
||||
self.log_signal.emit(f"ℹ️ Created directory: {main_ui_download_dir}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Directory Error", f"Could not create directory: {e}")
|
||||
return False
|
||||
else:
|
||||
self.log_signal.emit("❌ Download cancelled: Output directory does not exist and was not created.")
|
||||
return False
|
||||
effective_output_dir_for_run = os.path.normpath(main_ui_download_dir)
|
||||
if not urls_to_download:
|
||||
QMessageBox.information(self, "Empty File", "No valid saint2.su URLs were found in 'saint2.su.txt'.")
|
||||
self.log_signal.emit(" 'saint2.su.txt' was found but contained no valid URLs.")
|
||||
return False
|
||||
|
||||
self.log_signal.emit(f" Found {len(urls_to_download)} URLs to process.")
|
||||
self.favorite_download_queue.clear()
|
||||
for url in urls_to_download:
|
||||
self.favorite_download_queue.append({
|
||||
'url': url,
|
||||
'name': f"saint2.su link from batch",
|
||||
'type': 'post' # Treat each URL as a single post-like item
|
||||
})
|
||||
|
||||
if not self.is_processing_favorites_queue:
|
||||
self._process_next_favorite_download()
|
||||
return True
|
||||
|
||||
if not is_restore:
|
||||
self._create_initial_session_file(api_url, effective_output_dir_for_run, remaining_queue=self.favorite_download_queue)
|
||||
@@ -3328,26 +3473,24 @@ class DownloaderApp (QWidget ):
|
||||
|
||||
self.cancellation_message_logged_this_session = False
|
||||
|
||||
# --- MODIFIED NHENTAI HANDLING ---
|
||||
nhentai_match = re.search(r'nhentai\.net/g/(\d+)', api_url)
|
||||
if nhentai_match:
|
||||
gallery_id = nhentai_match.group(1)
|
||||
service, id1, id2 = extract_post_info(api_url)
|
||||
|
||||
if service == 'nhentai':
|
||||
gallery_id = id1
|
||||
self.log_signal.emit("=" * 40)
|
||||
self.log_signal.emit(f"🚀 Detected nhentai gallery ID: {gallery_id}")
|
||||
|
||||
output_dir = self.dir_input.text().strip()
|
||||
if not output_dir or not os.path.isdir(output_dir):
|
||||
if not effective_output_dir_for_run or not os.path.isdir(effective_output_dir_for_run):
|
||||
QMessageBox.critical(self, "Input Error", "A valid Download Location is required.")
|
||||
return False
|
||||
|
||||
gallery_data = fetch_nhentai_gallery(gallery_id, self.log_signal.emit)
|
||||
|
||||
if not gallery_data:
|
||||
QMessageBox.critical(self, "Error", f"Could not retrieve gallery data for ID {gallery_id}. It may not exist or the API is unavailable.")
|
||||
QMessageBox.critical(self, "Error", f"Could not retrieve gallery data for ID {gallery_id}.")
|
||||
return False
|
||||
|
||||
self.set_ui_enabled(False)
|
||||
self.download_thread = NhentaiDownloadThread(gallery_data, output_dir, self)
|
||||
self.download_thread = NhentaiDownloadThread(gallery_data, effective_output_dir_for_run, self)
|
||||
self.download_thread.progress_signal.connect(self.handle_main_log)
|
||||
self.download_thread.finished_signal.connect(
|
||||
lambda dl, skip, cancelled: self.download_finished(dl, skip, cancelled, [])
|
||||
@@ -3355,9 +3498,20 @@ class DownloaderApp (QWidget ):
|
||||
self.download_thread.start()
|
||||
self._update_button_states_and_connections()
|
||||
return True
|
||||
# --- END MODIFIED HANDLING ---
|
||||
|
||||
service, id1, id2 = extract_post_info(api_url)
|
||||
if service == 'bunkr':
|
||||
self.log_signal.emit("ℹ️ Bunkr URL detected. Starting dedicated Bunkr download.")
|
||||
self.set_ui_enabled(False)
|
||||
|
||||
self.download_thread = BunkrDownloadThread(id1, effective_output_dir_for_run, self)
|
||||
self.download_thread.progress_signal.connect(self.handle_main_log)
|
||||
self.download_thread.file_progress_signal.connect(self.update_file_progress_display)
|
||||
self.download_thread.finished_signal.connect(
|
||||
lambda dl, skip, cancelled: self.download_finished(dl, skip, cancelled, [])
|
||||
)
|
||||
self.download_thread.start()
|
||||
self._update_button_states_and_connections()
|
||||
return True
|
||||
|
||||
if not service or not id1:
|
||||
QMessageBox.critical(self, "Input Error", "Invalid or unsupported URL format.")
|
||||
@@ -4137,6 +4291,79 @@ class DownloaderApp (QWidget ):
|
||||
self.is_restore_pending = True
|
||||
self.start_download(direct_api_url=restore_url, override_output_dir=restore_dir, is_restore=True)
|
||||
|
||||
def _update_ui_for_url_change(self, text=""):
|
||||
"""A single, authoritative function to update all UI states based on the URL."""
|
||||
url_text = self.link_input.text().strip()
|
||||
service, _, _ = extract_post_info(url_text)
|
||||
|
||||
# A list of all widgets that are context-dependent
|
||||
widgets_to_manage = [
|
||||
self.page_range_label, self.start_page_input, self.to_label, self.end_page_input,
|
||||
self.character_filter_widget, self.skip_words_input, self.skip_scope_toggle_button,
|
||||
self.remove_from_filename_input, self.radio_all, self.radio_images, self.radio_videos,
|
||||
self.radio_only_archives, self.radio_only_links, self.radio_only_audio, self.radio_more,
|
||||
self.skip_zip_checkbox, self.download_thumbnails_checkbox, self.compress_images_checkbox,
|
||||
self.use_subfolders_checkbox, self.use_subfolder_per_post_checkbox,
|
||||
self.scan_content_images_checkbox, self.external_links_checkbox, self.manga_mode_checkbox,
|
||||
self.use_cookie_checkbox, self.keep_duplicates_checkbox, self.date_prefix_checkbox,
|
||||
self.manga_rename_toggle_button, self.manga_date_prefix_input,
|
||||
self.multipart_toggle_button, self.custom_folder_input, self.custom_folder_label
|
||||
]
|
||||
|
||||
# --- Logic for Specialized Downloaders (Bunkr, nhentai) ---
|
||||
if service in ['bunkr', 'nhentai']:
|
||||
self.progress_log_label.setText("📜 Progress Log:")
|
||||
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
|
||||
|
||||
# Disable all complex settings
|
||||
for widget in widgets_to_manage:
|
||||
if widget:
|
||||
widget.setEnabled(False)
|
||||
|
||||
# Force 'All' filter and disable it
|
||||
if self.radio_all:
|
||||
self.radio_all.setChecked(True)
|
||||
|
||||
# Ensure Discord UI is hidden
|
||||
if hasattr(self, 'discord_scope_toggle_button'):
|
||||
self.discord_scope_toggle_button.setVisible(False)
|
||||
if hasattr(self, 'save_discord_as_pdf_btn'):
|
||||
self.save_discord_as_pdf_btn.setVisible(False)
|
||||
|
||||
return # CRUCIAL: Stop here for specialized URLs
|
||||
|
||||
# --- Logic for Standard Downloaders (Kemono, Coomer, Discord) ---
|
||||
|
||||
# First, re-enable all managed widgets as a baseline
|
||||
for widget in widgets_to_manage:
|
||||
if widget:
|
||||
widget.setEnabled(True)
|
||||
|
||||
# Now, apply context-specific rules for the standard downloaders
|
||||
is_discord = (service == 'discord')
|
||||
if hasattr(self, 'discord_scope_toggle_button'):
|
||||
self.discord_scope_toggle_button.setVisible(is_discord)
|
||||
if hasattr(self, 'save_discord_as_pdf_btn'):
|
||||
self.save_discord_as_pdf_btn.setVisible(is_discord)
|
||||
|
||||
if is_discord:
|
||||
self._update_discord_scope_button_text()
|
||||
else:
|
||||
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
|
||||
|
||||
# Re-run all the standard UI state functions to apply the correct logic
|
||||
is_manga_checked = self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False
|
||||
is_subfolder_checked = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False
|
||||
is_cookie_checked = self.use_cookie_checkbox.isChecked() if self.use_cookie_checkbox else False
|
||||
|
||||
self.update_ui_for_manga_mode(is_manga_checked)
|
||||
self.update_custom_folder_visibility()
|
||||
self.update_page_range_enabled_state()
|
||||
if self.radio_group and self.radio_group.checkedButton():
|
||||
self._handle_filter_mode_change(self.radio_group.checkedButton(), True)
|
||||
self.update_ui_for_subfolders(is_subfolder_checked)
|
||||
self._update_cookie_input_visibility(is_cookie_checked)
|
||||
|
||||
def start_single_threaded_download (self ,**kwargs ):
|
||||
global BackendDownloadThread
|
||||
try :
|
||||
@@ -4825,6 +5052,18 @@ class DownloaderApp (QWidget ):
|
||||
self.log_signal.emit(" Signaling nhentai download thread to cancel.")
|
||||
self.download_thread.cancel()
|
||||
|
||||
if isinstance(self.download_thread, BunkrDownloadThread):
|
||||
self.log_signal.emit(" Signaling Bunkr download thread to cancel.")
|
||||
self.download_thread.cancel()
|
||||
|
||||
if isinstance(self.download_thread, Saint2DownloadThread):
|
||||
self.log_signal.emit(" Signaling Saint2 download thread to cancel.")
|
||||
self.download_thread.cancel()
|
||||
|
||||
if isinstance(self.download_thread, EromeDownloadThread):
|
||||
self.log_signal.emit(" Signaling Erome download thread to cancel.")
|
||||
self.download_thread.cancel()
|
||||
|
||||
def _get_domain_for_service(self, service_name: str) -> str:
|
||||
"""Determines the base domain for a given service."""
|
||||
if not isinstance(service_name, str):
|
||||
@@ -5939,6 +6178,325 @@ class DownloaderApp (QWidget ):
|
||||
# Use a QTimer to avoid deep recursion and correctly move to the next item.
|
||||
QTimer.singleShot(100, self._process_next_favorite_download)
|
||||
|
||||
class Saint2DownloadThread(QThread):
|
||||
"""A dedicated QThread for handling saint2.su downloads."""
|
||||
progress_signal = pyqtSignal(str)
|
||||
file_progress_signal = pyqtSignal(str, object)
|
||||
finished_signal = pyqtSignal(int, int, bool) # dl_count, skip_count, cancelled
|
||||
|
||||
def __init__(self, url, output_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self.saint2_url = url
|
||||
self.output_dir = output_dir
|
||||
self.is_cancelled = False
|
||||
|
||||
def run(self):
|
||||
download_count = 0
|
||||
skip_count = 0
|
||||
self.progress_signal.emit("=" * 40)
|
||||
self.progress_signal.emit(f"🚀 Starting Saint2.su Download for: {self.saint2_url}")
|
||||
|
||||
# Use the new client to get the download info
|
||||
album_name, files_to_download = fetch_saint2_data(self.saint2_url, self.progress_signal.emit)
|
||||
|
||||
if not files_to_download:
|
||||
self.progress_signal.emit("❌ Failed to extract file information from Saint2. Aborting.")
|
||||
self.finished_signal.emit(0, 0, self.is_cancelled)
|
||||
return
|
||||
|
||||
# For single media, album_name is the title; for albums, it's the album title
|
||||
album_path = os.path.join(self.output_dir, album_name)
|
||||
try:
|
||||
os.makedirs(album_path, exist_ok=True)
|
||||
self.progress_signal.emit(f" Saving to folder: '{album_path}'")
|
||||
except OSError as e:
|
||||
self.progress_signal.emit(f"❌ Critical error creating directory: {e}")
|
||||
self.finished_signal.emit(0, len(files_to_download), self.is_cancelled)
|
||||
return
|
||||
|
||||
total_files = len(files_to_download)
|
||||
session = requests.Session()
|
||||
|
||||
for i, file_data in enumerate(files_to_download):
|
||||
if self.is_cancelled:
|
||||
self.progress_signal.emit(" Download cancelled by user.")
|
||||
skip_count = total_files - download_count
|
||||
break
|
||||
|
||||
filename = file_data.get('filename', f'untitled_{i+1}.mp4')
|
||||
file_url = file_data.get('url')
|
||||
headers = file_data.get('headers')
|
||||
filepath = os.path.join(album_path, filename)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
self.progress_signal.emit(f" -> Skip ({i+1}/{total_files}): '{filename}' already exists.")
|
||||
skip_count += 1
|
||||
continue
|
||||
|
||||
self.progress_signal.emit(f" Downloading ({i+1}/{total_files}): '{filename}'...")
|
||||
|
||||
try:
|
||||
response = session.get(file_url, stream=True, headers=headers, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded_size = 0
|
||||
last_update_time = time.time()
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if self.is_cancelled:
|
||||
break
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
current_time = time.time()
|
||||
if total_size > 0 and (current_time - last_update_time) > 0.5:
|
||||
self.file_progress_signal.emit(filename, (downloaded_size, total_size))
|
||||
last_update_time = current_time
|
||||
|
||||
if self.is_cancelled:
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
continue
|
||||
|
||||
if total_size > 0:
|
||||
self.file_progress_signal.emit(filename, (total_size, total_size))
|
||||
|
||||
download_count += 1
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.progress_signal.emit(f" ❌ Failed to download '{filename}'. Error: {e}")
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
skip_count += 1
|
||||
except Exception as e:
|
||||
self.progress_signal.emit(f" ❌ An unexpected error occurred with '{filename}': {e}")
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
skip_count += 1
|
||||
|
||||
self.file_progress_signal.emit("", None)
|
||||
self.finished_signal.emit(download_count, skip_count, self.is_cancelled)
|
||||
|
||||
def cancel(self):
|
||||
self.is_cancelled = True
|
||||
self.progress_signal.emit(" Cancellation signal received by Saint2 thread.")
|
||||
|
||||
class EromeDownloadThread(QThread):
|
||||
"""A dedicated QThread for handling erome.com downloads."""
|
||||
progress_signal = pyqtSignal(str)
|
||||
file_progress_signal = pyqtSignal(str, object)
|
||||
finished_signal = pyqtSignal(int, int, bool) # dl_count, skip_count, cancelled
|
||||
|
||||
def __init__(self, url, output_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self.erome_url = url
|
||||
self.output_dir = output_dir
|
||||
self.is_cancelled = False
|
||||
|
||||
def run(self):
|
||||
download_count = 0
|
||||
skip_count = 0
|
||||
self.progress_signal.emit("=" * 40)
|
||||
self.progress_signal.emit(f"🚀 Starting Erome.com Download for: {self.erome_url}")
|
||||
|
||||
album_name, files_to_download = fetch_erome_data(self.erome_url, self.progress_signal.emit)
|
||||
|
||||
if not files_to_download:
|
||||
self.progress_signal.emit("❌ Failed to extract file information from Erome. Aborting.")
|
||||
self.finished_signal.emit(0, 0, self.is_cancelled)
|
||||
return
|
||||
|
||||
album_path = os.path.join(self.output_dir, album_name)
|
||||
try:
|
||||
os.makedirs(album_path, exist_ok=True)
|
||||
self.progress_signal.emit(f" Saving to folder: '{album_path}'")
|
||||
except OSError as e:
|
||||
self.progress_signal.emit(f"❌ Critical error creating directory: {e}")
|
||||
self.finished_signal.emit(0, len(files_to_download), self.is_cancelled)
|
||||
return
|
||||
|
||||
total_files = len(files_to_download)
|
||||
session = requests.Session()
|
||||
|
||||
for i, file_data in enumerate(files_to_download):
|
||||
if self.is_cancelled:
|
||||
self.progress_signal.emit(" Download cancelled by user.")
|
||||
skip_count = total_files - download_count
|
||||
break
|
||||
|
||||
filename = file_data.get('filename', f'untitled_{i+1}.mp4')
|
||||
file_url = file_data.get('url')
|
||||
headers = file_data.get('headers')
|
||||
filepath = os.path.join(album_path, filename)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
self.progress_signal.emit(f" -> Skip ({i+1}/{total_files}): '{filename}' already exists.")
|
||||
skip_count += 1
|
||||
continue
|
||||
|
||||
self.progress_signal.emit(f" Downloading ({i+1}/{total_files}): '{filename}'...")
|
||||
|
||||
try:
|
||||
response = session.get(file_url, stream=True, headers=headers, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded_size = 0
|
||||
last_update_time = time.time()
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if self.is_cancelled:
|
||||
break
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
current_time = time.time()
|
||||
if total_size > 0 and (current_time - last_update_time) > 0.5:
|
||||
self.file_progress_signal.emit(filename, (downloaded_size, total_size))
|
||||
last_update_time = current_time
|
||||
|
||||
if self.is_cancelled:
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
continue
|
||||
|
||||
if total_size > 0:
|
||||
self.file_progress_signal.emit(filename, (total_size, total_size))
|
||||
|
||||
download_count += 1
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.progress_signal.emit(f" ❌ Failed to download '{filename}'. Error: {e}")
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
skip_count += 1
|
||||
except Exception as e:
|
||||
self.progress_signal.emit(f" ❌ An unexpected error occurred with '{filename}': {e}")
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
skip_count += 1
|
||||
|
||||
self.file_progress_signal.emit("", None)
|
||||
self.finished_signal.emit(download_count, skip_count, self.is_cancelled)
|
||||
|
||||
def cancel(self):
|
||||
self.is_cancelled = True
|
||||
self.progress_signal.emit(" Cancellation signal received by Erome thread.")
|
||||
|
||||
class BunkrDownloadThread(QThread):
|
||||
"""A dedicated QThread for handling Bunkr downloads."""
|
||||
progress_signal = pyqtSignal(str)
|
||||
# --- ADD THIS SIGNAL for detailed file progress ---
|
||||
file_progress_signal = pyqtSignal(str, object)
|
||||
finished_signal = pyqtSignal(int, int, bool, list)
|
||||
|
||||
def __init__(self, url, output_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self.bunkr_url = url
|
||||
self.output_dir = output_dir
|
||||
self.is_cancelled = False
|
||||
|
||||
class ThreadLogger:
|
||||
def __init__(self, signal_emitter):
|
||||
self.signal_emitter = signal_emitter
|
||||
def info(self, msg, *args, **kwargs):
|
||||
self.signal_emitter.emit(str(msg))
|
||||
def error(self, msg, *args, **kwargs):
|
||||
self.signal_emitter.emit(f"❌ ERROR: {msg}")
|
||||
def warning(self, msg, *args, **kwargs):
|
||||
self.signal_emitter.emit(f"⚠️ WARNING: {msg}")
|
||||
def debug(self, msg, *args, **kwargs):
|
||||
pass
|
||||
|
||||
self.logger = ThreadLogger(self.progress_signal)
|
||||
|
||||
def run(self):
|
||||
download_count = 0
|
||||
skip_count = 0
|
||||
self.progress_signal.emit("=" * 40)
|
||||
self.progress_signal.emit(f"🚀 Starting Bunkr Download for: {self.bunkr_url}")
|
||||
|
||||
album_name, files_to_download = fetch_bunkr_data(self.bunkr_url, self.logger)
|
||||
|
||||
if not files_to_download:
|
||||
self.progress_signal.emit("❌ Failed to extract file information from Bunkr. Aborting.")
|
||||
self.finished_signal.emit(0, 0, self.is_cancelled, [])
|
||||
return
|
||||
|
||||
album_path = os.path.join(self.output_dir, album_name)
|
||||
try:
|
||||
os.makedirs(album_path, exist_ok=True)
|
||||
self.progress_signal.emit(f" Saving to folder: '{album_path}'")
|
||||
except OSError as e:
|
||||
self.progress_signal.emit(f"❌ Critical error creating directory: {e}")
|
||||
self.finished_signal.emit(0, len(files_to_download), self.is_cancelled, [])
|
||||
return
|
||||
|
||||
total_files = len(files_to_download)
|
||||
for i, file_data in enumerate(files_to_download):
|
||||
if self.is_cancelled:
|
||||
self.progress_signal.emit(" Download cancelled by user.")
|
||||
skip_count = total_files - download_count
|
||||
break
|
||||
|
||||
filename = file_data.get('name', 'untitled_file')
|
||||
file_url = file_data.get('url')
|
||||
headers = file_data.get('_http_headers')
|
||||
|
||||
filename = re.sub(r'[<>:"/\\|?*]', '_', filename).strip()
|
||||
filepath = os.path.join(album_path, filename)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
self.progress_signal.emit(f" -> Skip ({i+1}/{total_files}): '{filename}' already exists.")
|
||||
skip_count += 1
|
||||
continue
|
||||
|
||||
self.progress_signal.emit(f" Downloading ({i+1}/{total_files}): '{filename}'...")
|
||||
|
||||
try:
|
||||
response = requests.get(file_url, stream=True, headers=headers, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
# --- MODIFY THIS BLOCK to calculate and emit progress ---
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded_size = 0
|
||||
last_update_time = time.time()
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if self.is_cancelled:
|
||||
break
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
current_time = time.time()
|
||||
if total_size > 0 and (current_time - last_update_time) > 0.5:
|
||||
self.file_progress_signal.emit(filename, (downloaded_size, total_size))
|
||||
last_update_time = current_time
|
||||
|
||||
if self.is_cancelled:
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
continue
|
||||
|
||||
# Emit final progress to show 100%
|
||||
if total_size > 0:
|
||||
self.file_progress_signal.emit(filename, (total_size, total_size))
|
||||
|
||||
download_count += 1
|
||||
# --- END MODIFICATION ---
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.progress_signal.emit(f" ❌ Failed to download '{filename}'. Error: {e}")
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
skip_count += 1
|
||||
except Exception as e:
|
||||
self.progress_signal.emit(f" ❌ An unexpected error occurred with '{filename}': {e}")
|
||||
if os.path.exists(filepath): os.remove(filepath)
|
||||
skip_count += 1
|
||||
|
||||
# Clear the progress label when finished
|
||||
self.file_progress_signal.emit("", None)
|
||||
self.finished_signal.emit(download_count, skip_count, self.is_cancelled, [])
|
||||
|
||||
def cancel(self):
|
||||
self.is_cancelled = True
|
||||
self.progress_signal.emit(" Cancellation signal received by Bunkr thread.")
|
||||
|
||||
class ExternalLinkDownloadThread (QThread ):
|
||||
"""A QThread to handle downloading multiple external links sequentially."""
|
||||
progress_signal =pyqtSignal (str )
|
||||
|
||||
Reference in New Issue
Block a user