mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e52cfd78 | ||
|
|
e665fd3cde | ||
|
|
fc94f4c691 | ||
|
|
78e2012f04 | ||
|
|
3fe9dbacc6 | ||
|
|
004dea06e0 | ||
|
|
8994a69c34 | ||
|
|
f4a692673e | ||
|
|
4cb5f14ef6 | ||
|
|
a596c4f350 | ||
|
|
e091c60d29 | ||
|
|
d2ea026a41 |
BIN
Kemono.png
Normal file
BIN
Kemono.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/discord.png
Normal file
BIN
assets/discord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/github.png
Normal file
BIN
assets/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/instagram.png
Normal file
BIN
assets/instagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -31,6 +31,7 @@ from io import BytesIO
|
|||||||
STYLE_POST_TITLE = "post_title"
|
STYLE_POST_TITLE = "post_title"
|
||||||
STYLE_ORIGINAL_NAME = "original_name"
|
STYLE_ORIGINAL_NAME = "original_name"
|
||||||
STYLE_DATE_BASED = "date_based" # For manga date-based sequential naming
|
STYLE_DATE_BASED = "date_based" # For manga date-based sequential naming
|
||||||
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # For manga post title + global counter
|
||||||
|
|
||||||
SKIP_SCOPE_FILES = "files"
|
SKIP_SCOPE_FILES = "files"
|
||||||
SKIP_SCOPE_POSTS = "posts"
|
SKIP_SCOPE_POSTS = "posts"
|
||||||
@@ -276,7 +277,7 @@ def prepare_cookies_for_request(use_cookie_flag, cookie_text_input, selected_coo
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_event=None, pause_event=None, cookies_dict=None):
|
def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_event=None, pause_event=None, cookies_dict=None):
|
||||||
if cancellation_event and cancellation_event.is_set():
|
if cancellation_event and cancellation_event.is_set(): # type: ignore
|
||||||
logger(" Fetch cancelled before request.")
|
logger(" Fetch cancelled before request.")
|
||||||
raise RuntimeError("Fetch operation cancelled by user.")
|
raise RuntimeError("Fetch operation cancelled by user.")
|
||||||
|
|
||||||
@@ -284,7 +285,7 @@ def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_ev
|
|||||||
logger(" Post fetching paused...")
|
logger(" Post fetching paused...")
|
||||||
while pause_event.is_set():
|
while pause_event.is_set():
|
||||||
if cancellation_event and cancellation_event.is_set():
|
if cancellation_event and cancellation_event.is_set():
|
||||||
logger(" Post fetching cancelled while paused.")
|
logger(" Post fetching cancelled while paused.") # type: ignore
|
||||||
raise RuntimeError("Fetch operation cancelled by user.")
|
raise RuntimeError("Fetch operation cancelled by user.")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
logger(" Post fetching resumed.")
|
logger(" Post fetching resumed.")
|
||||||
@@ -379,21 +380,37 @@ def download_from_api(api_url_input, logger=print, start_page=None, end_page=Non
|
|||||||
page_size = 50
|
page_size = 50
|
||||||
|
|
||||||
if is_creator_feed_for_manga:
|
if is_creator_feed_for_manga:
|
||||||
logger(" Manga Mode: Fetching all posts to reverse order (oldest posts processed first)...")
|
logger(" Manga Mode: Fetching posts to sort by date (oldest processed first)...")
|
||||||
all_posts_for_manga_mode = []
|
all_posts_for_manga_mode = []
|
||||||
|
|
||||||
current_offset_manga = 0
|
current_offset_manga = 0
|
||||||
|
# Determine starting page and offset for manga mode
|
||||||
|
if start_page and start_page > 1:
|
||||||
|
current_offset_manga = (start_page - 1) * page_size
|
||||||
|
logger(f" Manga Mode: Starting fetch from page {start_page} (offset {current_offset_manga}).")
|
||||||
|
elif start_page: # start_page is 1
|
||||||
|
logger(f" Manga Mode: Starting fetch from page 1 (offset 0).")
|
||||||
|
|
||||||
|
if end_page:
|
||||||
|
logger(f" Manga Mode: Will fetch up to page {end_page}.")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if pause_event and pause_event.is_set():
|
if pause_event and pause_event.is_set():
|
||||||
logger(" Manga mode post fetching paused...") # type: ignor
|
logger(" Manga mode post fetching paused...") # type: ignore
|
||||||
while pause_event.is_set():
|
while pause_event.is_set():
|
||||||
if cancellation_event and cancellation_event.is_set():
|
if cancellation_event and cancellation_event.is_set():
|
||||||
logger(" Manga mode post fetching cancelled while paused.")
|
logger(" Manga mode post fetching cancelled while paused.") # type: ignore
|
||||||
break
|
break
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
if not (cancellation_event and cancellation_event.is_set()): logger(" Manga mode post fetching resumed.")
|
if not (cancellation_event and cancellation_event.is_set()): logger(" Manga mode post fetching resumed.")
|
||||||
if cancellation_event and cancellation_event.is_set():
|
if cancellation_event and cancellation_event.is_set():
|
||||||
logger(" Manga mode post fetching cancelled.")
|
logger(" Manga mode post fetching cancelled.")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
current_page_num_manga = (current_offset_manga // page_size) + 1
|
||||||
|
if end_page and current_page_num_manga > end_page:
|
||||||
|
logger(f" Manga Mode: Reached specified end page ({end_page}). Stopping post fetch.")
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
posts_batch_manga = fetch_posts_paginated(api_base_url, headers, current_offset_manga, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
|
posts_batch_manga = fetch_posts_paginated(api_base_url, headers, current_offset_manga, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
|
||||||
if not isinstance(posts_batch_manga, list):
|
if not isinstance(posts_batch_manga, list):
|
||||||
@@ -401,7 +418,11 @@ def download_from_api(api_url_input, logger=print, start_page=None, end_page=Non
|
|||||||
break
|
break
|
||||||
if not posts_batch_manga:
|
if not posts_batch_manga:
|
||||||
logger("✅ Reached end of posts (Manga Mode fetch all).")
|
logger("✅ Reached end of posts (Manga Mode fetch all).")
|
||||||
break
|
if start_page and not end_page and current_page_num_manga < start_page: # Started on a page with no posts
|
||||||
|
logger(f" Manga Mode: No posts found on or after specified start page {start_page}.")
|
||||||
|
elif end_page and current_page_num_manga <= end_page and not all_posts_for_manga_mode: # Range specified but no posts in it
|
||||||
|
logger(f" Manga Mode: No posts found within the specified page range ({start_page or 1}-{end_page}).")
|
||||||
|
break # No more posts from API
|
||||||
all_posts_for_manga_mode.extend(posts_batch_manga)
|
all_posts_for_manga_mode.extend(posts_batch_manga)
|
||||||
current_offset_manga += page_size # Increment by page_size for the next API call's 'o' parameter
|
current_offset_manga += page_size # Increment by page_size for the next API call's 'o' parameter
|
||||||
time.sleep(0.6)
|
time.sleep(0.6)
|
||||||
@@ -420,7 +441,7 @@ def download_from_api(api_url_input, logger=print, start_page=None, end_page=Non
|
|||||||
|
|
||||||
if all_posts_for_manga_mode:
|
if all_posts_for_manga_mode:
|
||||||
logger(f" Manga Mode: Fetched {len(all_posts_for_manga_mode)} total posts. Sorting by publication date (oldest first)...")
|
logger(f" Manga Mode: Fetched {len(all_posts_for_manga_mode)} total posts. Sorting by publication date (oldest first)...")
|
||||||
|
# ... (rest of sorting and yielding logic for manga mode remains the same) ...
|
||||||
def sort_key_tuple(post):
|
def sort_key_tuple(post):
|
||||||
published_date_str = post.get('published')
|
published_date_str = post.get('published')
|
||||||
added_date_str = post.get('added')
|
added_date_str = post.get('added')
|
||||||
@@ -584,7 +605,8 @@ class PostProcessorWorker:
|
|||||||
selected_cookie_file=None, # Added missing parameter
|
selected_cookie_file=None, # Added missing parameter
|
||||||
app_base_dir=None, # New parameter for app's base directory
|
app_base_dir=None, # New parameter for app's base directory
|
||||||
manga_date_file_counter_ref=None, # New parameter for date-based manga naming
|
manga_date_file_counter_ref=None, # New parameter for date-based manga naming
|
||||||
):
|
manga_global_file_counter_ref=None, # New parameter for global numbering
|
||||||
|
): # type: ignore
|
||||||
self.post = post_data
|
self.post = post_data
|
||||||
self.download_root = download_root
|
self.download_root = download_root
|
||||||
self.known_names = known_names
|
self.known_names = known_names
|
||||||
@@ -630,6 +652,7 @@ class PostProcessorWorker:
|
|||||||
self.selected_cookie_file = selected_cookie_file # Store selected cookie file path
|
self.selected_cookie_file = selected_cookie_file # Store selected cookie file path
|
||||||
self.app_base_dir = app_base_dir # Store app base dir
|
self.app_base_dir = app_base_dir # Store app base dir
|
||||||
self.cookie_text = cookie_text # Store cookie text
|
self.cookie_text = cookie_text # Store cookie text
|
||||||
|
self.manga_global_file_counter_ref = manga_global_file_counter_ref # Store global counter
|
||||||
self.use_cookie = use_cookie # Store cookie setting
|
self.use_cookie = use_cookie # Store cookie setting
|
||||||
|
|
||||||
if self.compress_images and Image is None:
|
if self.compress_images and Image is None:
|
||||||
@@ -667,6 +690,7 @@ class PostProcessorWorker:
|
|||||||
post_title="", file_index_in_post=0, num_files_in_this_post=1,
|
post_title="", file_index_in_post=0, num_files_in_this_post=1,
|
||||||
manga_date_file_counter_ref=None): # Added manga_date_file_counter_ref
|
manga_date_file_counter_ref=None): # Added manga_date_file_counter_ref
|
||||||
was_original_name_kept_flag = False
|
was_original_name_kept_flag = False
|
||||||
|
manga_global_file_counter_ref = None # Placeholder, will be passed from process()
|
||||||
final_filename_saved_for_return = ""
|
final_filename_saved_for_return = ""
|
||||||
|
|
||||||
def _get_current_character_filters(self):
|
def _get_current_character_filters(self):
|
||||||
@@ -677,7 +701,8 @@ class PostProcessorWorker:
|
|||||||
def _download_single_file(self, file_info, target_folder_path, headers, original_post_id_for_log, skip_event,
|
def _download_single_file(self, file_info, target_folder_path, headers, original_post_id_for_log, skip_event,
|
||||||
post_title="", file_index_in_post=0, num_files_in_this_post=1, # Added manga_date_file_counter_ref
|
post_title="", file_index_in_post=0, num_files_in_this_post=1, # Added manga_date_file_counter_ref
|
||||||
manga_date_file_counter_ref=None,
|
manga_date_file_counter_ref=None,
|
||||||
forced_filename_override=None): # New for retries
|
forced_filename_override=None, # New for retries
|
||||||
|
manga_global_file_counter_ref=None): # New for global numbering
|
||||||
was_original_name_kept_flag = False
|
was_original_name_kept_flag = False
|
||||||
final_filename_saved_for_return = ""
|
final_filename_saved_for_return = ""
|
||||||
retry_later_details = None # For storing info if retryable failure
|
retry_later_details = None # For storing info if retryable failure
|
||||||
@@ -739,6 +764,19 @@ class PostProcessorWorker:
|
|||||||
self.logger(f"⚠️ Manga Date Mode: Counter ref not provided or malformed for '{api_original_filename}'. Using original. Ref: {manga_date_file_counter_ref}")
|
self.logger(f"⚠️ Manga Date Mode: Counter ref not provided or malformed for '{api_original_filename}'. Using original. Ref: {manga_date_file_counter_ref}")
|
||||||
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
||||||
self.logger(f"⚠️ Manga mode (Date Based Style Fallback): Using cleaned original filename '{filename_to_save_in_main_path}' for post {original_post_id_for_log}.")
|
self.logger(f"⚠️ Manga mode (Date Based Style Fallback): Using cleaned original filename '{filename_to_save_in_main_path}' for post {original_post_id_for_log}.")
|
||||||
|
elif self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING:
|
||||||
|
if manga_global_file_counter_ref is not None and len(manga_global_file_counter_ref) == 2:
|
||||||
|
counter_val_for_filename = -1
|
||||||
|
counter_lock = manga_global_file_counter_ref[1]
|
||||||
|
with counter_lock:
|
||||||
|
counter_val_for_filename = manga_global_file_counter_ref[0]
|
||||||
|
manga_global_file_counter_ref[0] += 1
|
||||||
|
|
||||||
|
cleaned_post_title_base_for_global = clean_filename(post_title.strip() if post_title and post_title.strip() else "post")
|
||||||
|
filename_to_save_in_main_path = f"{cleaned_post_title_base_for_global}_{counter_val_for_filename:03d}{original_ext}"
|
||||||
|
else:
|
||||||
|
self.logger(f"⚠️ Manga Title+GlobalNum Mode: Counter ref not provided or malformed for '{api_original_filename}'. Using original. Ref: {manga_global_file_counter_ref}")
|
||||||
|
self.logger(f"⚠️ Manga mode (Date Based Style Fallback): Using cleaned original filename '{filename_to_save_in_main_path}' for post {original_post_id_for_log}.")
|
||||||
else:
|
else:
|
||||||
self.logger(f"⚠️ Manga mode: Unknown filename style '{self.manga_filename_style}'. Defaulting to original filename for '{api_original_filename}'.")
|
self.logger(f"⚠️ Manga mode: Unknown filename style '{self.manga_filename_style}'. Defaulting to original filename for '{api_original_filename}'.")
|
||||||
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
||||||
@@ -1429,6 +1467,14 @@ class PostProcessorWorker:
|
|||||||
|
|
||||||
target_folder_path_for_this_file = current_path_for_file
|
target_folder_path_for_this_file = current_path_for_file
|
||||||
|
|
||||||
|
manga_date_counter_to_pass = None
|
||||||
|
manga_global_counter_to_pass = None
|
||||||
|
if self.manga_mode_active:
|
||||||
|
if self.manga_filename_style == STYLE_DATE_BASED:
|
||||||
|
manga_date_counter_to_pass = self.manga_date_file_counter_ref
|
||||||
|
elif self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING:
|
||||||
|
manga_global_counter_to_pass = self.manga_global_file_counter_ref if self.manga_global_file_counter_ref is not None else self.manga_date_file_counter_ref
|
||||||
|
|
||||||
futures_list.append(file_pool.submit(
|
futures_list.append(file_pool.submit(
|
||||||
self._download_single_file,
|
self._download_single_file,
|
||||||
file_info_to_dl,
|
file_info_to_dl,
|
||||||
@@ -1436,8 +1482,9 @@ class PostProcessorWorker:
|
|||||||
headers,
|
headers,
|
||||||
post_id,
|
post_id,
|
||||||
self.skip_current_file_flag,
|
self.skip_current_file_flag,
|
||||||
post_title=post_title, # Keyword argument
|
post_title=post_title,
|
||||||
manga_date_file_counter_ref=self.manga_date_file_counter_ref if self.manga_mode_active and self.manga_filename_style == STYLE_DATE_BASED else None,
|
manga_date_file_counter_ref=manga_date_counter_to_pass,
|
||||||
|
manga_global_file_counter_ref=manga_global_counter_to_pass,
|
||||||
file_index_in_post=file_idx, # Changed to keyword argument
|
file_index_in_post=file_idx, # Changed to keyword argument
|
||||||
num_files_in_this_post=num_files_in_this_post_for_naming # Changed to keyword argument
|
num_files_in_this_post=num_files_in_this_post_for_naming # Changed to keyword argument
|
||||||
))
|
))
|
||||||
@@ -1505,6 +1552,9 @@ class DownloadThread(QThread):
|
|||||||
selected_cookie_file=None, # New parameter for selected cookie file
|
selected_cookie_file=None, # New parameter for selected cookie file
|
||||||
app_base_dir=None, # New parameter
|
app_base_dir=None, # New parameter
|
||||||
manga_date_file_counter_ref=None, # New parameter
|
manga_date_file_counter_ref=None, # New parameter
|
||||||
|
manga_global_file_counter_ref=None, # New parameter for global numbering
|
||||||
|
use_cookie=False, # Added: Expected by main.py
|
||||||
|
cookie_text="", # Added: Expected by main.py
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.api_url_input = api_url_input
|
self.api_url_input = api_url_input
|
||||||
@@ -1553,6 +1603,7 @@ class DownloadThread(QThread):
|
|||||||
self.cookie_text = cookie_text # Store cookie text
|
self.cookie_text = cookie_text # Store cookie text
|
||||||
self.use_cookie = use_cookie # Store cookie setting
|
self.use_cookie = use_cookie # Store cookie setting
|
||||||
self.manga_date_file_counter_ref = manga_date_file_counter_ref # Store for passing to worker by DownloadThread
|
self.manga_date_file_counter_ref = manga_date_file_counter_ref # Store for passing to worker by DownloadThread
|
||||||
|
self.manga_global_file_counter_ref = manga_global_file_counter_ref # Store for global numbering
|
||||||
if self.compress_images and Image is None:
|
if self.compress_images and Image is None:
|
||||||
self.logger("⚠️ Image compression disabled: Pillow library not found (DownloadThread).")
|
self.logger("⚠️ Image compression disabled: Pillow library not found (DownloadThread).")
|
||||||
self.compress_images = False
|
self.compress_images = False
|
||||||
@@ -1591,8 +1642,8 @@ class DownloadThread(QThread):
|
|||||||
not self.extract_links_only and current_manga_date_file_counter_ref is None: # Check if it needs calculation
|
not self.extract_links_only and current_manga_date_file_counter_ref is None: # Check if it needs calculation
|
||||||
series_scan_dir = self.output_dir
|
series_scan_dir = self.output_dir
|
||||||
if self.use_subfolders:
|
if self.use_subfolders:
|
||||||
if self.filter_character_list_objects and self.filter_character_list_objects[0] and self.filter_character_list_objects[0].get("name"):
|
if self.filter_character_list_objects_initial and self.filter_character_list_objects_initial[0] and self.filter_character_list_objects_initial[0].get("name"):
|
||||||
series_folder_name = clean_folder_name(self.filter_character_list_objects[0]["name"])
|
series_folder_name = clean_folder_name(self.filter_character_list_objects_initial[0]["name"])
|
||||||
series_scan_dir = os.path.join(series_scan_dir, series_folder_name)
|
series_scan_dir = os.path.join(series_scan_dir, series_folder_name)
|
||||||
elif self.service and self.user_id:
|
elif self.service and self.user_id:
|
||||||
creator_based_folder_name = clean_folder_name(self.user_id)
|
creator_based_folder_name = clean_folder_name(self.user_id)
|
||||||
@@ -1605,9 +1656,16 @@ class DownloadThread(QThread):
|
|||||||
for filename_to_check in filenames_in_dir:
|
for filename_to_check in filenames_in_dir:
|
||||||
base_name_no_ext = os.path.splitext(filename_to_check)[0]
|
base_name_no_ext = os.path.splitext(filename_to_check)[0]
|
||||||
match = re.match(r"(\d{3,})", base_name_no_ext)
|
match = re.match(r"(\d{3,})", base_name_no_ext)
|
||||||
if match: highest_num = max(highest_num, int(match.group(1)))
|
if match: highest_num = max(highest_num, int(match.group(1))) # Corrected indentation
|
||||||
current_manga_date_file_counter_ref = [highest_num + 1, threading.Lock()]
|
current_manga_date_file_counter_ref = [highest_num + 1, threading.Lock()]
|
||||||
self.logger(f"ℹ️ [Thread] Manga Date Mode: Initialized counter at {current_manga_date_file_counter_ref[0]}.")
|
self.logger(f"ℹ️ [Thread] Manga Date Mode: Initialized counter at {current_manga_date_file_counter_ref[0]}.")
|
||||||
|
elif self.manga_mode_active and self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING and not self.extract_links_only and current_manga_date_file_counter_ref is None: # Use current_manga_date_file_counter_ref for STYLE_POST_TITLE_GLOBAL_NUMBERING as well
|
||||||
|
# For global numbering, we always start from 1 for the session unless a ref is passed.
|
||||||
|
# If you need to resume global numbering across sessions, similar scanning logic would be needed.
|
||||||
|
# For now, it starts at 1 per session if no ref is provided.
|
||||||
|
current_manga_date_file_counter_ref = [1, threading.Lock()] # Start global numbering at 1
|
||||||
|
self.logger(f"ℹ️ [Thread] Manga Title+GlobalNum Mode: Initialized counter at {current_manga_date_file_counter_ref[0]}.")
|
||||||
|
|
||||||
worker_signals_obj = PostProcessorSignals()
|
worker_signals_obj = PostProcessorSignals()
|
||||||
try:
|
try:
|
||||||
worker_signals_obj.progress_signal.connect(self.progress_signal)
|
worker_signals_obj.progress_signal.connect(self.progress_signal)
|
||||||
@@ -1674,6 +1732,7 @@ class DownloadThread(QThread):
|
|||||||
selected_cookie_file=self.selected_cookie_file, # Pass selected cookie file
|
selected_cookie_file=self.selected_cookie_file, # Pass selected cookie file
|
||||||
app_base_dir=self.app_base_dir, # Pass app_base_dir
|
app_base_dir=self.app_base_dir, # Pass app_base_dir
|
||||||
cookie_text=self.cookie_text, # Pass cookie text
|
cookie_text=self.cookie_text, # Pass cookie text
|
||||||
|
manga_global_file_counter_ref=self.manga_global_file_counter_ref, # Pass the ref
|
||||||
use_cookie=self.use_cookie, # Pass cookie setting to worker
|
use_cookie=self.use_cookie, # Pass cookie setting to worker
|
||||||
manga_date_file_counter_ref=current_manga_date_file_counter_ref, # Pass the calculated or passed-in ref
|
manga_date_file_counter_ref=current_manga_date_file_counter_ref, # Pass the calculated or passed-in ref
|
||||||
)
|
)
|
||||||
|
|||||||
694
main.py
694
main.py
@@ -16,7 +16,8 @@ from concurrent.futures import ThreadPoolExecutor, CancelledError, Future
|
|||||||
|
|
||||||
from PyQt5.QtGui import (
|
from PyQt5.QtGui import (
|
||||||
QIcon,
|
QIcon,
|
||||||
QIntValidator
|
QIntValidator,
|
||||||
|
QDesktopServices
|
||||||
)
|
)
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton,
|
QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton,
|
||||||
@@ -26,7 +27,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QFrame,
|
QFrame,
|
||||||
QAbstractButton
|
QAbstractButton
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex, QMutexLocker, QObject, QTimer, QSettings, QStandardPaths, QCoreApplication
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex, QMutexLocker, QObject, QTimer, QSettings, QStandardPaths, QCoreApplication, QUrl, QSize
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -53,20 +54,33 @@ try:
|
|||||||
CHAR_SCOPE_FILES, # Ensure this is imported
|
CHAR_SCOPE_FILES, # Ensure this is imported
|
||||||
CHAR_SCOPE_BOTH,
|
CHAR_SCOPE_BOTH,
|
||||||
CHAR_SCOPE_COMMENTS,
|
CHAR_SCOPE_COMMENTS,
|
||||||
FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER # Import the new status
|
FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER, # Import the new status
|
||||||
|
STYLE_POST_TITLE_GLOBAL_NUMBERING # Import new manga style
|
||||||
)
|
)
|
||||||
print("Successfully imported names from downloader_utils.")
|
print("Successfully imported names from downloader_utils.")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"--- IMPORT ERROR ---")
|
print(f"--- IMPORT ERROR ---")
|
||||||
print(f"Failed to import from 'downloader_utils.py': {e}")
|
print(f"Failed to import from 'downloader_utils.py': {e}")
|
||||||
|
print(f"--- Check downloader_utils.py for syntax errors or missing dependencies. ---")
|
||||||
KNOWN_NAMES = []
|
KNOWN_NAMES = []
|
||||||
PostProcessorSignals = QObject
|
|
||||||
PostProcessorWorker = object
|
PostProcessorWorker = object
|
||||||
|
# Create a mock PostProcessorSignals class with the expected signals
|
||||||
|
class _MockPostProcessorSignals(QObject):
|
||||||
|
progress_signal = pyqtSignal(str)
|
||||||
|
file_download_status_signal = pyqtSignal(bool)
|
||||||
|
external_link_signal = pyqtSignal(str, str, str, str)
|
||||||
|
file_progress_signal = pyqtSignal(str, object)
|
||||||
|
missed_character_post_signal = pyqtSignal(str, str)
|
||||||
|
# Add any other signals that might be expected if the real class is extended
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
print("WARNING: Using MOCK PostProcessorSignals due to import error from downloader_utils.py. Some functionalities might be impaired.")
|
||||||
|
PostProcessorSignals = _MockPostProcessorSignals # Use the mock class
|
||||||
BackendDownloadThread = QThread
|
BackendDownloadThread = QThread
|
||||||
def clean_folder_name(n): return str(n)
|
def clean_folder_name(n): return str(n)
|
||||||
def extract_post_info(u): return None, None, None
|
def extract_post_info(u): return None, None, None
|
||||||
def download_from_api(*a, **k): yield []
|
def download_from_api(*a, **k): yield []
|
||||||
SKIP_SCOPE_FILES = "files"
|
SKIP_SCOPE_FILES = "files" # type: ignore
|
||||||
SKIP_SCOPE_POSTS = "posts"
|
SKIP_SCOPE_POSTS = "posts"
|
||||||
SKIP_SCOPE_BOTH = "both"
|
SKIP_SCOPE_BOTH = "both"
|
||||||
CHAR_SCOPE_TITLE = "title"
|
CHAR_SCOPE_TITLE = "title"
|
||||||
@@ -74,6 +88,7 @@ except ImportError as e:
|
|||||||
CHAR_SCOPE_BOTH = "both"
|
CHAR_SCOPE_BOTH = "both"
|
||||||
CHAR_SCOPE_COMMENTS = "comments"
|
CHAR_SCOPE_COMMENTS = "comments"
|
||||||
FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER = "failed_retry_later"
|
FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER = "failed_retry_later"
|
||||||
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # Mock for safety
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"--- UNEXPECTED IMPORT ERROR ---")
|
print(f"--- UNEXPECTED IMPORT ERROR ---")
|
||||||
@@ -100,6 +115,7 @@ MANGA_FILENAME_STYLE_KEY = "mangaFilenameStyleV1"
|
|||||||
STYLE_POST_TITLE = "post_title"
|
STYLE_POST_TITLE = "post_title"
|
||||||
STYLE_ORIGINAL_NAME = "original_name"
|
STYLE_ORIGINAL_NAME = "original_name"
|
||||||
STYLE_DATE_BASED = "date_based" # New style for date-based naming
|
STYLE_DATE_BASED = "date_based" # New style for date-based naming
|
||||||
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = STYLE_POST_TITLE_GLOBAL_NUMBERING # Use imported or mocked
|
||||||
SKIP_WORDS_SCOPE_KEY = "skipWordsScopeV1"
|
SKIP_WORDS_SCOPE_KEY = "skipWordsScopeV1"
|
||||||
ALLOW_MULTIPART_DOWNLOAD_KEY = "allowMultipartDownloadV1"
|
ALLOW_MULTIPART_DOWNLOAD_KEY = "allowMultipartDownloadV1"
|
||||||
|
|
||||||
@@ -107,7 +123,6 @@ USE_COOKIE_KEY = "useCookieV1" # New setting key
|
|||||||
COOKIE_TEXT_KEY = "cookieTextV1" # New setting key for cookie text
|
COOKIE_TEXT_KEY = "cookieTextV1" # New setting key for cookie text
|
||||||
CHAR_FILTER_SCOPE_KEY = "charFilterScopeV1"
|
CHAR_FILTER_SCOPE_KEY = "charFilterScopeV1"
|
||||||
|
|
||||||
# Custom dialog result constants for ConfirmAddAllDialog
|
|
||||||
CONFIRM_ADD_ALL_ACCEPTED = 1
|
CONFIRM_ADD_ALL_ACCEPTED = 1
|
||||||
CONFIRM_ADD_ALL_SKIP_ADDING = 2
|
CONFIRM_ADD_ALL_SKIP_ADDING = 2
|
||||||
CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3
|
CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3
|
||||||
@@ -214,9 +229,148 @@ class ConfirmAddAllDialog(QDialog):
|
|||||||
super().exec_()
|
super().exec_()
|
||||||
# If user accepted but selected nothing, treat it as skipping addition
|
# If user accepted but selected nothing, treat it as skipping addition
|
||||||
if isinstance(self.user_choice, list) and not self.user_choice:
|
if isinstance(self.user_choice, list) and not self.user_choice:
|
||||||
QMessageBox.information(self, "No Selection", "No names were selected to be added. Skipping addition.")
|
# QMessageBox.information(self, "No Selection", "No names were selected to be added. Skipping addition.")
|
||||||
return CONFIRM_ADD_ALL_SKIP_ADDING
|
return CONFIRM_ADD_ALL_SKIP_ADDING
|
||||||
return self.user_choice
|
return self.user_choice
|
||||||
|
|
||||||
|
class HelpGuideDialog(QDialog):
|
||||||
|
"""A multi-page dialog for displaying the feature guide."""
|
||||||
|
def __init__(self, steps_data, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.current_step = 0
|
||||||
|
self.steps_data = steps_data # List of (title, content_html) tuples
|
||||||
|
|
||||||
|
self.setWindowTitle("Kemono Downloader - Feature Guide")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setFixedSize(650, 600) # Adjusted size for guide content
|
||||||
|
|
||||||
|
# Apply similar styling to TourDialog, or a distinct one if preferred
|
||||||
|
self.setStyleSheet(parent.get_dark_theme() if hasattr(parent, 'get_dark_theme') else """
|
||||||
|
QDialog { background-color: #2E2E2E; border: 1px solid #5A5A5A; }
|
||||||
|
QLabel { color: #E0E0E0; }
|
||||||
|
QPushButton { background-color: #555; color: #F0F0F0; border: 1px solid #6A6A6A; padding: 8px 15px; border-radius: 4px; min-height: 25px; font-size: 11pt; }
|
||||||
|
QPushButton:hover { background-color: #656565; }
|
||||||
|
QPushButton:pressed { background-color: #4A4A4A; }
|
||||||
|
""")
|
||||||
|
self._init_ui()
|
||||||
|
if parent: # Attempt to center on parent
|
||||||
|
self.move(parent.geometry().center() - self.rect().center())
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
main_layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.stacked_widget = QStackedWidget()
|
||||||
|
main_layout.addWidget(self.stacked_widget, 1)
|
||||||
|
|
||||||
|
self.tour_steps_widgets = [] # To hold TourStepWidget instances
|
||||||
|
for title, content in self.steps_data:
|
||||||
|
step_widget = TourStepWidget(title, content) # Reuse TourStepWidget
|
||||||
|
self.tour_steps_widgets.append(step_widget)
|
||||||
|
self.stacked_widget.addWidget(step_widget)
|
||||||
|
|
||||||
|
buttons_layout = QHBoxLayout()
|
||||||
|
buttons_layout.setContentsMargins(15, 10, 15, 15)
|
||||||
|
buttons_layout.setSpacing(10)
|
||||||
|
|
||||||
|
self.back_button = QPushButton("Back")
|
||||||
|
self.back_button.clicked.connect(self._previous_step)
|
||||||
|
self.back_button.setEnabled(False)
|
||||||
|
|
||||||
|
# Determine base directory for assets
|
||||||
|
# This logic assumes 'assest' folder is at the same level as main.py or the executable
|
||||||
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
# For PyInstaller, assets are in _MEIPASS or a relative path from executable
|
||||||
|
# If 'assest' is bundled at the root of _MEIPASS:
|
||||||
|
assets_base_dir = sys._MEIPASS
|
||||||
|
# If 'assest' is bundled relative to the executable directory:
|
||||||
|
# assets_base_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
# For development, assets are relative to the script
|
||||||
|
assets_base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
github_icon_path = os.path.join(assets_base_dir, "assets", "github.png")
|
||||||
|
instagram_icon_path = os.path.join(assets_base_dir, "assets", "instagram.png")
|
||||||
|
discord_icon_path = os.path.join(assets_base_dir, "assets", "discord.png")
|
||||||
|
|
||||||
|
self.github_button = QPushButton(QIcon(github_icon_path), "")
|
||||||
|
self.instagram_button = QPushButton(QIcon(instagram_icon_path), "")
|
||||||
|
self.Discord_button = QPushButton(QIcon(discord_icon_path), "")
|
||||||
|
|
||||||
|
# Optional: Set a fixed icon size for consistency
|
||||||
|
icon_size = QSize(24, 24) # Adjust as needed
|
||||||
|
self.github_button.setIconSize(icon_size)
|
||||||
|
self.instagram_button.setIconSize(icon_size)
|
||||||
|
self.Discord_button.setIconSize(icon_size)
|
||||||
|
|
||||||
|
self.next_button = QPushButton("Next")
|
||||||
|
self.next_button.clicked.connect(self._next_step_action)
|
||||||
|
self.next_button.setDefault(True)
|
||||||
|
self.github_button.clicked.connect(self._open_github_link)
|
||||||
|
self.instagram_button.clicked.connect(self._open_instagram_link)
|
||||||
|
self.Discord_button.clicked.connect(self._open_Discord_link)
|
||||||
|
self.github_button.setToolTip("Visit project's GitHub page (Opens in browser)")
|
||||||
|
self.instagram_button.setToolTip("Visit our Instagram page (Opens in browser)")
|
||||||
|
self.Discord_button.setToolTip("Visit our Discord community (Opens in browser)")
|
||||||
|
|
||||||
|
|
||||||
|
# Social media buttons layout
|
||||||
|
social_layout = QHBoxLayout()
|
||||||
|
social_layout.setSpacing(10)
|
||||||
|
social_layout.addWidget(self.github_button)
|
||||||
|
social_layout.addWidget(self.instagram_button)
|
||||||
|
social_layout.addWidget(self.Discord_button)
|
||||||
|
# social_layout.addStretch(1) # Pushes social buttons to the left if uncommented and placed before nav buttons
|
||||||
|
|
||||||
|
# Add social buttons to the main buttons_layout, before the stretch, to keep them left
|
||||||
|
# Clear buttons_layout and rebuild to ensure order
|
||||||
|
while buttons_layout.count():
|
||||||
|
item = buttons_layout.takeAt(0) # Removes the item from the layout
|
||||||
|
if item.widget(): # Check if the item is a widget
|
||||||
|
item.widget().setParent(None) # Detach the widget from this layout
|
||||||
|
elif item.layout(): # If it's a sub-layout
|
||||||
|
pass # Sub-layouts are handled by Qt's ownership or need explicit deletion if complex
|
||||||
|
buttons_layout.addLayout(social_layout) # Add social buttons on the left
|
||||||
|
buttons_layout.addStretch(1) # Stretch between social and nav buttons
|
||||||
|
buttons_layout.addWidget(self.back_button) # Back and Next on the right
|
||||||
|
buttons_layout.addWidget(self.next_button)
|
||||||
|
main_layout.addLayout(buttons_layout)
|
||||||
|
self._update_button_states() # Set initial button states
|
||||||
|
|
||||||
|
def _next_step_action(self):
|
||||||
|
if self.current_step < len(self.tour_steps_widgets) - 1:
|
||||||
|
self.current_step += 1
|
||||||
|
self.stacked_widget.setCurrentIndex(self.current_step)
|
||||||
|
else: # Last page
|
||||||
|
self.accept() # Close dialog
|
||||||
|
self._update_button_states()
|
||||||
|
|
||||||
|
def _previous_step(self):
|
||||||
|
if self.current_step > 0:
|
||||||
|
self.current_step -= 1
|
||||||
|
self.stacked_widget.setCurrentIndex(self.current_step)
|
||||||
|
self._update_button_states()
|
||||||
|
|
||||||
|
def _update_button_states(self):
|
||||||
|
if self.current_step == len(self.tour_steps_widgets) - 1:
|
||||||
|
self.next_button.setText("Finish")
|
||||||
|
else:
|
||||||
|
self.next_button.setText("Next")
|
||||||
|
self.back_button.setEnabled(self.current_step > 0)
|
||||||
|
|
||||||
|
def _open_github_link(self):
|
||||||
|
# Replace with your actual GitHub project URL
|
||||||
|
QDesktopServices.openUrl(QUrl("https://github.com/Yuvi9587"))
|
||||||
|
|
||||||
|
def _open_instagram_link(self):
|
||||||
|
# Replace with your actual Instagram URL
|
||||||
|
QDesktopServices.openUrl(QUrl("https://www.instagram.com/uvi.arts/"))
|
||||||
|
|
||||||
|
def _open_Discord_link(self):
|
||||||
|
# Replace with your actual Discord URL
|
||||||
|
QDesktopServices.openUrl(QUrl("https://discord.gg/BqP64XTdJN"))
|
||||||
|
|
||||||
class TourStepWidget(QWidget):
|
class TourStepWidget(QWidget):
|
||||||
"""A single step/page in the tour."""
|
"""A single step/page in the tour."""
|
||||||
def __init__(self, title_text, content_text, parent=None):
|
def __init__(self, title_text, content_text, parent=None):
|
||||||
@@ -703,9 +857,8 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
print(f"ℹ️ Known.txt will be loaded/saved at: {self.config_file}")
|
print(f"ℹ️ Known.txt will be loaded/saved at: {self.config_file}")
|
||||||
|
|
||||||
|
self.setWindowTitle("Kemono Downloader v4.0.0")
|
||||||
self.load_known_names_from_util()
|
# self.load_known_names_from_util() # This call is premature and causes the error.
|
||||||
self.setWindowTitle("Kemono Downloader v3.5.0")
|
|
||||||
self.setStyleSheet(self.get_dark_theme())
|
self.setStyleSheet(self.get_dark_theme())
|
||||||
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
@@ -725,8 +878,8 @@ class DownloaderApp(QWidget):
|
|||||||
def _get_tooltip_for_character_input(self):
|
def _get_tooltip_for_character_input(self):
|
||||||
return (
|
return (
|
||||||
"Names, comma-separated.\n"
|
"Names, comma-separated.\n"
|
||||||
"- Individual names: `Tifa`, `Aerith`\n"
|
"- Individual names: `Tifa`, `Aerith` (separate folders, separate Known.txt entries).\n"
|
||||||
"- Group for separate folders: `(Vivi, Ulti, Uta)` -> creates separate Known.txt entries & folders for Vivi, Ulti, Uta.\n"
|
"- Group for shared folder, separate Known.txt: `(Vivi, Ulti, Uta)` -> creates folder 'Vivi Ulti Uta', but adds Vivi, Ulti, Uta as separate Known.txt entries.\n"
|
||||||
"- Group for a single shared folder: `(Yuffie, Sonon)~` (note the `~`) -> creates one Known.txt entry for folder 'Yuffie Sonon', with Yuffie and Sonon as aliases.\n"
|
"- Group for a single shared folder: `(Yuffie, Sonon)~` (note the `~`) -> creates one Known.txt entry for folder 'Yuffie Sonon', with Yuffie and Sonon as aliases.\n"
|
||||||
"All names in any group type are used as aliases for matching content."
|
"All names in any group type are used as aliases for matching content."
|
||||||
)
|
)
|
||||||
@@ -838,11 +991,18 @@ class DownloaderApp(QWidget):
|
|||||||
group_content_str = part_str[1:-1].strip()
|
group_content_str = part_str[1:-1].strip()
|
||||||
aliases_in_group = [alias.strip() for alias in group_content_str.split(',') if alias.strip()]
|
aliases_in_group = [alias.strip() for alias in group_content_str.split(',') if alias.strip()]
|
||||||
if aliases_in_group:
|
if aliases_in_group:
|
||||||
# Create separate entries for each item in a non-tilde group
|
# For (A, B, C) type groups:
|
||||||
for alias_item in aliases_in_group:
|
# Create a single filter object for a shared folder in the current download.
|
||||||
parsed_character_filter_objects.append({"name": alias_item, "is_group": False, "aliases": [alias_item]})
|
# Mark with a special flag to handle Known.txt addition differently.
|
||||||
|
group_folder_name = " ".join(aliases_in_group) # Folder name from all aliases
|
||||||
|
parsed_character_filter_objects.append({
|
||||||
|
"name": group_folder_name,
|
||||||
|
"is_group": True, # Behaves like a tilde group for current download folder
|
||||||
|
"aliases": aliases_in_group,
|
||||||
|
"components_are_distinct_for_known_txt": True # New flag
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
parsed_character_filter_objects.append({"name": part_str, "is_group": False, "aliases": [part_str]}) # Standard single entry
|
parsed_character_filter_objects.append({"name": part_str, "is_group": False, "aliases": [part_str], "components_are_distinct_for_known_txt": False}) # Standard single entry
|
||||||
return parsed_character_filter_objects
|
return parsed_character_filter_objects
|
||||||
|
|
||||||
def _process_worker_queue(self):
|
def _process_worker_queue(self):
|
||||||
@@ -1363,8 +1523,18 @@ class DownloaderApp(QWidget):
|
|||||||
self.new_char_input.returnPressed.connect(self.add_char_button.click)
|
self.new_char_input.returnPressed.connect(self.add_char_button.click)
|
||||||
self.delete_char_button.clicked.connect(self.delete_selected_character)
|
self.delete_char_button.clicked.connect(self.delete_selected_character)
|
||||||
char_manage_layout.addWidget(self.new_char_input, 2)
|
char_manage_layout.addWidget(self.new_char_input, 2)
|
||||||
char_manage_layout.addWidget(self.add_char_button, 1)
|
char_manage_layout.addWidget(self.add_char_button, 0)
|
||||||
char_manage_layout.addWidget(self.delete_char_button, 1)
|
|
||||||
|
# Help button for Known Names list
|
||||||
|
self.known_names_help_button = QPushButton("?") # Restored question mark
|
||||||
|
self.known_names_help_button.setFixedWidth(35) # Small width for a square-like button
|
||||||
|
# self.known_names_help_button.setStyleSheet("font-weight: bold; padding-left: 8px; padding-right: 8px;") # Removed stylesheet
|
||||||
|
self.known_names_help_button.setToolTip("Open the application feature guide.")
|
||||||
|
self.known_names_help_button.clicked.connect(self._show_feature_guide)
|
||||||
|
|
||||||
|
|
||||||
|
char_manage_layout.addWidget(self.delete_char_button, 0)
|
||||||
|
char_manage_layout.addWidget(self.known_names_help_button, 0) # Moved to the end (rightmost)
|
||||||
left_layout.addLayout(char_manage_layout)
|
left_layout.addLayout(char_manage_layout)
|
||||||
left_layout.addStretch(0)
|
left_layout.addStretch(0)
|
||||||
|
|
||||||
@@ -1892,10 +2062,17 @@ class DownloaderApp(QWidget):
|
|||||||
self.log_signal.emit(f"="*20 + f" Mode changed to: {filter_mode_text} " + "="*20)
|
self.log_signal.emit(f"="*20 + f" Mode changed to: {filter_mode_text} " + "="*20)
|
||||||
|
|
||||||
subfolders_on = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False
|
subfolders_on = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False
|
||||||
|
|
||||||
manga_on = self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False
|
manga_on = self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False
|
||||||
|
|
||||||
enable_character_filter_related_widgets = file_download_mode_active and (subfolders_on or manga_on)
|
# Determine if character filter section should be active (visible and enabled)
|
||||||
|
# It should be active if we are in a file downloading mode (not 'Only Links' or 'Only Archives')
|
||||||
|
character_filter_should_be_active = not is_only_links and not is_only_archives
|
||||||
|
|
||||||
|
if self.character_filter_widget:
|
||||||
|
self.character_filter_widget.setVisible(character_filter_should_be_active)
|
||||||
|
|
||||||
|
# Enable/disable character input and its scope button based on whether character filtering is active
|
||||||
|
enable_character_filter_related_widgets = character_filter_should_be_active
|
||||||
|
|
||||||
if self.character_input:
|
if self.character_input:
|
||||||
self.character_input.setEnabled(enable_character_filter_related_widgets)
|
self.character_input.setEnabled(enable_character_filter_related_widgets)
|
||||||
@@ -1905,7 +2082,9 @@ class DownloaderApp(QWidget):
|
|||||||
if self.char_filter_scope_toggle_button:
|
if self.char_filter_scope_toggle_button:
|
||||||
self.char_filter_scope_toggle_button.setEnabled(enable_character_filter_related_widgets)
|
self.char_filter_scope_toggle_button.setEnabled(enable_character_filter_related_widgets)
|
||||||
|
|
||||||
self.update_ui_for_subfolders(subfolders_on)
|
# Call update_ui_for_subfolders to correctly set the "Subfolder per Post" checkbox state
|
||||||
|
# and "Custom Folder Name" visibility, which DO depend on the "Separate Folders" checkbox.
|
||||||
|
self.update_ui_for_subfolders(subfolders_on) # Pass the current state of the main subfolder checkbox
|
||||||
self.update_custom_folder_visibility()
|
self.update_custom_folder_visibility()
|
||||||
self.update_ui_for_manga_mode(self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False)
|
self.update_ui_for_manga_mode(self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False)
|
||||||
|
|
||||||
@@ -2125,16 +2304,53 @@ class DownloaderApp(QWidget):
|
|||||||
def _handle_ui_add_new_character(self):
|
def _handle_ui_add_new_character(self):
|
||||||
"""Handles adding a new character from the UI input field."""
|
"""Handles adding a new character from the UI input field."""
|
||||||
name_from_ui_input = self.new_char_input.text().strip()
|
name_from_ui_input = self.new_char_input.text().strip()
|
||||||
|
successfully_added_any = False
|
||||||
|
|
||||||
if not name_from_ui_input:
|
if not name_from_ui_input:
|
||||||
QMessageBox.warning(self, "Input Error", "Name cannot be empty.")
|
QMessageBox.warning(self, "Input Error", "Name cannot be empty.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# For UI additions, it's always a simple, non-group entry.
|
if name_from_ui_input.startswith("(") and name_from_ui_input.endswith(")~"):
|
||||||
# The special ( ) and ( )~ parsing is for the "Filter by Character(s)" field.
|
# Format: (Name1, Name2)~ -> Group "Name1 Name2" with aliases Name1, Name2
|
||||||
self.add_new_character(name_to_add=name_from_ui_input,
|
content = name_from_ui_input[1:-2].strip() # Remove ( and )~
|
||||||
|
aliases = [alias.strip() for alias in content.split(',') if alias.strip()]
|
||||||
|
if aliases:
|
||||||
|
folder_name = " ".join(aliases) # The primary name for the KNOWN_NAMES entry
|
||||||
|
if self.add_new_character(name_to_add=folder_name,
|
||||||
|
is_group_to_add=True,
|
||||||
|
aliases_to_add=aliases,
|
||||||
|
suppress_similarity_prompt=False):
|
||||||
|
successfully_added_any = True
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Input Error", "Empty group content for `~` format.")
|
||||||
|
|
||||||
|
elif name_from_ui_input.startswith("(") and name_from_ui_input.endswith(")"):
|
||||||
|
# Format: (Name1, Name2) -> Add Name1 and Name2 as separate entries
|
||||||
|
content = name_from_ui_input[1:-1].strip() # Remove ( and )
|
||||||
|
names_to_add_separately = [name.strip() for name in content.split(',') if name.strip()]
|
||||||
|
if names_to_add_separately:
|
||||||
|
for name_item in names_to_add_separately:
|
||||||
|
if self.add_new_character(name_to_add=name_item,
|
||||||
|
is_group_to_add=False,
|
||||||
|
aliases_to_add=[name_item],
|
||||||
|
suppress_similarity_prompt=False):
|
||||||
|
successfully_added_any = True
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Input Error", "Empty group content for standard group format.")
|
||||||
|
else:
|
||||||
|
# Simple name, add as a single non-group entry
|
||||||
|
if self.add_new_character(name_to_add=name_from_ui_input,
|
||||||
is_group_to_add=False,
|
is_group_to_add=False,
|
||||||
aliases_to_add=[name_from_ui_input],
|
aliases_to_add=[name_from_ui_input],
|
||||||
suppress_similarity_prompt=False) # UI adds one by one, so prompt is fine
|
suppress_similarity_prompt=False):
|
||||||
|
successfully_added_any = True
|
||||||
|
|
||||||
|
if successfully_added_any:
|
||||||
|
self.new_char_input.clear()
|
||||||
|
self.save_known_names()
|
||||||
|
# The add_new_character method itself handles logging success/failure of individual additions
|
||||||
|
# and updating the character_list widget.
|
||||||
|
|
||||||
|
|
||||||
def add_new_character(self, name_to_add, is_group_to_add, aliases_to_add, suppress_similarity_prompt=False):
|
def add_new_character(self, name_to_add, is_group_to_add, aliases_to_add, suppress_similarity_prompt=False):
|
||||||
global KNOWN_NAMES, clean_folder_name
|
global KNOWN_NAMES, clean_folder_name
|
||||||
@@ -2208,7 +2424,6 @@ class DownloaderApp(QWidget):
|
|||||||
log_msg_suffix = f" (as group with aliases: {', '.join(new_entry['aliases'])})" if is_group_to_add and len(new_entry['aliases']) > 1 else ""
|
log_msg_suffix = f" (as group with aliases: {', '.join(new_entry['aliases'])})" if is_group_to_add and len(new_entry['aliases']) > 1 else ""
|
||||||
self.log_signal.emit(f"✅ Added '{name_to_add}' to known names list{log_msg_suffix}.")
|
self.log_signal.emit(f"✅ Added '{name_to_add}' to known names list{log_msg_suffix}.")
|
||||||
self.new_char_input.clear()
|
self.new_char_input.clear()
|
||||||
self.save_known_names()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -2260,27 +2475,17 @@ class DownloaderApp(QWidget):
|
|||||||
if self.custom_folder_input: self.custom_folder_input.clear()
|
if self.custom_folder_input: self.custom_folder_input.clear()
|
||||||
|
|
||||||
|
|
||||||
def update_ui_for_subfolders(self, checked):
|
def update_ui_for_subfolders(self, separate_folders_by_name_title_checked: bool):
|
||||||
is_only_links = self.radio_only_links and self.radio_only_links.isChecked()
|
is_only_links = self.radio_only_links and self.radio_only_links.isChecked()
|
||||||
is_only_archives = self.radio_only_archives and self.radio_only_archives.isChecked()
|
is_only_archives = self.radio_only_archives and self.radio_only_archives.isChecked()
|
||||||
|
|
||||||
|
can_enable_subfolder_per_post_checkbox = not is_only_links and not is_only_archives
|
||||||
|
|
||||||
if self.use_subfolder_per_post_checkbox:
|
if self.use_subfolder_per_post_checkbox:
|
||||||
self.use_subfolder_per_post_checkbox.setEnabled(not is_only_links and not is_only_archives)
|
self.use_subfolder_per_post_checkbox.setEnabled(can_enable_subfolder_per_post_checkbox)
|
||||||
|
|
||||||
if hasattr(self, 'use_cookie_checkbox'):
|
if not can_enable_subfolder_per_post_checkbox:
|
||||||
self.use_cookie_checkbox.setEnabled(not is_only_links) # Cookies might be relevant for archives
|
self.use_subfolder_per_post_checkbox.setChecked(False)
|
||||||
|
|
||||||
|
|
||||||
enable_character_filter_related_widgets = checked and not is_only_links and not is_only_archives
|
|
||||||
|
|
||||||
|
|
||||||
if self.character_filter_widget:
|
|
||||||
self.character_filter_widget.setVisible(enable_character_filter_related_widgets)
|
|
||||||
if not self.character_filter_widget.isVisible():
|
|
||||||
if self.character_input: self.character_input.clear()
|
|
||||||
if self.char_filter_scope_toggle_button: self.char_filter_scope_toggle_button.setEnabled(False)
|
|
||||||
else:
|
|
||||||
if self.char_filter_scope_toggle_button: self.char_filter_scope_toggle_button.setEnabled(True)
|
|
||||||
|
|
||||||
self.update_custom_folder_visibility()
|
self.update_custom_folder_visibility()
|
||||||
|
|
||||||
@@ -2318,12 +2523,12 @@ class DownloaderApp(QWidget):
|
|||||||
_, _, post_id = extract_post_info(url_text)
|
_, _, post_id = extract_post_info(url_text)
|
||||||
|
|
||||||
is_creator_feed = not post_id if url_text else False
|
is_creator_feed = not post_id if url_text else False
|
||||||
manga_mode_active = self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False
|
# Manga mode no longer directly dictates page range enabled state.
|
||||||
|
# Page range is enabled if it's a creator feed.
|
||||||
enable_page_range = is_creator_feed and not manga_mode_active
|
enable_page_range = is_creator_feed
|
||||||
|
|
||||||
for widget in [self.page_range_label, self.start_page_input, self.to_label, self.end_page_input]:
|
for widget in [self.page_range_label, self.start_page_input, self.to_label, self.end_page_input]:
|
||||||
if widget: widget.setEnabled(enable_page_range)
|
if widget: widget.setEnabled(enable_page_range) # Enable/disable based on whether it's a creator feed
|
||||||
|
|
||||||
if not enable_page_range:
|
if not enable_page_range:
|
||||||
if self.start_page_input: self.start_page_input.clear()
|
if self.start_page_input: self.start_page_input.clear()
|
||||||
@@ -2356,6 +2561,18 @@ class DownloaderApp(QWidget):
|
|||||||
" Downloads as: \"001.jpg\", \"002.jpg\".\n\n"
|
" Downloads as: \"001.jpg\", \"002.jpg\".\n\n"
|
||||||
"Click to change to: Post Title"
|
"Click to change to: Post Title"
|
||||||
)
|
)
|
||||||
|
elif self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING:
|
||||||
|
self.manga_rename_toggle_button.setText("Name: Title+G.Num")
|
||||||
|
self.manga_rename_toggle_button.setToolTip(
|
||||||
|
"Manga Filename Style: Post Title + Global Numbering\n\n"
|
||||||
|
"When Manga/Comic Mode is active for a creator feed:\n"
|
||||||
|
"- All files across all posts in the current download session are named sequentially using the post's title as a prefix.\n"
|
||||||
|
"- Example: Post 'Chapter 1' (2 files) -> 'Chapter 1_001.jpg', 'Chapter 1_002.png'.\n"
|
||||||
|
" Next Post 'Chapter 2' (1 file) -> 'Chapter 2_003.jpg'.\n"
|
||||||
|
"- Multithreading for post processing is automatically disabled for this style.\n\n"
|
||||||
|
"Click to change to: Post Title"
|
||||||
|
)
|
||||||
|
|
||||||
elif self.manga_filename_style == STYLE_DATE_BASED:
|
elif self.manga_filename_style == STYLE_DATE_BASED:
|
||||||
self.manga_rename_toggle_button.setText("Name: Date Based")
|
self.manga_rename_toggle_button.setText("Name: Date Based")
|
||||||
self.manga_rename_toggle_button.setToolTip(
|
self.manga_rename_toggle_button.setToolTip(
|
||||||
@@ -2382,8 +2599,10 @@ class DownloaderApp(QWidget):
|
|||||||
if current_style == STYLE_POST_TITLE: # Title -> Original
|
if current_style == STYLE_POST_TITLE: # Title -> Original
|
||||||
new_style = STYLE_ORIGINAL_NAME
|
new_style = STYLE_ORIGINAL_NAME
|
||||||
elif current_style == STYLE_ORIGINAL_NAME: # Original -> Date
|
elif current_style == STYLE_ORIGINAL_NAME: # Original -> Date
|
||||||
|
new_style = STYLE_POST_TITLE_GLOBAL_NUMBERING # Original -> Title+GlobalNum
|
||||||
|
elif current_style == STYLE_POST_TITLE_GLOBAL_NUMBERING: # Title+GlobalNum -> Date Based
|
||||||
new_style = STYLE_DATE_BASED
|
new_style = STYLE_DATE_BASED
|
||||||
elif current_style == STYLE_DATE_BASED: # Date -> Title
|
elif current_style == STYLE_DATE_BASED: # Date Based -> Title
|
||||||
new_style = STYLE_POST_TITLE
|
new_style = STYLE_POST_TITLE
|
||||||
else:
|
else:
|
||||||
self.log_signal.emit(f"⚠️ Unknown current manga filename style: {current_style}. Resetting to default ('{STYLE_POST_TITLE}').")
|
self.log_signal.emit(f"⚠️ Unknown current manga filename style: {current_style}. Resetting to default ('{STYLE_POST_TITLE}').")
|
||||||
@@ -2416,24 +2635,20 @@ class DownloaderApp(QWidget):
|
|||||||
if self.manga_rename_toggle_button:
|
if self.manga_rename_toggle_button:
|
||||||
self.manga_rename_toggle_button.setVisible(manga_mode_effectively_on and not (is_only_links_mode or is_only_archives_mode))
|
self.manga_rename_toggle_button.setVisible(manga_mode_effectively_on and not (is_only_links_mode or is_only_archives_mode))
|
||||||
|
|
||||||
|
# Always update page range enabled state, as it depends on URL type, not directly manga mode.
|
||||||
if manga_mode_effectively_on:
|
|
||||||
if self.page_range_label: self.page_range_label.setEnabled(False)
|
|
||||||
if self.start_page_input: self.start_page_input.setEnabled(False); self.start_page_input.clear()
|
|
||||||
if self.to_label: self.to_label.setEnabled(False)
|
|
||||||
if self.end_page_input: self.end_page_input.setEnabled(False); self.end_page_input.clear()
|
|
||||||
else:
|
|
||||||
self.update_page_range_enabled_state()
|
self.update_page_range_enabled_state()
|
||||||
|
|
||||||
file_download_mode_active = not (self.radio_only_links and self.radio_only_links.isChecked())
|
file_download_mode_active = not (self.radio_only_links and self.radio_only_links.isChecked())
|
||||||
subfolders_on = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False
|
# Character filter widgets should be enabled if it's a file download mode
|
||||||
enable_char_filter_widgets = file_download_mode_active and (subfolders_on or manga_mode_effectively_on)
|
enable_char_filter_widgets = file_download_mode_active and not (self.radio_only_archives and self.radio_only_archives.isChecked())
|
||||||
|
|
||||||
if self.character_input:
|
if self.character_input:
|
||||||
self.character_input.setEnabled(enable_char_filter_widgets)
|
self.character_input.setEnabled(enable_char_filter_widgets)
|
||||||
if not enable_char_filter_widgets: self.character_input.clear()
|
if not enable_char_filter_widgets: self.character_input.clear()
|
||||||
if self.char_filter_scope_toggle_button:
|
if self.char_filter_scope_toggle_button:
|
||||||
self.char_filter_scope_toggle_button.setEnabled(enable_char_filter_widgets)
|
self.char_filter_scope_toggle_button.setEnabled(enable_char_filter_widgets)
|
||||||
|
if self.character_filter_widget: # Also ensure the main widget visibility is correct
|
||||||
|
self.character_filter_widget.setVisible(enable_char_filter_widgets)
|
||||||
self._update_multithreading_for_date_mode() # Update multithreading state based on manga mode
|
self._update_multithreading_for_date_mode() # Update multithreading state based on manga mode
|
||||||
|
|
||||||
|
|
||||||
@@ -2448,7 +2663,7 @@ class DownloaderApp(QWidget):
|
|||||||
if self.use_multithreading_checkbox.isChecked():
|
if self.use_multithreading_checkbox.isChecked():
|
||||||
try:
|
try:
|
||||||
num_threads_val = int(text)
|
num_threads_val = int(text)
|
||||||
if num_threads_val > 0 : self.use_multithreading_checkbox.setText(f"Use Multithreading ({num_threads_val} Threads)")
|
if num_threads_val > 0 : self.use_multithreading_checkbox.setText(f"Use Multithreading ({num_threads_val} Threads)") # type: ignore
|
||||||
else: self.use_multithreading_checkbox.setText("Use Multithreading (Invalid: >0)")
|
else: self.use_multithreading_checkbox.setText("Use Multithreading (Invalid: >0)")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.use_multithreading_checkbox.setText("Use Multithreading (Invalid Input)")
|
self.use_multithreading_checkbox.setText("Use Multithreading (Invalid Input)")
|
||||||
@@ -2474,10 +2689,12 @@ class DownloaderApp(QWidget):
|
|||||||
if not hasattr(self, 'manga_mode_checkbox') or not hasattr(self, 'use_multithreading_checkbox'):
|
if not hasattr(self, 'manga_mode_checkbox') or not hasattr(self, 'use_multithreading_checkbox'):
|
||||||
return # UI elements not ready
|
return # UI elements not ready
|
||||||
|
|
||||||
manga_on = self.manga_mode_checkbox.isChecked()
|
manga_on = self.manga_mode_checkbox.isChecked() # type: ignore
|
||||||
is_date_style = (self.manga_filename_style == STYLE_DATE_BASED)
|
is_sequential_style_requiring_single_thread = (
|
||||||
|
self.manga_filename_style == STYLE_DATE_BASED or
|
||||||
if manga_on and is_date_style:
|
self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING
|
||||||
|
)
|
||||||
|
if manga_on and is_sequential_style_requiring_single_thread:
|
||||||
if self.use_multithreading_checkbox.isChecked() or self.use_multithreading_checkbox.isEnabled():
|
if self.use_multithreading_checkbox.isChecked() or self.use_multithreading_checkbox.isEnabled():
|
||||||
if self.use_multithreading_checkbox.isChecked():
|
if self.use_multithreading_checkbox.isChecked():
|
||||||
self.log_signal.emit("ℹ️ Manga Date Mode: Multithreading for post processing has been disabled to ensure correct sequential file numbering.")
|
self.log_signal.emit("ℹ️ Manga Date Mode: Multithreading for post processing has been disabled to ensure correct sequential file numbering.")
|
||||||
@@ -2618,16 +2835,42 @@ class DownloaderApp(QWidget):
|
|||||||
start_page_str, end_page_str = self.start_page_input.text().strip(), self.end_page_input.text().strip()
|
start_page_str, end_page_str = self.start_page_input.text().strip(), self.end_page_input.text().strip()
|
||||||
start_page, end_page = None, None
|
start_page, end_page = None, None
|
||||||
is_creator_feed = bool(not post_id_from_url)
|
is_creator_feed = bool(not post_id_from_url)
|
||||||
if is_creator_feed and not manga_mode:
|
|
||||||
|
if is_creator_feed: # Page range is only relevant and parsed for creator feeds
|
||||||
try:
|
try:
|
||||||
if start_page_str: start_page = int(start_page_str)
|
if start_page_str: start_page = int(start_page_str)
|
||||||
if end_page_str: end_page = int(end_page_str)
|
if end_page_str: end_page = int(end_page_str)
|
||||||
|
|
||||||
|
# Validate parsed page numbers
|
||||||
if start_page is not None and start_page <= 0: raise ValueError("Start page must be positive.")
|
if start_page is not None and start_page <= 0: raise ValueError("Start page must be positive.")
|
||||||
if end_page is not None and end_page <= 0: raise ValueError("End page must be positive.")
|
if end_page is not None and end_page <= 0: raise ValueError("End page must be positive.")
|
||||||
if start_page and end_page and start_page > end_page: raise ValueError("Start page cannot be greater than end page.")
|
if start_page and end_page and start_page > end_page: raise ValueError("Start page cannot be greater than end page.")
|
||||||
except ValueError as e: QMessageBox.critical(self, "Page Range Error", f"Invalid page range: {e}"); return
|
|
||||||
elif manga_mode:
|
# If it's a creator feed, and manga mode is on, and both page fields were filled, show warning
|
||||||
start_page, end_page = None, None
|
if manga_mode and start_page and end_page:
|
||||||
|
msg_box = QMessageBox(self)
|
||||||
|
msg_box.setIcon(QMessageBox.Warning)
|
||||||
|
msg_box.setWindowTitle("Manga Mode & Page Range Warning")
|
||||||
|
msg_box.setText(
|
||||||
|
"You have enabled <b>Manga/Comic Mode</b> and also specified a <b>Page Range</b>.\n\n"
|
||||||
|
"Manga Mode processes posts from oldest to newest across all available pages by default.\n"
|
||||||
|
"If you use a page range, you might miss parts of the manga/comic if it starts before your 'Start Page' or continues after your 'End Page'.\n\n"
|
||||||
|
"However, if you are certain the content you want is entirely within this page range (e.g., a short series, or you know the specific pages for a volume), then proceeding is okay.\n\n"
|
||||||
|
"Do you want to proceed with this page range in Manga Mode?"
|
||||||
|
)
|
||||||
|
proceed_button = msg_box.addButton("Proceed Anyway", QMessageBox.AcceptRole)
|
||||||
|
cancel_button = msg_box.addButton("Cancel Download", QMessageBox.RejectRole)
|
||||||
|
msg_box.setDefaultButton(proceed_button)
|
||||||
|
msg_box.setEscapeButton(cancel_button)
|
||||||
|
msg_box.exec_()
|
||||||
|
|
||||||
|
if msg_box.clickedButton() == cancel_button:
|
||||||
|
self.log_signal.emit("❌ Download cancelled by user due to Manga Mode & Page Range warning.")
|
||||||
|
self.set_ui_enabled(True); return # Re-enable UI and stop
|
||||||
|
except ValueError as e:
|
||||||
|
QMessageBox.critical(self, "Page Range Error", f"Invalid page range: {e}")
|
||||||
|
self.set_ui_enabled(True); return # Re-enable UI and stop
|
||||||
|
# If not a creator_feed, start_page and end_page remain None.
|
||||||
self.external_link_queue.clear(); self.extracted_links_cache = []; self._is_processing_external_link_queue = False; self._current_link_post_title = None
|
self.external_link_queue.clear(); self.extracted_links_cache = []; self._is_processing_external_link_queue = False; self._current_link_post_title = None
|
||||||
|
|
||||||
raw_character_filters_text = self.character_input.text().strip() # Get current text
|
raw_character_filters_text = self.character_input.text().strip() # Get current text
|
||||||
@@ -2688,7 +2931,22 @@ class DownloaderApp(QWidget):
|
|||||||
elif isinstance(dialog_result, list): # User chose to add selected items
|
elif isinstance(dialog_result, list): # User chose to add selected items
|
||||||
if dialog_result: # If the list of selected filter_objects is not empty
|
if dialog_result: # If the list of selected filter_objects is not empty
|
||||||
self.log_signal.emit(f"ℹ️ User chose to add {len(dialog_result)} new entry/entries to Known.txt.")
|
self.log_signal.emit(f"ℹ️ User chose to add {len(dialog_result)} new entry/entries to Known.txt.")
|
||||||
for filter_obj_to_add in dialog_result: # dialog_result is the list of selected filter_obj
|
for filter_obj_to_add in dialog_result: # dialog_result is the list of selected filter_obj from ConfirmAddAllDialog
|
||||||
|
if filter_obj_to_add.get("components_are_distinct_for_known_txt"):
|
||||||
|
# This was a (A, B, C) group. Add A, B, C separately to Known.txt.
|
||||||
|
# The dialog presented the group name (e.g., "Power Reze Himeno") for selection.
|
||||||
|
# Now, we iterate its components (aliases) for individual Known.txt addition.
|
||||||
|
self.log_signal.emit(f" Processing group '{filter_obj_to_add['name']}' to add its components individually to Known.txt.")
|
||||||
|
for alias_component in filter_obj_to_add["aliases"]:
|
||||||
|
self.add_new_character(
|
||||||
|
name_to_add=alias_component,
|
||||||
|
is_group_to_add=False, # Add as individual non-group entry
|
||||||
|
aliases_to_add=[alias_component], # Alias is itself
|
||||||
|
suppress_similarity_prompt=True # Suppress for batch adding
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# This is a tilde group (A,B,C)~ or a simple name "Tifa"
|
||||||
|
# Add to Known.txt as is (either a group or a simple name).
|
||||||
self.add_new_character(
|
self.add_new_character(
|
||||||
name_to_add=filter_obj_to_add["name"],
|
name_to_add=filter_obj_to_add["name"],
|
||||||
is_group_to_add=filter_obj_to_add["is_group"],
|
is_group_to_add=filter_obj_to_add["is_group"],
|
||||||
@@ -2747,17 +3005,27 @@ class DownloaderApp(QWidget):
|
|||||||
self.retryable_failed_files_info.clear() # Clear previous retryable failures before new session
|
self.retryable_failed_files_info.clear() # Clear previous retryable failures before new session
|
||||||
manga_date_file_counter_ref_for_thread = None
|
manga_date_file_counter_ref_for_thread = None
|
||||||
if manga_mode and self.manga_filename_style == STYLE_DATE_BASED and not extract_links_only:
|
if manga_mode and self.manga_filename_style == STYLE_DATE_BASED and not extract_links_only:
|
||||||
manga_date_file_counter_ref_for_thread = None
|
# Initialization for STYLE_DATE_BASED (scanning existing files) happens in DownloadThread.run
|
||||||
|
manga_date_file_counter_ref_for_thread = None # Placeholder, actual init in thread
|
||||||
self.log_signal.emit(f"ℹ️ Manga Date Mode: File counter will be initialized by the download thread.")
|
self.log_signal.emit(f"ℹ️ Manga Date Mode: File counter will be initialized by the download thread.")
|
||||||
|
|
||||||
|
manga_global_file_counter_ref_for_thread = None
|
||||||
|
if manga_mode and self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING and not extract_links_only:
|
||||||
|
manga_global_file_counter_ref_for_thread = None # Placeholder, actual init in thread
|
||||||
|
self.log_signal.emit(f"ℹ️ Manga Title+GlobalNum Mode: File counter will be initialized by the download thread (starts at 1).")
|
||||||
|
|
||||||
effective_num_post_workers = 1
|
effective_num_post_workers = 1
|
||||||
|
|
||||||
effective_num_file_threads_per_worker = 1 # Default to 1 for all cases initially
|
effective_num_file_threads_per_worker = 1 # Default to 1 for all cases initially
|
||||||
|
|
||||||
if post_id_from_url:
|
if post_id_from_url:
|
||||||
if use_multithreading_enabled_by_checkbox:
|
if use_multithreading_enabled_by_checkbox:
|
||||||
effective_num_file_threads_per_worker = max(1, min(num_threads_from_gui, MAX_FILE_THREADS_PER_POST_OR_WORKER))
|
effective_num_file_threads_per_worker = max(1, min(num_threads_from_gui, MAX_FILE_THREADS_PER_POST_OR_WORKER))
|
||||||
else:
|
else: # This is the outer else block
|
||||||
if manga_mode and self.manga_filename_style == STYLE_DATE_BASED:
|
if manga_mode and self.manga_filename_style == STYLE_DATE_BASED:
|
||||||
effective_num_post_workers = 1
|
effective_num_post_workers = 1
|
||||||
|
elif manga_mode and self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING: # Correctly indented elif
|
||||||
|
effective_num_post_workers = 1
|
||||||
effective_num_file_threads_per_worker = 1 # Files are sequential for this worker too
|
effective_num_file_threads_per_worker = 1 # Files are sequential for this worker too
|
||||||
elif use_multithreading_enabled_by_checkbox: # Standard creator feed with multithreading enabled
|
elif use_multithreading_enabled_by_checkbox: # Standard creator feed with multithreading enabled
|
||||||
effective_num_post_workers = max(1, min(num_threads_from_gui, MAX_THREADS)) # For posts
|
effective_num_post_workers = max(1, min(num_threads_from_gui, MAX_THREADS)) # For posts
|
||||||
@@ -2773,12 +3041,14 @@ class DownloaderApp(QWidget):
|
|||||||
log_messages.append(f" Mode: Creator Feed")
|
log_messages.append(f" Mode: Creator Feed")
|
||||||
log_messages.append(f" Post Processing: {'Multi-threaded (' + str(effective_num_post_workers) + ' workers)' if effective_num_post_workers > 1 else 'Single-threaded (1 worker)'}")
|
log_messages.append(f" Post Processing: {'Multi-threaded (' + str(effective_num_post_workers) + ' workers)' if effective_num_post_workers > 1 else 'Single-threaded (1 worker)'}")
|
||||||
log_messages.append(f" ↳ File Downloads per Worker: Up to {effective_num_file_threads_per_worker} concurrent file(s)")
|
log_messages.append(f" ↳ File Downloads per Worker: Up to {effective_num_file_threads_per_worker} concurrent file(s)")
|
||||||
if is_creator_feed:
|
# Logging for page range (applies if is_creator_feed is true)
|
||||||
if manga_mode: log_messages.append(" Page Range: All (Manga Mode - Oldest Posts Processed First)")
|
|
||||||
else:
|
|
||||||
pr_log = "All"
|
pr_log = "All"
|
||||||
if start_page or end_page:
|
if start_page or end_page: # Construct pr_log if start_page or end_page have values
|
||||||
pr_log = f"{f'From {start_page} ' if start_page else ''}{'to ' if start_page and end_page else ''}{f'{end_page}' if end_page else (f'Up to {end_page}' if end_page else (f'From {start_page}' if start_page else 'Specific Range'))}".strip()
|
pr_log = f"{f'From {start_page} ' if start_page else ''}{'to ' if start_page and end_page else ''}{f'{end_page}' if end_page else (f'Up to {end_page}' if end_page else (f'From {start_page}' if start_page else 'Specific Range'))}".strip()
|
||||||
|
|
||||||
|
if manga_mode:
|
||||||
|
log_messages.append(f" Page Range: {pr_log if pr_log else 'All'} (Manga Mode - Oldest Posts Processed First within range)")
|
||||||
|
else: # Not manga mode, but still a creator feed
|
||||||
log_messages.append(f" Page Range: {pr_log if pr_log else 'All'}")
|
log_messages.append(f" Page Range: {pr_log if pr_log else 'All'}")
|
||||||
|
|
||||||
|
|
||||||
@@ -2820,8 +3090,9 @@ class DownloaderApp(QWidget):
|
|||||||
elif use_cookie_from_checkbox and selected_cookie_file_path_for_backend:
|
elif use_cookie_from_checkbox and selected_cookie_file_path_for_backend:
|
||||||
log_messages.append(f" ↳ Cookie File Selected: {os.path.basename(selected_cookie_file_path_for_backend)}")
|
log_messages.append(f" ↳ Cookie File Selected: {os.path.basename(selected_cookie_file_path_for_backend)}")
|
||||||
should_use_multithreading_for_posts = use_multithreading_enabled_by_checkbox and not post_id_from_url
|
should_use_multithreading_for_posts = use_multithreading_enabled_by_checkbox and not post_id_from_url
|
||||||
if manga_mode and self.manga_filename_style == STYLE_DATE_BASED and not post_id_from_url:
|
if manga_mode and (self.manga_filename_style == STYLE_DATE_BASED or self.manga_filename_style == STYLE_POST_TITLE_GLOBAL_NUMBERING) and not post_id_from_url:
|
||||||
log_messages.append(f" Threading: Single-threaded (posts) - Enforced by Manga Date Mode")
|
enforced_by_style = "Date Mode" if self.manga_filename_style == STYLE_DATE_BASED else "Title+GlobalNum Mode"
|
||||||
|
log_messages.append(f" Threading: Single-threaded (posts) - Enforced by Manga {enforced_by_style}")
|
||||||
should_use_multithreading_for_posts = False # Ensure this reflects the forced state
|
should_use_multithreading_for_posts = False # Ensure this reflects the forced state
|
||||||
else:
|
else:
|
||||||
log_messages.append(f" Threading: {'Multi-threaded (posts)' if should_use_multithreading_for_posts else 'Single-threaded (posts)'}")
|
log_messages.append(f" Threading: {'Multi-threaded (posts)' if should_use_multithreading_for_posts else 'Single-threaded (posts)'}")
|
||||||
@@ -2875,6 +3146,7 @@ class DownloaderApp(QWidget):
|
|||||||
'allow_multipart_download': allow_multipart,
|
'allow_multipart_download': allow_multipart,
|
||||||
'cookie_text': cookie_text_from_input, # Pass cookie text
|
'cookie_text': cookie_text_from_input, # Pass cookie text
|
||||||
'selected_cookie_file': selected_cookie_file_path_for_backend, # Pass selected cookie file
|
'selected_cookie_file': selected_cookie_file_path_for_backend, # Pass selected cookie file
|
||||||
|
'manga_global_file_counter_ref': manga_global_file_counter_ref_for_thread, # Pass new counter
|
||||||
'app_base_dir': app_base_dir_for_cookies, # Pass app base dir
|
'app_base_dir': app_base_dir_for_cookies, # Pass app base dir
|
||||||
'use_cookie': use_cookie_from_checkbox, # Pass cookie setting
|
'use_cookie': use_cookie_from_checkbox, # Pass cookie setting
|
||||||
}
|
}
|
||||||
@@ -2896,7 +3168,8 @@ class DownloaderApp(QWidget):
|
|||||||
'skip_words_list', 'skip_words_scope', 'char_filter_scope',
|
'skip_words_list', 'skip_words_scope', 'char_filter_scope',
|
||||||
'show_external_links', 'extract_links_only', 'num_file_threads_for_worker',
|
'show_external_links', 'extract_links_only', 'num_file_threads_for_worker',
|
||||||
'start_page', 'end_page', 'target_post_id_from_initial_url',
|
'start_page', 'end_page', 'target_post_id_from_initial_url',
|
||||||
'manga_date_file_counter_ref', # Ensure this is passed for single thread mode
|
'manga_date_file_counter_ref',
|
||||||
|
'manga_global_file_counter_ref', # Pass new counter for single thread mode
|
||||||
'manga_mode_active', 'unwanted_keywords', 'manga_filename_style',
|
'manga_mode_active', 'unwanted_keywords', 'manga_filename_style',
|
||||||
'allow_multipart_download', 'use_cookie', 'cookie_text', 'app_base_dir', 'selected_cookie_file' # Added selected_cookie_file
|
'allow_multipart_download', 'use_cookie', 'cookie_text', 'app_base_dir', 'selected_cookie_file' # Added selected_cookie_file
|
||||||
]
|
]
|
||||||
@@ -3093,7 +3366,8 @@ class DownloaderApp(QWidget):
|
|||||||
'skip_words_list', 'skip_words_scope', 'char_filter_scope',
|
'skip_words_list', 'skip_words_scope', 'char_filter_scope',
|
||||||
'show_external_links', 'extract_links_only', 'allow_multipart_download', 'use_cookie', 'cookie_text', 'app_base_dir', 'selected_cookie_file', # Added selected_cookie_file
|
'show_external_links', 'extract_links_only', 'allow_multipart_download', 'use_cookie', 'cookie_text', 'app_base_dir', 'selected_cookie_file', # Added selected_cookie_file
|
||||||
'num_file_threads', 'skip_current_file_flag', 'manga_date_file_counter_ref',
|
'num_file_threads', 'skip_current_file_flag', 'manga_date_file_counter_ref',
|
||||||
'manga_mode_active', 'manga_filename_style'
|
'manga_mode_active', 'manga_filename_style',
|
||||||
|
'manga_global_file_counter_ref' # Add new counter here
|
||||||
]
|
]
|
||||||
ppw_optional_keys_with_defaults = {
|
ppw_optional_keys_with_defaults = {
|
||||||
'skip_words_list', 'skip_words_scope', 'char_filter_scope', 'remove_from_filename_words_list',
|
'skip_words_list', 'skip_words_scope', 'char_filter_scope', 'remove_from_filename_words_list',
|
||||||
@@ -3101,6 +3375,8 @@ class DownloaderApp(QWidget):
|
|||||||
'num_file_threads', 'skip_current_file_flag', 'manga_mode_active', 'manga_filename_style',
|
'num_file_threads', 'skip_current_file_flag', 'manga_mode_active', 'manga_filename_style',
|
||||||
'manga_date_file_counter_ref', 'use_cookie', 'cookie_text', 'app_base_dir', 'selected_cookie_file' # Added selected_cookie_file
|
'manga_date_file_counter_ref', 'use_cookie', 'cookie_text', 'app_base_dir', 'selected_cookie_file' # Added selected_cookie_file
|
||||||
}
|
}
|
||||||
|
# Batching is generally for high worker counts.
|
||||||
|
# If num_post_workers is low (e.g., 1), the num_post_workers > POST_WORKER_BATCH_THRESHOLD condition will prevent batching.
|
||||||
if num_post_workers > POST_WORKER_BATCH_THRESHOLD and self.total_posts_to_process > POST_WORKER_NUM_BATCHES :
|
if num_post_workers > POST_WORKER_BATCH_THRESHOLD and self.total_posts_to_process > POST_WORKER_NUM_BATCHES :
|
||||||
self.log_signal.emit(f" High thread count ({num_post_workers}) detected. Batching post submissions into {POST_WORKER_NUM_BATCHES} parts.")
|
self.log_signal.emit(f" High thread count ({num_post_workers}) detected. Batching post submissions into {POST_WORKER_NUM_BATCHES} parts.")
|
||||||
|
|
||||||
@@ -3719,6 +3995,261 @@ class DownloaderApp(QWidget):
|
|||||||
self._update_manga_filename_style_button_text()
|
self._update_manga_filename_style_button_text()
|
||||||
self.update_ui_for_manga_mode(False)
|
self.update_ui_for_manga_mode(False)
|
||||||
|
|
||||||
|
def _show_feature_guide(self):
|
||||||
|
# Define content for each page
|
||||||
|
page1_title = "① Introduction & Main Inputs"
|
||||||
|
page1_content = """<html><head/><body>
|
||||||
|
<p>This guide provides an overview of the Kemono Downloader's features, fields, and buttons.</p>
|
||||||
|
|
||||||
|
<h3>Main Input Area (Top Left)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>🔗 Kemono Creator/Post URL:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Enter the full web address of a creator's page (e.g., <i>https://kemono.su/patreon/user/12345</i>) or a specific post (e.g., <i>.../post/98765</i>).</li>
|
||||||
|
<li>Supports Kemono (kemono.su, kemono.party) and Coomer (coomer.su, coomer.party) URLs.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Page Range (Start to End):</b>
|
||||||
|
<ul>
|
||||||
|
<li>For creator URLs: Specify a range of pages to fetch (e.g., pages 2 to 5). Leave blank for all pages.</li>
|
||||||
|
<li>Disabled for single post URLs or when <b>Manga/Comic Mode</b> is active.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>📁 Download Location:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Click <b>'Browse...'</b> to choose a main folder on your computer where all downloaded files will be saved.</li>
|
||||||
|
<li>This field is required unless you are using <b>'🔗 Only Links'</b> mode.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul></body></html>"""
|
||||||
|
|
||||||
|
page2_title = "② Filtering Downloads"
|
||||||
|
page2_content = """<html><head/><body>
|
||||||
|
<h3>Filtering Downloads (Left Panel)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>🎯 Filter by Character(s):</b>
|
||||||
|
<ul>
|
||||||
|
<li>Enter names, comma-separated (e.g., <code>Tifa, Aerith</code>).</li>
|
||||||
|
<li><b>Grouped Aliases for Shared Folder (Separate Known.txt Entries):</b> <code>(Vivi, Ulti, Uta)</code>.
|
||||||
|
<ul><li>Content matching "Vivi", "Ulti", OR "Uta" will go into a shared folder named "Vivi Ulti Uta" (after cleaning).</li>
|
||||||
|
<li>If these names are new, "Vivi", "Ulti", and "Uta" will be prompted to be added as <i>separate individual entries</i> to <code>Known.txt</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Grouped Aliases for Shared Folder (Single Known.txt Entry):</b> <code>(Yuffie, Sonon)~</code> (note the tilde <code>~</code>).
|
||||||
|
<ul><li>Content matching "Yuffie" OR "Sonon" will go into a shared folder named "Yuffie Sonon".</li>
|
||||||
|
<li>If new, "Yuffie Sonon" (with aliases Yuffie, Sonon) will be prompted to be added as a <i>single group entry</i> to <code>Known.txt</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>This filter influences folder naming if 'Separate Folders by Name/Title' is enabled.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Filter: [Type] Button (Character Filter Scope):</b> Cycles how the 'Filter by Character(s)' applies:
|
||||||
|
<ul>
|
||||||
|
<li><code>Filter: Files</code>: Checks individual filenames. A post is kept if any file matches; only matching files are downloaded. Folder naming uses the character from the matching filename.</li>
|
||||||
|
<li><code>Filter: Title</code>: Checks post titles. All files from a matching post are downloaded. Folder naming uses the character from the matching post title.</li>
|
||||||
|
<li><code>Filter: Both</code>: Checks post title first. If it matches, all files are downloaded. If not, it then checks filenames, and only matching files are downloaded. Folder naming prioritizes title match, then file match.</li>
|
||||||
|
<li><code>Filter: Comments (Beta)</code>: Checks filenames first. If a file matches, all files from the post are downloaded. If no file match, it then checks post comments. If a comment matches, all files are downloaded. (Uses more API requests). Folder naming prioritizes file match, then comment match.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>🗄️ Custom Folder Name (Single Post Only):</b>
|
||||||
|
<ul>
|
||||||
|
<li>Visible and usable only when downloading a single specific post URL AND 'Separate Folders by Name/Title' is enabled.</li>
|
||||||
|
<li>Allows you to specify a custom name for that single post's download folder.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>🚫 Skip with Words:</b>
|
||||||
|
<ul><li>Enter words, comma-separated (e.g., <code>WIP, sketch, preview</code>) to skip certain content.</li></ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Scope: [Type] Button (Skip Words Scope):</b> Cycles how 'Skip with Words' applies:
|
||||||
|
<ul>
|
||||||
|
<li><code>Scope: Files</code>: Skips individual files if their names contain any of these words.</li>
|
||||||
|
<li><code>Scope: Posts</code>: Skips entire posts if their titles contain any of these words.</li>
|
||||||
|
<li><code>Scope: Both</code>: Applies both (post title first, then individual files).</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>✂️ Remove Words from name:</b>
|
||||||
|
<ul><li>Enter words, comma-separated (e.g., <code>patreon, [HD]</code>), to remove from downloaded filenames (case-insensitive).</li></ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Filter Files (Radio Buttons):</b> Choose what to download:
|
||||||
|
<ul>
|
||||||
|
<li><code>All</code>: Downloads all file types found.</li>
|
||||||
|
<li><code>Images/GIFs</code>: Only common image formats (JPG, PNG, GIF, WEBP, etc.) and GIFs.</li>
|
||||||
|
<li><code>Videos</code>: Only common video formats (MP4, MKV, WEBM, MOV, etc.).</li>
|
||||||
|
<li><code>📦 Only Archives</code>: Exclusively downloads <b>.zip</b> and <b>.rar</b> files. When selected, 'Skip .zip' and 'Skip .rar' checkboxes are automatically disabled and unchecked. 'Show External Links' is also disabled.</li>
|
||||||
|
<li><code>🔗 Only Links</code>: Extracts and displays external links from post descriptions instead of downloading files. Download-related options and 'Show External Links' are disabled. The main download button changes to '🔗 Extract Links'.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul></body></html>"""
|
||||||
|
|
||||||
|
page3_title = "③ Download Options & Settings"
|
||||||
|
page3_content = """<html><head/><body>
|
||||||
|
<h3>Download Options & Settings (Left Panel)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>Skip .zip / Skip .rar:</b> Checkboxes to avoid downloading these archive file types. (Disabled and ignored if '📦 Only Archives' filter mode is selected).</li>
|
||||||
|
<li><b>Download Thumbnails Only:</b> Downloads small preview images instead of full-sized files (if available).</li>
|
||||||
|
<li><b>Compress Large Images (to WebP):</b> If the 'Pillow' (PIL) library is installed, images larger than 1.5MB will be converted to WebP format if the WebP version is significantly smaller.</li>
|
||||||
|
<li><b>⚙️ Advanced Settings:</b>
|
||||||
|
<ul>
|
||||||
|
<li><b>Separate Folders by Name/Title:</b> Creates subfolders based on the 'Filter by Character(s)' input or post titles. Can use the <b>Known.txt</b> list as a fallback for folder names.</li></ul></li></ul></body></html>"""
|
||||||
|
|
||||||
|
page4_title = "④ Advanced Settings (Part 1)"
|
||||||
|
page4_content = """<html><head/><body><h3>⚙️ Advanced Settings (Continued)</h3><ul><ul>
|
||||||
|
<!-- Continuing from previous page's ul for Advanced Settings -->
|
||||||
|
<li><b>Subfolder per Post:</b> If 'Separate Folders' is on, this creates an additional subfolder for <i>each individual post</i> inside the main character/title folder.</li>
|
||||||
|
<li><b>Use Cookie:</b> Check this to use cookies for requests.
|
||||||
|
<ul>
|
||||||
|
<li><b>Text Field:</b> Enter a cookie string directly (e.g., <code>name1=value1; name2=value2</code>).</li>
|
||||||
|
<li><b>Browse...:</b> Select a <code>cookies.txt</code> file (Netscape format). The path will appear in the text field.</li>
|
||||||
|
<li><b>Precedence:</b> The text field (if filled) takes precedence over a browsed file. If 'Use Cookie' is checked but both are empty, it attempts to load <code>cookies.txt</code> from the app's directory.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Use Multithreading & Threads Input:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Enables faster operations. The number in 'Threads' input means:
|
||||||
|
<ul>
|
||||||
|
<li>For <b>Creator Feeds:</b> Number of posts to process simultaneously. Files within each post are downloaded sequentially by its worker (unless 'Date Based' manga naming is on, which forces 1 post worker).</li>
|
||||||
|
<li>For <b>Single Post URLs:</b> Number of files to download concurrently from that single post.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>If unchecked, 1 thread is used. High thread counts (e.g., >40) may show an advisory.</li>
|
||||||
|
</ul>
|
||||||
|
</li></ul></ul></body></html>"""
|
||||||
|
|
||||||
|
page5_title = "⑤ Advanced Settings (Part 2) & Actions"
|
||||||
|
page5_content = """<html><head/><body><h3>⚙️ Advanced Settings (Continued)</h3><ul><ul>
|
||||||
|
<!-- Continuing from previous page's ul for Advanced Settings -->
|
||||||
|
<li><b>Show External Links in Log:</b> If checked, a secondary log panel appears below the main log to display any external links found in post descriptions. (Disabled if '🔗 Only Links' or '📦 Only Archives' mode is active).</li>
|
||||||
|
<li><b>📖 Manga/Comic Mode (Creator URLs only):</b> Tailored for sequential content.
|
||||||
|
<ul>
|
||||||
|
<li>Downloads posts from <b>oldest to newest</b>.</li>
|
||||||
|
<li>The 'Page Range' input is disabled as all posts are fetched.</li>
|
||||||
|
<li>A <b>filename style toggle button</b> (e.g., 'Name: Post Title') appears in the top-right of the log area when this mode is active for a creator feed. Click it to cycle through naming styles:
|
||||||
|
<ul>
|
||||||
|
<li><code>Name: Post Title (Default)</code>: The first file in a post is named after the post's title. Subsequent files in the same post keep original names.</li>
|
||||||
|
<li><code>Name: Original File</code>: All files attempt to keep their original filenames.</li>
|
||||||
|
<li><code>Name: Date Based</code>: Files are named sequentially (001.ext, 002.ext, ...) based on post publication order. Multithreading for post processing is automatically disabled for this style.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>For best results with 'Name: Post Title' or 'Name: Date Based' styles, use the 'Filter by Character(s)' field with the manga/series title for folder organization.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul></li></ul>
|
||||||
|
|
||||||
|
<h3>Main Action Buttons (Left Panel)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>⬇️ Start Download / 🔗 Extract Links:</b> This button's text and function change based on the 'Filter Files' radio button selection. It starts the primary operation.</li>
|
||||||
|
<li><b>⏸️ Pause Download / ▶️ Resume Download:</b> Allows you to temporarily halt the current download/extraction process and resume it later. Some UI settings can be changed while paused.</li>
|
||||||
|
<li><b>❌ Cancel & Reset UI:</b> Stops the current operation and performs a soft UI reset. Your URL and Download Directory inputs are preserved, but other settings and logs are cleared.</li>
|
||||||
|
</ul></body></html>"""
|
||||||
|
|
||||||
|
page6_title = "⑥ Known Shows/Characters List"
|
||||||
|
page6_content = """<html><head/><body>
|
||||||
|
<h3>Known Shows/Characters List Management (Bottom Left)</h3>
|
||||||
|
<p>This section helps manage the <code>Known.txt</code> file, which is used for smart folder organization when 'Separate Folders by Name/Title' is enabled, especially as a fallback if a post doesn't match your active 'Filter by Character(s)' input.</p>
|
||||||
|
<ul>
|
||||||
|
<li><b>Open Known.txt:</b> Opens the <code>Known.txt</code> file (located in the app's directory) in your default text editor for advanced editing (like creating complex grouped aliases).</li>
|
||||||
|
<li><b>Search characters...:</b> Filters the list of known names displayed below.</li>
|
||||||
|
<li><b>List Widget:</b> Displays the primary names from your <code>Known.txt</code>. Select entries here to delete them.</li>
|
||||||
|
<li><b>Add new show/character name (Input Field):</b> Enter a name or group to add.
|
||||||
|
<ul>
|
||||||
|
<li><b>Simple Name:</b> e.g., <code>My Awesome Series</code>. Adds as a single entry.</li>
|
||||||
|
<li><b>Group for Separate Known.txt Entries:</b> e.g., <code>(Vivi, Ulti, Uta)</code>. Adds "Vivi", "Ulti", and "Uta" as three separate individual entries to <code>Known.txt</code>.</li>
|
||||||
|
<li><b>Group for Shared Folder & Single Known.txt Entry (Tilde <code>~</code>):</b> e.g., <code>(Character A, Char A)~</code>. Adds one entry to <code>Known.txt</code> named "Character A Char A". "Character A" and "Char A" become aliases for this single folder/entry.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>➕ Add Button:</b> Adds the name/group from the input field above to the list and <code>Known.txt</code>.</li>
|
||||||
|
<li><b>🗑️ Delete Selected Button:</b> Deletes the selected name(s) from the list and <code>Known.txt</code>.</li>
|
||||||
|
<li><b>❓ Button (This one!):</b> Displays this comprehensive help guide.</li>
|
||||||
|
</ul></body></html>"""
|
||||||
|
|
||||||
|
page7_title = "⑦ Log Area & Controls"
|
||||||
|
page7_content = """<html><head/><body>
|
||||||
|
<h3>Log Area & Controls (Right Panel)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>📜 Progress Log / Extracted Links Log (Label):</b> Title for the main log area; changes if '🔗 Only Links' mode is active.</li>
|
||||||
|
<li><b>Search Links... / 🔍 Button (Link Search):</b>
|
||||||
|
<ul><li>Visible only when '🔗 Only Links' mode is active. Allows real-time filtering of the extracted links displayed in the main log by text, URL, or platform.</li></ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Name: [Style] Button (Manga Filename Style):</b>
|
||||||
|
<ul><li>Visible only when <b>Manga/Comic Mode</b> is active for a creator feed and not in 'Only Links' or 'Only Archives' mode.</li>
|
||||||
|
<li>Cycles through filename styles: <code>Post Title</code>, <code>Original File</code>, <code>Date Based</code>. (See Manga/Comic Mode section for details).</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Multi-part: [ON/OFF] Button:</b>
|
||||||
|
<ul><li>Toggles multi-segment downloads for individual large files.
|
||||||
|
<ul><li><b>ON:</b> Can speed up large file downloads but may increase UI choppiness or log spam with many small files. An advisory appears when enabling. If a multi-part download fails, it retries as single-stream.</li>
|
||||||
|
<li><b>OFF (Default):</b> Files are downloaded in a single stream.</li>
|
||||||
|
</ul>
|
||||||
|
<li>Disabled if '🔗 Only Links' or '📦 Only Archives' mode is active.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>👁️ / 🙈 Button (Log View Toggle):</b> Switches the main log view:
|
||||||
|
<ul>
|
||||||
|
<li><b>👁️ Progress Log (Default):</b> Shows all download activity, errors, and summaries.</li>
|
||||||
|
<li><b>🙈 Missed Character Log:</b> Displays a list of key terms from post titles/content that were skipped due to your 'Filter by Character(s)' settings. Useful for identifying content you might be unintentionally missing.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>🔄 Reset Button:</b> Clears all input fields, logs, and resets temporary settings to their defaults. Can only be used when no download is active.</li>
|
||||||
|
<li><b>Main Log Output (Text Area):</b> Displays detailed progress messages, errors, and summaries. If '🔗 Only Links' mode is active, this area displays the extracted links.</li>
|
||||||
|
<li><b>Missed Character Log Output (Text Area):</b> (Viewable via 👁️ / 🙈 toggle) Displays posts/files skipped due to character filters.</li>
|
||||||
|
<li><b>External Log Output (Text Area):</b> Appears below the main log if 'Show External Links in Log' is checked. Displays external links found in post descriptions.</li>
|
||||||
|
<li><b>Export Links Button:</b>
|
||||||
|
<ul><li>Visible and enabled only when '🔗 Only Links' mode is active and links have been extracted.</li>
|
||||||
|
<li>Allows you to save all extracted links to a <code>.txt</code> file.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Progress: [Status] Label:</b> Shows the overall progress of the download or link extraction process (e.g., posts processed).</li>
|
||||||
|
<li><b>File Progress Label:</b> Shows the progress of individual file downloads, including speed and size, or multi-part download status.</li>
|
||||||
|
</ul></body></html>"""
|
||||||
|
|
||||||
|
page8_title = "⑧ Key Files & Tour"
|
||||||
|
page8_content = """<html><head/><body>
|
||||||
|
<h3>Key Files Used by the Application</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b><code>Known.txt</code>:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Located in the application's directory (where the <code>.exe</code> or <code>main.py</code> is).</li>
|
||||||
|
<li>Stores your list of known shows, characters, or series titles for automatic folder organization when 'Separate Folders by Name/Title' is enabled.</li>
|
||||||
|
<li><b>Format:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Each line is an entry.</li>
|
||||||
|
<li><b>Simple Name:</b> e.g., <code>My Awesome Series</code>. Content matching this will go into a folder named "My Awesome Series".</li>
|
||||||
|
<li><b>Grouped Aliases:</b> e.g., <code>(Character A, Char A, Alt Name A)</code>. Content matching "Character A", "Char A", OR "Alt Name A" will ALL go into a single folder named "Character A Char A Alt Name A" (after cleaning). All terms in the parentheses become aliases for that folder.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b>Usage:</b> Serves as a fallback for folder naming if a post doesn't match your active 'Filter by Character(s)' input. You can manage simple entries via the UI or edit the file directly for complex aliases. The app reloads it on startup or next use.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><b><code>cookies.txt</code> (Optional):</b>
|
||||||
|
<ul>
|
||||||
|
<li>If you use the 'Use Cookie' feature and don't provide a direct cookie string or browse to a specific file, the application will look for a file named <code>cookies.txt</code> in its directory.</li>
|
||||||
|
<li><b>Format:</b> Must be in Netscape cookie file format.</li>
|
||||||
|
<li><b>Usage:</b> Allows the downloader to use your browser's login session for accessing content that might be behind a login on Kemono/Coomer.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>First-Time User Tour</h3>
|
||||||
|
<ul>
|
||||||
|
<li>On the first launch (or if reset), a welcome tour dialog appears, guiding you through the main features. You can skip it or choose to "Never show this tour again."</li>
|
||||||
|
</ul>
|
||||||
|
<p><em>Many UI elements also have tooltips that appear when you hover your mouse over them, providing quick hints.</em></p>
|
||||||
|
</body></html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
(page1_title, page1_content),
|
||||||
|
(page2_title, page2_content),
|
||||||
|
(page3_title, page3_content),
|
||||||
|
(page4_title, page4_content),
|
||||||
|
(page5_title, page5_content),
|
||||||
|
(page6_title, page6_content),
|
||||||
|
(page7_title, page7_content),
|
||||||
|
(page8_title, page8_content),
|
||||||
|
]
|
||||||
|
guide_dialog = HelpGuideDialog(steps, self)
|
||||||
|
guide_dialog.exec_()
|
||||||
|
|
||||||
def prompt_add_character(self, character_name):
|
def prompt_add_character(self, character_name):
|
||||||
global KNOWN_NAMES
|
global KNOWN_NAMES
|
||||||
reply = QMessageBox.question(self, "Add Filter Name to Known List?", f"The name '{character_name}' was encountered or used as a filter.\nIt's not in your known names list (used for folder suggestions).\nAdd it now?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
reply = QMessageBox.question(self, "Add Filter Name to Known List?", f"The name '{character_name}' was encountered or used as a filter.\nIt's not in your known names list (used for folder suggestions).\nAdd it now?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||||
@@ -3819,6 +4350,17 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import traceback
|
import traceback
|
||||||
|
import sys # Ensure sys is imported here if not already
|
||||||
|
import os # Ensure os is imported here
|
||||||
|
import time # For timestamping errors
|
||||||
|
|
||||||
|
def log_error_to_file(exc_info_tuple):
|
||||||
|
# Log file will be next to the .exe or main.py
|
||||||
|
log_file_path = os.path.join(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__), "critical_error_log.txt")
|
||||||
|
with open(log_file_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
|
traceback.print_exception(*exc_info_tuple, file=f)
|
||||||
|
f.write("-" * 80 + "\n\n")
|
||||||
try:
|
try:
|
||||||
qt_app = QApplication(sys.argv)
|
qt_app = QApplication(sys.argv)
|
||||||
if getattr(sys, 'frozen', False): base_dir = sys._MEIPASS
|
if getattr(sys, 'frozen', False): base_dir = sys._MEIPASS
|
||||||
|
|||||||
17
readme.md
17
readme.md
@@ -1,4 +1,4 @@
|
|||||||
<h1 align="center">Kemono Downloader v3.5.0</h1>
|
<h1 align="center">Kemono Downloader v4.0.0</h1>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://github.com/Yuvi9587/Kemono-Downloader/blob/main/Read.png" alt="Kemono Downloader"/>
|
<img src="https://github.com/Yuvi9587/Kemono-Downloader/blob/main/Read.png" alt="Kemono Downloader"/>
|
||||||
@@ -11,7 +11,7 @@ Built with **PyQt5**, this tool is ideal for users who want deep filtering, cust
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What's New in v3.5.0?
|
## What's New in v4.0.0?
|
||||||
|
|
||||||
Version 3.5.0 focuses on enhancing access to content and providing even smarter organization:
|
Version 3.5.0 focuses on enhancing access to content and providing even smarter organization:
|
||||||
|
|
||||||
@@ -39,11 +39,6 @@ The `Known.txt` file and the "Filter by Character(s)" input field work together
|
|||||||
- **Simple Entries:**
|
- **Simple Entries:**
|
||||||
- A line like `My Awesome Series` or `Nami`.
|
- A line like `My Awesome Series` or `Nami`.
|
||||||
- **Behavior:** Content matching this term will be saved into a folder named "My Awesome Series" or "Nami" respectively (if "Separate Folders" is enabled).
|
- **Behavior:** Content matching this term will be saved into a folder named "My Awesome Series" or "Nami" respectively (if "Separate Folders" is enabled).
|
||||||
- **Grouped Alias Entries (for a single character/entity):**
|
|
||||||
- Format: `(PrimaryFolderName, alias1, alias2, ...)`
|
|
||||||
- **Example:** `(Boa Hancock, Boa, Hancock)`
|
|
||||||
- **Behavior:** Content matching "Boa Hancock", "Boa", OR "Hancock" will be saved into a folder named "Boa Hancock". The first item in the parentheses is the primary folder name; all items are matching aliases.
|
|
||||||
- **Example:** `(Power, powwr, pwr, Blood Devil)` creates a folder "Power" for content matching any of those terms.
|
|
||||||
|
|
||||||
**2. "Filter by Character(s)" UI Input Field:**
|
**2. "Filter by Character(s)" UI Input Field:**
|
||||||
|
|
||||||
@@ -58,7 +53,7 @@ This field allows for dynamic filtering for the current download session and pro
|
|||||||
- Input: `(Boa, Hancock)~`
|
- Input: `(Boa, Hancock)~`
|
||||||
- Meaning: "Boa" and "Hancock" are different names/aliases for the *same character*. The names are listed within parentheses separated by commas (e.g., `name1, alias1, alias2`), and the entire group is followed by a `~` symbol. This is useful when a creator uses different names for the same character.
|
- Meaning: "Boa" and "Hancock" are different names/aliases for the *same character*. The names are listed within parentheses separated by commas (e.g., `name1, alias1, alias2`), and the entire group is followed by a `~` symbol. This is useful when a creator uses different names for the same character.
|
||||||
- Session Behavior: Filters for "Boa" OR "Hancock". If "Separate Folders" is on, creates a single folder named "Boa Hancock".
|
- Session Behavior: Filters for "Boa" OR "Hancock". If "Separate Folders" is on, creates a single folder named "Boa Hancock".
|
||||||
- `Known.txt` Addition: If this group is new and selected for addition, it's added to `Known.txt` as a grouped alias entry, typically `(Boa Hancock, Boa, Hancock)`. The first name in the `Known.txt` entry (e.g., "Boa Hancock") becomes the primary folder name.
|
- `Known.txt` Addition: If this group is new and selected for addition, it's added to `Known.txt` as a grouped alias entry, typically `(Boa Hancock)`. The first name in the `Known.txt` entry (e.g., "Boa Hancock") becomes the primary folder name.
|
||||||
|
|
||||||
- **Combined Folder for Distinct Characters (using `(...)` syntax):**
|
- **Combined Folder for Distinct Characters (using `(...)` syntax):**
|
||||||
- Input: `(Vivi, Uta)`
|
- Input: `(Vivi, Uta)`
|
||||||
@@ -78,7 +73,7 @@ This field allows for dynamic filtering for the current download session and pro
|
|||||||
- **Direct Management:** You can add simple entries directly to `Known.txt` using the list and "Add" button in the UI's `Known.txt` management section. For creating or modifying complex grouped alias entries directly in the file, or for bulk edits, click the "Open Known.txt" button. The application reloads `Known.txt` on startup or before a download process begins.
|
- **Direct Management:** You can add simple entries directly to `Known.txt` using the list and "Add" button in the UI's `Known.txt` management section. For creating or modifying complex grouped alias entries directly in the file, or for bulk edits, click the "Open Known.txt" button. The application reloads `Known.txt` on startup or before a download process begins.
|
||||||
|
|
||||||
---
|
---
|
||||||
## What's in v3.4.0? (Previous Update)
|
## What's in v3.5.0? (Previous Update)
|
||||||
This version brings significant enhancements to manga/comic downloading, filtering capabilities, and user experience:
|
This version brings significant enhancements to manga/comic downloading, filtering capabilities, and user experience:
|
||||||
|
|
||||||
### Enhanced Manga/Comic Mode
|
### Enhanced Manga/Comic Mode
|
||||||
@@ -135,7 +130,7 @@ This version brings significant enhancements to manga/comic downloading, filteri
|
|||||||
|
|
||||||
### Updated Onboarding Tour
|
### Updated Onboarding Tour
|
||||||
|
|
||||||
- Improved guide for new users, covering v3.4.0 features and existing core functions.
|
- Improved guide for new users, covering v4.0.0 features and existing core functions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -172,7 +167,7 @@ This version brings significant enhancements to manga/comic downloading, filteri
|
|||||||
- Flexible input for current session and for adding to `Known.txt`.
|
- Flexible input for current session and for adding to `Known.txt`.
|
||||||
- Examples:
|
- Examples:
|
||||||
- `Nami` (simple character)
|
- `Nami` (simple character)
|
||||||
- `(Boa ~ Hancock)` (aliases for one character, session folder "Boa Hancock", adds `(Boa Hancock, Boa, Hancock)` to `Known.txt`)
|
- `(Boa Hancock)~` (aliases for one character, session folder "Boa Hancock", adds `(Boa Hancock)` to `Known.txt`)
|
||||||
- `(Vivi, Uta)` (distinct characters, session folder "Vivi Uta", adds `Vivi` and `Uta` separately to `Known.txt`)
|
- `(Vivi, Uta)` (distinct characters, session folder "Vivi Uta", adds `Vivi` and `Uta` separately to `Known.txt`)
|
||||||
- See "Advanced `Known.txt` and Character Filtering" for full details.
|
- See "Advanced `Known.txt` and Character Filtering" for full details.
|
||||||
- **Filter Scopes:**
|
- **Filter Scopes:**
|
||||||
|
|||||||
Reference in New Issue
Block a user